Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/dulwich/refs.py: 32%

Shortcuts on this page

r m x   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

835 statements  

1# refs.py -- For dealing with git refs 

2# Copyright (C) 2008-2013 Jelmer Vernooij <jelmer@jelmer.uk> 

3# 

4# SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later 

5# Dulwich is dual-licensed under the Apache License, Version 2.0 and the GNU 

6# General Public License as published by the Free Software Foundation; version 2.0 

7# or (at your option) any later version. You can redistribute it and/or 

8# modify it under the terms of either of these two licenses. 

9# 

10# Unless required by applicable law or agreed to in writing, software 

11# distributed under the License is distributed on an "AS IS" BASIS, 

12# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 

13# See the License for the specific language governing permissions and 

14# limitations under the License. 

15# 

16# You should have received a copy of the licenses; if not, see 

17# <http://www.gnu.org/licenses/> for a copy of the GNU General Public License 

18# and <http://www.apache.org/licenses/LICENSE-2.0> for a copy of the Apache 

19# License, Version 2.0. 

20# 

21 

22 

23"""Ref handling.""" 

24 

25__all__ = [ 

26 "BAD_REF_CHARS", 

27 "HEADREF", 

28 "LOCAL_BRANCH_PREFIX", 

29 "LOCAL_NOTES_PREFIX", 

30 "LOCAL_REMOTE_PREFIX", 

31 "LOCAL_REPLACE_PREFIX", 

32 "LOCAL_TAG_PREFIX", 

33 "SYMREF", 

34 "DictRefsContainer", 

35 "DiskRefsContainer", 

36 "NamespacedRefsContainer", 

37 "Ref", 

38 "RefsContainer", 

39 "SymrefLoop", 

40 "check_ref_format", 

41 "extract_branch_name", 

42 "extract_tag_name", 

43 "filter_ref_prefix", 

44 "is_local_branch", 

45 "is_per_worktree_ref", 

46 "local_branch_name", 

47 "local_replace_name", 

48 "local_tag_name", 

49 "locked_ref", 

50 "parse_remote_ref", 

51 "parse_symref_value", 

52 "read_info_refs", 

53 "read_packed_refs", 

54 "read_packed_refs_with_peeled", 

55 "refname_is_safe", 

56 "set_ref_from_raw", 

57 "shorten_ref_name", 

58 "write_packed_refs", 

59] 

60 

61import os 

62import sys 

63import types 

64import warnings 

65from collections.abc import Callable, Iterable, Iterator, Mapping 

66from contextlib import suppress 

67from typing import ( 

68 IO, 

69 TYPE_CHECKING, 

70 Any, 

71 BinaryIO, 

72 NewType, 

73 overload, 

74) 

75 

76if sys.version_info >= (3, 11): 

77 from typing import Self 

78else: 

79 from typing_extensions import Self 

80 

81if TYPE_CHECKING: 

82 from .config import ConfigFile 

83 from .file import _GitFile 

84 

85from .errors import PackedRefsException, RefFormatError 

86from .file import GitFile, ensure_dir_exists 

87from .objects import ZERO_SHA, ObjectID, git_line, valid_hexsha 

88 

89Ref = NewType("Ref", bytes) 

90 

91HEADREF = Ref(b"HEAD") 

92SYMREF = b"ref: " 

93LOCAL_BRANCH_PREFIX = b"refs/heads/" 

94LOCAL_TAG_PREFIX = b"refs/tags/" 

95LOCAL_REMOTE_PREFIX = b"refs/remotes/" 

96LOCAL_NOTES_PREFIX = b"refs/notes/" 

97LOCAL_REPLACE_PREFIX = b"refs/replace/" 

98BAD_REF_CHARS: set[int] = set(b"\177 ~^:?*[") 

99 

100 

101class SymrefLoop(Exception): 

102 """There is a loop between one or more symrefs.""" 

103 

104 def __init__(self, ref: bytes, depth: int) -> None: 

105 """Initialize SymrefLoop exception.""" 

106 self.ref = ref 

107 self.depth = depth 

108 

109 

110def parse_symref_value(contents: bytes) -> bytes: 

111 """Parse a symref value. 

112 

113 Args: 

114 contents: Contents to parse 

115 Returns: Destination 

116 """ 

117 if contents.startswith(SYMREF): 

118 return contents[len(SYMREF) :].rstrip(b"\r\n") 

119 raise ValueError(contents) 

120 

121 

122def check_ref_format(refname: Ref) -> bool: 

123 """Check if a refname is correctly formatted. 

124 

125 Implements all the same rules as git-check-ref-format[1]. 

126 

127 [1] 

128 http://www.kernel.org/pub/software/scm/git/docs/git-check-ref-format.html 

129 

130 Args: 

131 refname: The refname to check 

132 Returns: True if refname is valid, False otherwise 

133 """ 

134 # These could be combined into one big expression, but are listed 

135 # separately to parallel [1]. 

136 if refname == b"@": 

137 return False 

138 if b"/" not in refname: # type: ignore[comparison-overlap] 

139 return False 

140 if b".." in refname: # type: ignore[comparison-overlap] 

141 return False 

142 for i, c in enumerate(refname): 

143 if ord(refname[i : i + 1]) < 0o40 or c in BAD_REF_CHARS: 

144 return False 

145 if refname[-1] in b"/.": 

146 return False 

147 if b"@{" in refname: # type: ignore[comparison-overlap] 

148 return False 

149 if b"\\" in refname: # type: ignore[comparison-overlap] 

150 return False 

151 for component in refname.split(b"/"): 

152 if not component: 

153 return False 

154 if component.startswith(b"."): 

155 return False 

156 if component.endswith(b".lock"): 

157 return False 

158 return True 

159 

160 

161def _collapse_slashes(refname: bytes) -> bytes: 

162 """Collapse runs of consecutive slashes in a ref name into a single slash.""" 

163 return b"/".join(component for component in refname.split(b"/") if component) 

164 

165 

166# git's isupper() is ASCII-only, so non-ASCII (e.g. UTF-8) bytes never match. 

167_PSEUDOREF_CHARS = frozenset(b"ABCDEFGHIJKLMNOPQRSTUVWXYZ_") 

168 

169 

170def refname_is_safe(refname: Ref) -> bool: 

171 """Check if a refname is safe to use as a path within the ref store. 

172 

173 This is the equivalent of git's ``refname_is_safe()``: a weaker check than 

174 :func:`check_ref_format`, meant for refnames that have already been 

175 resolved (such as the target of a symref read from disk) and only 

176 establishes that turning the name into a path cannot escape the ref store. 

177 

178 A name under ``refs/`` must already be normalized, so that neither 

179 ``refs/../x`` nor ``refs/heads/../../x`` is accepted. Any other name must 

180 be a pseudoref, i.e. consist only of uppercase letters and underscores 

181 (e.g. ``HEAD`` or ``MERGE_HEAD``). 

182 

183 Args: 

184 refname: The refname to check 

185 Returns: True if the refname is safe, False otherwise 

186 """ 

187 if refname.startswith(b"refs/"): 

188 rest = refname[len(b"refs/") :] 

189 # rest must not be empty or end with "/" 

190 if not rest or rest.endswith(b"/"): 

191 return False 

192 # refpath() maps "/" onto os.sep, so on Windows a backslash would act 

193 # as a second separator; check_ref_format rejects it as well. 

194 if b"\\" in rest: 

195 return False 

196 # Unlike git, repeated slashes are tolerated here rather than treated 

197 # as a denormalized name, because _check_refname still only deprecates 

198 # them; only "." and ".." components make a name unsafe. 

199 return not any(c in (b".", b"..") for c in rest.split(b"/")) 

200 return bool(refname) and set(refname) <= _PSEUDOREF_CHARS 

201 

202 

203def parse_remote_ref(ref: bytes) -> tuple[bytes, bytes]: 

204 """Parse a remote ref into remote name and branch name. 

205 

206 Args: 

207 ref: Remote ref like b"refs/remotes/origin/main" 

208 

209 Returns: 

210 Tuple of (remote_name, branch_name) 

211 

212 Raises: 

213 ValueError: If ref is not a valid remote ref 

214 """ 

215 if not ref.startswith(LOCAL_REMOTE_PREFIX): 

216 raise ValueError(f"Not a remote ref: {ref!r}") 

217 

218 # Remove the prefix 

219 remainder = ref[len(LOCAL_REMOTE_PREFIX) :] 

220 

221 # Split into remote name and branch name 

222 parts = remainder.split(b"/", 1) 

223 if len(parts) != 2: 

224 raise ValueError(f"Invalid remote ref format: {ref!r}") 

225 

226 remote_name, branch_name = parts 

227 return (remote_name, branch_name) 

228 

229 

230def set_ref_from_raw(refs: "RefsContainer", name: Ref, raw_ref: bytes) -> None: 

231 """Set a reference from a raw ref value. 

232 

233 This handles both symbolic refs (starting with 'ref: ') and direct ObjectID refs. 

234 

235 Args: 

236 refs: The RefsContainer to set the ref in 

237 name: The ref name to set 

238 raw_ref: The raw ref value (either a symbolic ref or an ObjectID) 

239 """ 

240 if raw_ref.startswith(SYMREF): 

241 # It's a symbolic ref 

242 target = Ref(raw_ref[len(SYMREF) :]) 

243 refs.set_symbolic_ref(name, target) 

244 else: 

245 # It's a direct ObjectID 

246 refs[name] = ObjectID(raw_ref) 

247 

248 

249class RefsContainer: 

250 """A container for refs.""" 

251 

252 def __init__( 

253 self, 

254 logger: Callable[ 

255 [bytes, bytes, bytes, bytes | None, int | None, int | None, bytes], None 

256 ] 

257 | None = None, 

258 ) -> None: 

259 """Initialize RefsContainer with optional logger function.""" 

260 self._logger = logger 

261 

262 def _log( 

263 self, 

264 ref: bytes, 

265 old_sha: bytes | None, 

266 new_sha: bytes | None, 

267 committer: bytes | None = None, 

268 timestamp: int | None = None, 

269 timezone: int | None = None, 

270 message: bytes | None = None, 

271 ) -> None: 

272 if self._logger is None: 

273 return 

274 if message is None: 

275 return 

276 # Use ZERO_SHA for None values, matching git behavior 

277 if old_sha is None: 

278 old_sha = ZERO_SHA 

279 if new_sha is None: 

280 new_sha = ZERO_SHA 

281 self._logger(ref, old_sha, new_sha, committer, timestamp, timezone, message) 

282 

283 def set_symbolic_ref( 

284 self, 

285 name: Ref, 

286 other: Ref, 

287 committer: bytes | None = None, 

288 timestamp: int | None = None, 

289 timezone: int | None = None, 

290 message: bytes | None = None, 

291 ) -> None: 

292 """Make a ref point at another ref. 

293 

294 Args: 

295 name: Name of the ref to set 

296 other: Name of the ref to point at 

297 committer: Optional committer name/email 

298 timestamp: Optional timestamp 

299 timezone: Optional timezone 

300 message: Optional message 

301 """ 

302 raise NotImplementedError(self.set_symbolic_ref) 

303 

304 def get_packed_refs(self) -> dict[Ref, ObjectID]: 

305 """Get contents of the packed-refs file. 

306 

307 Returns: Dictionary mapping ref names to SHA1s 

308 

309 Note: Will return an empty dictionary when no packed-refs file is 

310 present. 

311 """ 

312 raise NotImplementedError(self.get_packed_refs) 

313 

314 def add_packed_refs(self, new_refs: Mapping[Ref, ObjectID | None]) -> None: 

315 """Add the given refs as packed refs. 

316 

317 Args: 

318 new_refs: A mapping of ref names to targets; if a target is None that 

319 means remove the ref 

320 """ 

321 raise NotImplementedError(self.add_packed_refs) 

322 

323 def get_peeled(self, name: Ref) -> ObjectID | None: 

324 """Return the cached peeled value of a ref, if available. 

325 

326 Args: 

327 name: Name of the ref to peel 

328 Returns: The peeled value of the ref. If the ref is known not point to 

329 a tag, this will be the SHA the ref refers to. If the ref may point 

330 to a tag, but no cached information is available, None is returned. 

331 """ 

332 return None 

333 

334 def import_refs( 

335 self, 

336 base: Ref, 

337 other: Mapping[Ref, ObjectID | None], 

338 committer: bytes | None = None, 

339 timestamp: bytes | None = None, 

340 timezone: bytes | None = None, 

341 message: bytes | None = None, 

342 prune: bool = False, 

343 ) -> None: 

344 """Import refs from another repository. 

345 

346 Args: 

347 base: Base ref to import into (e.g., b'refs/remotes/origin') 

348 other: Dictionary of refs to import 

349 committer: Optional committer for reflog 

350 timestamp: Optional timestamp for reflog 

351 timezone: Optional timezone for reflog 

352 message: Optional message for reflog 

353 prune: If True, remove refs not in other 

354 """ 

355 # Strip a trailing slash so joining ``base`` with a ref name below 

356 # does not produce a malformed ref with an empty path component 

357 # (e.g. b'refs/tags/' + b'/' + b'v1.0' -> b'refs/tags//v1.0'). 

358 base = Ref(base.rstrip(b"/")) 

359 if prune: 

360 to_delete = set(self.subkeys(base)) 

361 else: 

362 to_delete = set() 

363 for name, value in other.items(): 

364 if value is None: 

365 to_delete.add(name) 

366 else: 

367 self.set_if_equals( 

368 Ref(b"/".join((base, name))), None, value, message=message 

369 ) 

370 if to_delete: 

371 try: 

372 to_delete.remove(name) 

373 except KeyError: 

374 pass 

375 for ref in to_delete: 

376 self.remove_if_equals(Ref(b"/".join((base, ref))), None, message=message) 

377 

378 def allkeys(self) -> set[Ref]: 

379 """All refs present in this container.""" 

380 raise NotImplementedError(self.allkeys) 

381 

382 def __iter__(self) -> Iterator[Ref]: 

383 """Iterate over all reference keys.""" 

384 return iter(self.allkeys()) 

385 

386 def keys(self, base: Ref | None = None) -> set[Ref]: 

387 """Refs present in this container. 

388 

389 Args: 

390 base: An optional base to return refs under. 

391 Returns: An unsorted set of valid refs in this container, including 

392 packed refs. 

393 """ 

394 if base is not None: 

395 return self.subkeys(base) 

396 else: 

397 return self.allkeys() 

398 

399 def subkeys(self, base: Ref) -> set[Ref]: 

400 """Refs present in this container under a base. 

401 

402 Args: 

403 base: The base to return refs under. 

404 Returns: A set of valid refs in this container under the base; the base 

405 prefix is stripped from the ref names returned. 

406 """ 

407 keys: set[Ref] = set() 

408 base_len = len(base) + 1 

409 for refname in self.allkeys(): 

410 if refname.startswith(base): 

411 keys.add(Ref(refname[base_len:])) 

412 return keys 

413 

414 def as_dict(self, base: Ref | None = None) -> dict[Ref, ObjectID]: 

415 """Return the contents of this container as a dictionary.""" 

416 ret: dict[Ref, ObjectID] = {} 

417 keys = self.keys(base) 

418 base_bytes: bytes 

419 if base is None: 

420 base_bytes = b"" 

421 else: 

422 base_bytes = base.rstrip(b"/") 

423 for key in keys: 

424 try: 

425 ret[key] = self[Ref((base_bytes + b"/" + key).strip(b"/"))] 

426 except (SymrefLoop, KeyError): 

427 continue # Unable to resolve 

428 

429 return ret 

430 

431 def _check_refname(self, name: Ref) -> None: 

432 """Ensure a refname is valid and lives in refs or is HEAD. 

433 

434 HEAD is not a valid refname according to git-check-ref-format, but this 

435 class needs to be able to touch HEAD. Also, check_ref_format expects 

436 refnames without the leading 'refs/', but this class requires that 

437 so it cannot touch anything outside the refs dir (or HEAD). 

438 

439 Args: 

440 name: The name of the reference. 

441 

442 Raises: 

443 KeyError: if a refname is not HEAD or is otherwise not valid. 

444 """ 

445 if name in (HEADREF, Ref(b"refs/stash")): 

446 return 

447 if not name.startswith(b"refs/"): 

448 raise RefFormatError(name) 

449 rest = Ref(name[5:]) 

450 if check_ref_format(rest): 

451 return 

452 # As of Dulwich 1.2.3 check_ref_format rejects empty path components 

453 # (e.g. b'refs/tags//v1.0'). Such names were silently accepted before, 

454 # and some callers (e.g. older Poetry releases) still construct them. 

455 # Warn rather than raise for now if collapsing repeated slashes would 

456 # make the name valid, so the only defect is empty components. 

457 if ( 

458 b"//" in name # type: ignore[comparison-overlap] 

459 and check_ref_format(Ref(_collapse_slashes(rest))) 

460 ): 

461 warnings.warn( 

462 f"Ref name {name!r} contains empty path components; " 

463 "this will be rejected in a future version of Dulwich.", 

464 DeprecationWarning, 

465 stacklevel=3, 

466 ) 

467 return 

468 raise RefFormatError(name) 

469 

470 def _check_ref_value(self, ref: ObjectID) -> None: 

471 """Ensure a ref value is a valid object id or a symref. 

472 

473 Args: 

474 ref: The value a ref is being set to. 

475 

476 Raises: 

477 ValueError: if the value is neither a valid sha nor a symref. 

478 """ 

479 if not (valid_hexsha(ref) or ref.startswith(SYMREF)): 

480 raise ValueError(f"{ref!r} must be a valid sha or a symref") 

481 

482 def read_ref(self, refname: Ref) -> bytes | None: 

483 """Read a reference without following any references. 

484 

485 Args: 

486 refname: The name of the reference 

487 Returns: The contents of the ref file, or None if it does 

488 not exist. 

489 """ 

490 contents = self.read_loose_ref(refname) 

491 if not contents: 

492 contents = self.get_packed_refs().get(refname, None) 

493 return contents 

494 

495 def read_loose_ref(self, name: Ref) -> bytes | None: 

496 """Read a loose reference and return its contents. 

497 

498 Args: 

499 name: the refname to read 

500 Returns: The contents of the ref file, or None if it does 

501 not exist. 

502 """ 

503 raise NotImplementedError(self.read_loose_ref) 

504 

505 def follow(self, name: Ref) -> tuple[list[Ref], ObjectID | None]: 

506 """Follow a reference name. 

507 

508 Returns: a tuple of (refnames, sha), wheres refnames are the names of 

509 references in the chain 

510 """ 

511 contents: bytes | None = SYMREF + name 

512 depth = 0 

513 refnames: list[Ref] = [] 

514 while contents and contents.startswith(SYMREF): 

515 refname = Ref(contents[len(SYMREF) :]) 

516 refnames.append(refname) 

517 contents = self.read_ref(refname) 

518 if not contents: 

519 break 

520 depth += 1 

521 if depth > 5: 

522 raise SymrefLoop(name, depth) 

523 return refnames, ObjectID(contents) if contents else None 

524 

525 def __contains__(self, refname: Ref) -> bool: 

526 """Check if a reference exists.""" 

527 if self.read_ref(refname): 

528 return True 

529 return False 

530 

531 def __getitem__(self, name: Ref) -> ObjectID: 

532 """Get the SHA1 for a reference name. 

533 

534 This method follows all symbolic references. 

535 """ 

536 _, sha = self.follow(name) 

537 if sha is None: 

538 raise KeyError(name) 

539 return sha 

540 

541 def set_if_equals( 

542 self, 

543 name: Ref, 

544 old_ref: ObjectID | None, 

545 new_ref: ObjectID, 

546 committer: bytes | None = None, 

547 timestamp: int | None = None, 

548 timezone: int | None = None, 

549 message: bytes | None = None, 

550 ) -> bool: 

551 """Set a refname to new_ref only if it currently equals old_ref. 

552 

553 This method follows all symbolic references if applicable for the 

554 subclass, and can be used to perform an atomic compare-and-swap 

555 operation. 

556 

557 Args: 

558 name: The refname to set. 

559 old_ref: The old sha the refname must refer to, or None to set 

560 unconditionally. 

561 new_ref: The new sha the refname will refer to. 

562 committer: Optional committer name/email 

563 timestamp: Optional timestamp 

564 timezone: Optional timezone 

565 message: Message for reflog 

566 Returns: True if the set was successful, False otherwise. 

567 """ 

568 raise NotImplementedError(self.set_if_equals) 

569 

570 def add_if_new( 

571 self, 

572 name: Ref, 

573 ref: ObjectID, 

574 committer: bytes | None = None, 

575 timestamp: int | None = None, 

576 timezone: int | None = None, 

577 message: bytes | None = None, 

578 ) -> bool: 

579 """Add a new reference only if it does not already exist. 

580 

581 Args: 

582 name: Ref name 

583 ref: Ref value 

584 committer: Optional committer name/email 

585 timestamp: Optional timestamp 

586 timezone: Optional timezone 

587 message: Optional message for reflog 

588 """ 

589 raise NotImplementedError(self.add_if_new) 

590 

591 def __setitem__(self, name: Ref, ref: ObjectID) -> None: 

592 """Set a reference name to point to the given SHA1. 

593 

594 This method follows all symbolic references if applicable for the 

595 subclass. 

596 

597 Note: This method unconditionally overwrites the contents of a 

598 reference. To update atomically only if the reference has not 

599 changed, use set_if_equals(). 

600 

601 Args: 

602 name: The refname to set. 

603 ref: The new sha the refname will refer to. 

604 """ 

605 self._check_ref_value(ref) 

606 self.set_if_equals(name, None, ref) 

607 

608 def remove_if_equals( 

609 self, 

610 name: Ref, 

611 old_ref: ObjectID | None, 

612 committer: bytes | None = None, 

613 timestamp: int | None = None, 

614 timezone: int | None = None, 

615 message: bytes | None = None, 

616 ) -> bool: 

617 """Remove a refname only if it currently equals old_ref. 

618 

619 This method does not follow symbolic references, even if applicable for 

620 the subclass. It can be used to perform an atomic compare-and-delete 

621 operation. 

622 

623 Args: 

624 name: The refname to delete. 

625 old_ref: The old sha the refname must refer to, or None to 

626 delete unconditionally. 

627 committer: Optional committer name/email 

628 timestamp: Optional timestamp 

629 timezone: Optional timezone 

630 message: Message for reflog 

631 Returns: True if the delete was successful, False otherwise. 

632 """ 

633 raise NotImplementedError(self.remove_if_equals) 

634 

635 def __delitem__(self, name: Ref) -> None: 

636 """Remove a refname. 

637 

638 This method does not follow symbolic references, even if applicable for 

639 the subclass. 

640 

641 Note: This method unconditionally deletes the contents of a reference. 

642 To delete atomically only if the reference has not changed, use 

643 remove_if_equals(). 

644 

645 Args: 

646 name: The refname to delete. 

647 """ 

648 self.remove_if_equals(name, None) 

649 

650 def get_symrefs(self) -> dict[Ref, Ref]: 

651 """Get a dict with all symrefs in this container. 

652 

653 Returns: Dictionary mapping source ref to target ref 

654 """ 

655 ret: dict[Ref, Ref] = {} 

656 for src in self.allkeys(): 

657 try: 

658 ref_value = self.read_ref(src) 

659 assert ref_value is not None 

660 dst = parse_symref_value(ref_value) 

661 except ValueError: 

662 pass 

663 else: 

664 ret[src] = Ref(dst) 

665 return ret 

666 

667 def pack_refs(self, all: bool = False) -> None: 

668 """Pack loose refs into packed-refs file. 

669 

670 Args: 

671 all: If True, pack all refs. If False, only pack tags. 

672 """ 

673 raise NotImplementedError(self.pack_refs) 

674 

675 

676class DictRefsContainer(RefsContainer): 

677 """RefsContainer backed by a simple dict. 

678 

679 This container does not support symbolic or packed references and is not 

680 threadsafe. 

681 """ 

682 

683 def __init__( 

684 self, 

685 refs: dict[Ref, bytes], 

686 logger: Callable[ 

687 [ 

688 bytes, 

689 bytes | None, 

690 bytes | None, 

691 bytes | None, 

692 int | None, 

693 int | None, 

694 bytes | None, 

695 ], 

696 None, 

697 ] 

698 | None = None, 

699 ) -> None: 

700 """Initialize DictRefsContainer with refs dictionary and optional logger.""" 

701 super().__init__(logger=logger) 

702 self._refs = refs 

703 self._peeled: dict[Ref, ObjectID] = {} 

704 self._watchers: set[Any] = set() 

705 

706 def allkeys(self) -> set[Ref]: 

707 """Return all reference keys.""" 

708 return set(self._refs.keys()) 

709 

710 def read_loose_ref(self, name: Ref) -> bytes | None: 

711 """Read a loose reference.""" 

712 return self._refs.get(name, None) 

713 

714 def get_packed_refs(self) -> dict[Ref, ObjectID]: 

715 """Get packed references.""" 

716 return {} 

717 

718 def _notify(self, ref: bytes, newsha: bytes | None) -> None: 

719 for watcher in self._watchers: 

720 watcher._notify((ref, newsha)) 

721 

722 def set_symbolic_ref( 

723 self, 

724 name: Ref, 

725 other: Ref, 

726 committer: bytes | None = None, 

727 timestamp: int | None = None, 

728 timezone: int | None = None, 

729 message: bytes | None = None, 

730 ) -> None: 

731 """Make a ref point at another ref. 

732 

733 Args: 

734 name: Name of the ref to set 

735 other: Name of the ref to point at 

736 committer: Optional committer name for reflog 

737 timestamp: Optional timestamp for reflog 

738 timezone: Optional timezone for reflog 

739 message: Optional message for reflog 

740 """ 

741 old = self.follow(name)[-1] 

742 new = SYMREF + other 

743 self._refs[name] = new 

744 self._notify(name, new) 

745 self._log( 

746 name, 

747 old, 

748 new, 

749 committer=committer, 

750 timestamp=timestamp, 

751 timezone=timezone, 

752 message=message, 

753 ) 

754 

755 def set_if_equals( 

756 self, 

757 name: Ref, 

758 old_ref: ObjectID | None, 

759 new_ref: ObjectID, 

760 committer: bytes | None = None, 

761 timestamp: int | None = None, 

762 timezone: int | None = None, 

763 message: bytes | None = None, 

764 ) -> bool: 

765 """Set a refname to new_ref only if it currently equals old_ref. 

766 

767 This method follows all symbolic references, and can be used to perform 

768 an atomic compare-and-swap operation. 

769 

770 Args: 

771 name: The refname to set. 

772 old_ref: The old sha the refname must refer to, or None to set 

773 unconditionally. 

774 new_ref: The new sha the refname will refer to. 

775 committer: Optional committer name for reflog 

776 timestamp: Optional timestamp for reflog 

777 timezone: Optional timezone for reflog 

778 message: Optional message for reflog 

779 

780 Returns: 

781 True if the set was successful, False otherwise. 

782 """ 

783 self._check_ref_value(new_ref) 

784 if old_ref is not None and self._refs.get(name, ZERO_SHA) != old_ref: 

785 return False 

786 # Only update the specific ref requested, not the whole chain 

787 self._check_refname(name) 

788 old = self._refs.get(name) 

789 self._refs[name] = new_ref 

790 self._notify(name, new_ref) 

791 self._log( 

792 name, 

793 old, 

794 new_ref, 

795 committer=committer, 

796 timestamp=timestamp, 

797 timezone=timezone, 

798 message=message, 

799 ) 

800 return True 

801 

802 def add_if_new( 

803 self, 

804 name: Ref, 

805 ref: ObjectID, 

806 committer: bytes | None = None, 

807 timestamp: int | None = None, 

808 timezone: int | None = None, 

809 message: bytes | None = None, 

810 ) -> bool: 

811 """Add a new reference only if it does not already exist. 

812 

813 Args: 

814 name: Ref name 

815 ref: Ref value 

816 committer: Optional committer name for reflog 

817 timestamp: Optional timestamp for reflog 

818 timezone: Optional timezone for reflog 

819 message: Optional message for reflog 

820 

821 Returns: 

822 True if the add was successful, False otherwise. 

823 """ 

824 self._check_ref_value(ref) 

825 if name in self._refs: 

826 return False 

827 self._refs[name] = ref 

828 self._notify(name, ref) 

829 self._log( 

830 name, 

831 None, 

832 ref, 

833 committer=committer, 

834 timestamp=timestamp, 

835 timezone=timezone, 

836 message=message, 

837 ) 

838 return True 

839 

840 def remove_if_equals( 

841 self, 

842 name: Ref, 

843 old_ref: ObjectID | None, 

844 committer: bytes | None = None, 

845 timestamp: int | None = None, 

846 timezone: int | None = None, 

847 message: bytes | None = None, 

848 ) -> bool: 

849 """Remove a refname only if it currently equals old_ref. 

850 

851 This method does not follow symbolic references. It can be used to 

852 perform an atomic compare-and-delete operation. 

853 

854 Args: 

855 name: The refname to delete. 

856 old_ref: The old sha the refname must refer to, or None to 

857 delete unconditionally. 

858 committer: Optional committer name for reflog 

859 timestamp: Optional timestamp for reflog 

860 timezone: Optional timezone for reflog 

861 message: Optional message for reflog 

862 

863 Returns: 

864 True if the delete was successful, False otherwise. 

865 """ 

866 if old_ref is not None and self._refs.get(name, ZERO_SHA) != old_ref: 

867 return False 

868 try: 

869 old = self._refs.pop(name) 

870 except KeyError: 

871 pass 

872 else: 

873 self._notify(name, None) 

874 self._log( 

875 name, 

876 old, 

877 None, 

878 committer=committer, 

879 timestamp=timestamp, 

880 timezone=timezone, 

881 message=message, 

882 ) 

883 return True 

884 

885 def get_peeled(self, name: Ref) -> ObjectID | None: 

886 """Get peeled version of a reference.""" 

887 return self._peeled.get(name) 

888 

889 def _update(self, refs: Mapping[Ref, ObjectID]) -> None: 

890 """Update multiple refs; intended only for testing.""" 

891 # TODO(dborowitz): replace this with a public function that uses 

892 # set_if_equal. 

893 for ref, sha in refs.items(): 

894 self.set_if_equals(ref, None, sha) 

895 

896 def _update_peeled(self, peeled: Mapping[Ref, ObjectID]) -> None: 

897 """Update cached peeled refs; intended only for testing.""" 

898 self._peeled.update(peeled) 

899 

900 

901#: Identity of a particular packed-refs file, used to detect that the file a 

902#: cache was populated from has since been replaced. 

903_PackedRefsKey = tuple[int, int, int, int, int] 

904 

905 

906def _packed_refs_key(st: os.stat_result) -> _PackedRefsKey: 

907 """Build a key identifying the packed-refs file described by ``st``. 

908 

909 The fields mirror those git compares in ``match_stat_data()`` when 

910 deciding whether a file it has cached has been replaced. Size alone is a 

911 weak signal here, since repacking refs often produces a file of identical 

912 length, so the inode and timestamps do most of the work. 

913 

914 Args: 

915 st: Stat result for a packed-refs file. 

916 Returns: An opaque, comparable key. 

917 """ 

918 return (st.st_ino, st.st_dev, st.st_size, st.st_mtime_ns, st.st_ctime_ns) 

919 

920 

921class DiskRefsContainer(RefsContainer): 

922 """Refs container that reads refs from disk.""" 

923 

924 def __init__( 

925 self, 

926 path: str | bytes | os.PathLike[str], 

927 worktree_path: str | bytes | os.PathLike[str] | None = None, 

928 logger: Callable[ 

929 [bytes, bytes, bytes, bytes | None, int | None, int | None, bytes], None 

930 ] 

931 | None = None, 

932 ) -> None: 

933 """Initialize DiskRefsContainer.""" 

934 super().__init__(logger=logger) 

935 # Convert path-like objects to strings, then to bytes for Git compatibility 

936 self.path = os.fsencode(os.fspath(path)) 

937 if worktree_path is None: 

938 self.worktree_path = self.path 

939 else: 

940 self.worktree_path = os.fsencode(os.fspath(worktree_path)) 

941 self._packed_refs: dict[Ref, ObjectID] | None = None 

942 self._peeled_refs: dict[Ref, ObjectID] | None = None 

943 self._packed_refs_key: _PackedRefsKey | None = None 

944 

945 def __repr__(self) -> str: 

946 """Return string representation of DiskRefsContainer.""" 

947 return f"{self.__class__.__name__}({self.path!r})" 

948 

949 def _iter_dir( 

950 self, 

951 path: bytes, 

952 base: bytes, 

953 dir_filter: Callable[[bytes], bool] | None = None, 

954 ) -> Iterator[Ref]: 

955 refspath = os.path.join(path, base.rstrip(b"/")) 

956 prefix_len = len(os.path.join(path, b"")) 

957 

958 for root, dirs, files in os.walk(refspath): 

959 directory = root[prefix_len:] 

960 if os.path.sep != "/": 

961 directory = directory.replace(os.fsencode(os.path.sep), b"/") 

962 if dir_filter is not None: 

963 dirs[:] = [ 

964 d for d in dirs if dir_filter(b"/".join([directory, d, b""])) 

965 ] 

966 

967 for filename in files: 

968 refname = b"/".join([directory, filename]) 

969 if check_ref_format(Ref(refname)): 

970 yield Ref(refname) 

971 

972 def _iter_loose_refs(self, base: bytes = b"refs/") -> Iterator[Ref]: 

973 base = base.rstrip(b"/") + b"/" 

974 search_paths: list[tuple[bytes, Callable[[bytes], bool] | None]] = [] 

975 if base != b"refs/": 

976 path = self.worktree_path if is_per_worktree_ref(base) else self.path 

977 search_paths.append((path, None)) 

978 elif self.worktree_path == self.path: 

979 # Iterate through all the refs from the main worktree 

980 search_paths.append((self.path, None)) 

981 else: 

982 # Iterate through all the shared refs from the commondir, excluding per-worktree refs 

983 search_paths.append((self.path, lambda r: not is_per_worktree_ref(r))) 

984 # Iterate through all the per-worktree refs from the worktree's gitdir 

985 search_paths.append((self.worktree_path, is_per_worktree_ref)) 

986 

987 for path, dir_filter in search_paths: 

988 yield from self._iter_dir(path, base, dir_filter=dir_filter) 

989 

990 def subkeys(self, base: Ref) -> set[Ref]: 

991 """Return subkeys under a given base reference path.""" 

992 subkeys: set[Ref] = set() 

993 

994 for key in self._iter_loose_refs(base): 

995 if key.startswith(base): 

996 subkeys.add(Ref(key[len(base) :].strip(b"/"))) 

997 

998 for key in self.get_packed_refs(): 

999 if key.startswith(base): 

1000 subkeys.add(Ref(key[len(base) :].strip(b"/"))) 

1001 return subkeys 

1002 

1003 def allkeys(self) -> set[Ref]: 

1004 """Return all reference keys.""" 

1005 allkeys: set[Ref] = set() 

1006 if os.path.exists(self.refpath(HEADREF)): 

1007 allkeys.add(Ref(HEADREF)) 

1008 

1009 allkeys.update(self._iter_loose_refs()) 

1010 allkeys.update(self.get_packed_refs()) 

1011 return allkeys 

1012 

1013 def refpath(self, name: bytes) -> bytes: 

1014 """Return the disk path of a ref.""" 

1015 path = name 

1016 if os.path.sep != "/": 

1017 path = path.replace(b"/", os.fsencode(os.path.sep)) 

1018 

1019 root_dir = self.worktree_path if is_per_worktree_ref(name) else self.path 

1020 return os.path.join(root_dir, path) 

1021 

1022 def _current_packed_refs_key(self) -> _PackedRefsKey | None: 

1023 """Identify the packed-refs file currently on disk. 

1024 

1025 Returns: An opaque key for the current packed-refs file, or None if 

1026 no packed-refs file is present. 

1027 """ 

1028 try: 

1029 st = os.stat(os.path.join(self.path, b"packed-refs")) 

1030 except FileNotFoundError: 

1031 return None 

1032 return _packed_refs_key(st) 

1033 

1034 def _invalidate_packed_refs_cache(self) -> None: 

1035 """Discard cached packed and peeled refs.""" 

1036 self._packed_refs = None 

1037 self._peeled_refs = None 

1038 self._packed_refs_key = None 

1039 

1040 def get_packed_refs(self) -> dict[Ref, ObjectID]: 

1041 """Get contents of the packed-refs file. 

1042 

1043 Returns: Dictionary mapping ref names to SHA1s 

1044 

1045 Note: Will return an empty dictionary when no packed-refs file is 

1046 present. 

1047 """ 

1048 if ( 

1049 self._packed_refs is not None 

1050 and self._packed_refs_key != self._current_packed_refs_key() 

1051 ): 

1052 # The packed-refs file was replaced underneath us, most likely by 

1053 # another process packing or unpacking refs. Drop the stale cache 

1054 # so that it is reloaded below. 

1055 self._invalidate_packed_refs_cache() 

1056 

1057 if self._packed_refs is None: 

1058 # set both to empty because we want _peeled_refs to be 

1059 # None if and only if _packed_refs is also None. 

1060 self._packed_refs = {} 

1061 self._peeled_refs = {} 

1062 self._packed_refs_key = None 

1063 path = os.path.join(self.path, b"packed-refs") 

1064 try: 

1065 f = GitFile(path, "rb") 

1066 except FileNotFoundError: 

1067 return {} 

1068 with f: 

1069 first_line = next(iter(f)).rstrip() 

1070 if first_line.startswith(b"# pack-refs") and b" peeled" in first_line: 

1071 for sha, name, peeled in read_packed_refs_with_peeled(f): 

1072 self._packed_refs[name] = sha 

1073 if peeled: 

1074 self._peeled_refs[name] = peeled 

1075 else: 

1076 f.seek(0) 

1077 for sha, name in read_packed_refs(f): 

1078 self._packed_refs[name] = sha 

1079 # Record which file the cache was populated from, so that a 

1080 # later replacement of it can be detected. Stat the open file 

1081 # rather than the path to avoid picking up a newer file that 

1082 # was renamed into place while we were reading. 

1083 self._packed_refs_key = _packed_refs_key(os.fstat(f.fileno())) 

1084 return self._packed_refs 

1085 

1086 def add_packed_refs(self, new_refs: Mapping[Ref, ObjectID | None]) -> None: 

1087 """Add the given refs as packed refs. 

1088 

1089 Args: 

1090 new_refs: A mapping of ref names to targets; if a target is None that 

1091 means remove the ref 

1092 """ 

1093 if not new_refs: 

1094 return 

1095 

1096 path = os.path.join(self.path, b"packed-refs") 

1097 

1098 try: 

1099 with GitFile(path, "wb") as f: 

1100 # reread cached refs from disk, while holding the lock 

1101 packed_refs = self.get_packed_refs().copy() 

1102 

1103 for ref, target in new_refs.items(): 

1104 # sanity check 

1105 if ref == HEADREF: 

1106 raise ValueError("cannot pack HEAD") 

1107 

1108 # remove any loose refs pointing to this one -- please 

1109 # note that this bypasses remove_if_equals as we don't 

1110 # want to affect packed refs in here 

1111 with suppress(OSError): 

1112 os.remove(self.refpath(ref)) 

1113 

1114 if target is not None: 

1115 packed_refs[ref] = target 

1116 else: 

1117 packed_refs.pop(ref, None) 

1118 

1119 write_packed_refs(f, packed_refs, self._peeled_refs) 

1120 finally: 

1121 # Do not stat the path and associate that identity with the data 

1122 # just written: another writer can replace packed-refs after the 

1123 # lock is released but before the stat. Reload on the next access 

1124 # instead. 

1125 self._invalidate_packed_refs_cache() 

1126 

1127 def get_peeled(self, name: Ref) -> ObjectID | None: 

1128 """Return the cached peeled value of a ref, if available. 

1129 

1130 Args: 

1131 name: Name of the ref to peel 

1132 Returns: The peeled value of the ref. If the ref is known not point to 

1133 a tag, this will be the SHA the ref refers to. If the ref may point 

1134 to a tag, but no cached information is available, None is returned. 

1135 """ 

1136 self.get_packed_refs() 

1137 if ( 

1138 self._peeled_refs is None 

1139 or self._packed_refs is None 

1140 or name not in self._packed_refs 

1141 ): 

1142 # No cache: no peeled refs were read, or this ref is loose 

1143 return None 

1144 if name in self._peeled_refs: 

1145 return self._peeled_refs[name] 

1146 else: 

1147 # Known not peelable 

1148 return self[name] 

1149 

1150 def read_loose_ref(self, name: Ref) -> bytes | None: 

1151 """Read a reference file and return its contents. 

1152 

1153 If the reference file a symbolic reference, only read the first line of 

1154 the file. Otherwise, read the hash (40 bytes for SHA1, 64 bytes for SHA256). 

1155 

1156 Args: 

1157 name: the refname to read, relative to refpath 

1158 Returns: The contents of the ref file, or None if the file does not 

1159 exist. 

1160 

1161 Raises: 

1162 IOError: if any other error occurs 

1163 """ 

1164 # Validate the name before turning it into a path. 

1165 try: 

1166 self._check_refname(name) 

1167 except RefFormatError: 

1168 return None 

1169 return self._read_loose_ref_file(name) 

1170 

1171 def _read_loose_ref_file(self, name: Ref) -> bytes | None: 

1172 """Read a reference file without validating its name. 

1173 

1174 Callers must have checked the name with :func:`refname_is_safe`. 

1175 """ 

1176 filename = self.refpath(name) 

1177 try: 

1178 with GitFile(filename, "rb") as f: 

1179 header = f.read(len(SYMREF)) 

1180 if header == SYMREF: 

1181 # Read only the first line 

1182 return header + next(iter(f)).rstrip(b"\r\n") 

1183 else: 

1184 # Read the entire line to get the full hash (handles both SHA1 and SHA256) 

1185 f.seek(0) 

1186 line = f.readline().rstrip(b"\r\n") 

1187 return line 

1188 except (OSError, UnicodeError): 

1189 # don't assume anything specific about the error; in 

1190 # particular, invalid or forbidden paths can raise weird 

1191 # errors depending on the specific operating system 

1192 return None 

1193 

1194 def _remove_packed_ref(self, name: Ref) -> None: 

1195 if name not in self.get_packed_refs(): 

1196 return 

1197 

1198 filename = os.path.join(self.path, b"packed-refs") 

1199 # reread cached refs from disk, while holding the lock 

1200 f = GitFile(filename, "wb") 

1201 try: 

1202 self._invalidate_packed_refs_cache() 

1203 packed_refs = self.get_packed_refs().copy() 

1204 peeled_refs = ( 

1205 self._peeled_refs.copy() if self._peeled_refs is not None else None 

1206 ) 

1207 

1208 if name not in packed_refs: 

1209 f.abort() 

1210 return 

1211 

1212 del packed_refs[name] 

1213 if peeled_refs is not None: 

1214 peeled_refs.pop(name, None) 

1215 write_packed_refs(f, packed_refs, peeled_refs) 

1216 f.close() 

1217 finally: 

1218 if not f.closed: 

1219 f.abort() 

1220 self._invalidate_packed_refs_cache() 

1221 

1222 def set_symbolic_ref( 

1223 self, 

1224 name: Ref, 

1225 other: Ref, 

1226 committer: bytes | None = None, 

1227 timestamp: int | None = None, 

1228 timezone: int | None = None, 

1229 message: bytes | None = None, 

1230 ) -> None: 

1231 """Make a ref point at another ref. 

1232 

1233 Args: 

1234 name: Name of the ref to set 

1235 other: Name of the ref to point at 

1236 committer: Optional committer name 

1237 timestamp: Optional timestamp 

1238 timezone: Optional timezone 

1239 message: Optional message to describe the change 

1240 """ 

1241 self._check_refname(name) 

1242 self._check_refname(other) 

1243 filename = self.refpath(name) 

1244 f = GitFile(filename, "wb") 

1245 try: 

1246 f.write(SYMREF + other + b"\n") 

1247 sha = self.follow(name)[-1] 

1248 self._log( 

1249 name, 

1250 sha, 

1251 sha, 

1252 committer=committer, 

1253 timestamp=timestamp, 

1254 timezone=timezone, 

1255 message=message, 

1256 ) 

1257 except BaseException: 

1258 f.abort() 

1259 raise 

1260 else: 

1261 f.close() 

1262 

1263 def set_if_equals( 

1264 self, 

1265 name: Ref, 

1266 old_ref: ObjectID | None, 

1267 new_ref: ObjectID, 

1268 committer: bytes | None = None, 

1269 timestamp: int | None = None, 

1270 timezone: int | None = None, 

1271 message: bytes | None = None, 

1272 ) -> bool: 

1273 """Set a refname to new_ref only if it currently equals old_ref. 

1274 

1275 This method follows all symbolic references, and can be used to perform 

1276 an atomic compare-and-swap operation. 

1277 

1278 Args: 

1279 name: The refname to set. 

1280 old_ref: The old sha the refname must refer to, or None to set 

1281 unconditionally. 

1282 new_ref: The new sha the refname will refer to. 

1283 committer: Optional committer name 

1284 timestamp: Optional timestamp 

1285 timezone: Optional timezone 

1286 message: Set message for reflog 

1287 Returns: True if the set was successful, False otherwise. 

1288 """ 

1289 self._check_refname(name) 

1290 self._check_ref_value(new_ref) 

1291 try: 

1292 realnames, _ = self.follow(name) 

1293 realname = realnames[-1] 

1294 except (KeyError, IndexError, SymrefLoop): 

1295 realname = name 

1296 # The resolved symref target may escape the ref store. 

1297 if not refname_is_safe(realname): 

1298 raise RefFormatError(realname) 

1299 filename = self.refpath(realname) 

1300 

1301 # make sure none of the ancestor folders is in packed refs 

1302 probe_ref = Ref(os.path.dirname(realname)) 

1303 packed_refs = self.get_packed_refs() 

1304 while probe_ref: 

1305 if packed_refs.get(probe_ref, None) is not None: 

1306 raise NotADirectoryError(filename) 

1307 probe_ref = Ref(os.path.dirname(probe_ref)) 

1308 

1309 ensure_dir_exists(os.path.dirname(filename)) 

1310 with GitFile(filename, "wb") as f: 

1311 if old_ref is not None: 

1312 try: 

1313 # read again while holding the lock to handle race conditions 

1314 orig_ref = self._read_loose_ref_file(realname) 

1315 if orig_ref is None: 

1316 orig_ref = self.get_packed_refs().get(realname, ZERO_SHA) 

1317 if orig_ref != old_ref: 

1318 f.abort() 

1319 return False 

1320 except OSError: 

1321 f.abort() 

1322 raise 

1323 

1324 # Check if ref already has the desired value while holding the lock 

1325 # This avoids fsync when ref is unchanged but still detects lock conflicts 

1326 current_ref = self._read_loose_ref_file(realname) 

1327 if current_ref is None: 

1328 current_ref = packed_refs.get(realname, None) 

1329 

1330 if current_ref is not None and current_ref == new_ref: 

1331 # Ref already has desired value, abort write to avoid fsync 

1332 f.abort() 

1333 return True 

1334 

1335 try: 

1336 f.write(new_ref + b"\n") 

1337 except OSError: 

1338 f.abort() 

1339 raise 

1340 self._log( 

1341 realname, 

1342 old_ref, 

1343 new_ref, 

1344 committer=committer, 

1345 timestamp=timestamp, 

1346 timezone=timezone, 

1347 message=message, 

1348 ) 

1349 return True 

1350 

1351 def add_if_new( 

1352 self, 

1353 name: Ref, 

1354 ref: ObjectID, 

1355 committer: bytes | None = None, 

1356 timestamp: int | None = None, 

1357 timezone: int | None = None, 

1358 message: bytes | None = None, 

1359 ) -> bool: 

1360 """Add a new reference only if it does not already exist. 

1361 

1362 This method follows symrefs, and only ensures that the last ref in the 

1363 chain does not exist. 

1364 

1365 Args: 

1366 name: The refname to set. 

1367 ref: The new sha the refname will refer to. 

1368 committer: Optional committer name 

1369 timestamp: Optional timestamp 

1370 timezone: Optional timezone 

1371 message: Optional message for reflog 

1372 Returns: True if the add was successful, False otherwise. 

1373 """ 

1374 self._check_refname(name) 

1375 self._check_ref_value(ref) 

1376 try: 

1377 realnames, contents = self.follow(name) 

1378 if contents is not None: 

1379 return False 

1380 realname = realnames[-1] 

1381 except (KeyError, IndexError): 

1382 realname = name 

1383 if not refname_is_safe(realname): 

1384 raise RefFormatError(realname) 

1385 filename = self.refpath(realname) 

1386 ensure_dir_exists(os.path.dirname(filename)) 

1387 with GitFile(filename, "wb") as f: 

1388 if os.path.exists(filename) or name in self.get_packed_refs(): 

1389 f.abort() 

1390 return False 

1391 try: 

1392 f.write(ref + b"\n") 

1393 except OSError: 

1394 f.abort() 

1395 raise 

1396 else: 

1397 self._log( 

1398 name, 

1399 None, 

1400 ref, 

1401 committer=committer, 

1402 timestamp=timestamp, 

1403 timezone=timezone, 

1404 message=message, 

1405 ) 

1406 return True 

1407 

1408 def remove_if_equals( 

1409 self, 

1410 name: Ref, 

1411 old_ref: ObjectID | None, 

1412 committer: bytes | None = None, 

1413 timestamp: int | None = None, 

1414 timezone: int | None = None, 

1415 message: bytes | None = None, 

1416 ) -> bool: 

1417 """Remove a refname only if it currently equals old_ref. 

1418 

1419 This method does not follow symbolic references. It can be used to 

1420 perform an atomic compare-and-delete operation. 

1421 

1422 Args: 

1423 name: The refname to delete. 

1424 old_ref: The old sha the refname must refer to, or None to 

1425 delete unconditionally. 

1426 committer: Optional committer name 

1427 timestamp: Optional timestamp 

1428 timezone: Optional timezone 

1429 message: Optional message 

1430 Returns: True if the delete was successful, False otherwise. 

1431 """ 

1432 self._check_refname(name) 

1433 filename = self.refpath(name) 

1434 ensure_dir_exists(os.path.dirname(filename)) 

1435 f = GitFile(filename, "wb") 

1436 try: 

1437 if old_ref is not None: 

1438 orig_ref = self.read_loose_ref(name) 

1439 if orig_ref is None: 

1440 orig_ref = self.get_packed_refs().get(name) 

1441 if orig_ref is None: 

1442 orig_ref = ZERO_SHA 

1443 if orig_ref != old_ref: 

1444 return False 

1445 

1446 # remove the reference file itself 

1447 try: 

1448 found = os.path.lexists(filename) 

1449 except OSError: 

1450 # may only be packed, or otherwise unstorable 

1451 found = False 

1452 

1453 if found: 

1454 os.remove(filename) 

1455 

1456 self._remove_packed_ref(name) 

1457 self._log( 

1458 name, 

1459 old_ref, 

1460 None, 

1461 committer=committer, 

1462 timestamp=timestamp, 

1463 timezone=timezone, 

1464 message=message, 

1465 ) 

1466 finally: 

1467 # never write, we just wanted the lock 

1468 f.abort() 

1469 

1470 # outside of the lock, clean-up any parent directory that might now 

1471 # be empty. this ensures that re-creating a reference of the same 

1472 # name of what was previously a directory works as expected 

1473 parent = name 

1474 while True: 

1475 try: 

1476 parent_bytes, _ = parent.rsplit(b"/", 1) 

1477 parent = Ref(parent_bytes) 

1478 except ValueError: 

1479 break 

1480 

1481 if parent == b"refs": 

1482 break 

1483 parent_filename = self.refpath(parent) 

1484 try: 

1485 os.rmdir(parent_filename) 

1486 except OSError: 

1487 # this can be caused by the parent directory being 

1488 # removed by another process, being not empty, etc. 

1489 # in any case, this is non fatal because we already 

1490 # removed the reference, just ignore it 

1491 break 

1492 

1493 return True 

1494 

1495 def pack_refs(self, all: bool = False) -> None: 

1496 """Pack loose refs into packed-refs file. 

1497 

1498 Args: 

1499 all: If True, pack all refs. If False, only pack tags. 

1500 """ 

1501 refs_to_pack: dict[Ref, ObjectID | None] = {} 

1502 for ref in self.allkeys(): 

1503 if ref == HEADREF: 

1504 # Never pack HEAD 

1505 continue 

1506 if all or ref.startswith(LOCAL_TAG_PREFIX): 

1507 try: 

1508 sha = self[ref] 

1509 if sha: 

1510 refs_to_pack[ref] = sha 

1511 except KeyError: 

1512 # Broken ref, skip it 

1513 pass 

1514 

1515 if refs_to_pack: 

1516 self.add_packed_refs(refs_to_pack) 

1517 

1518 

1519def _split_ref_line(line: bytes) -> tuple[ObjectID, Ref]: 

1520 """Split a single ref line into a tuple of SHA1 and name.""" 

1521 fields = line.rstrip(b"\n\r").split(b" ") 

1522 if len(fields) != 2: 

1523 raise PackedRefsException(f"invalid ref line {line!r}") 

1524 sha, name = fields 

1525 if not valid_hexsha(sha): 

1526 raise PackedRefsException(f"Invalid hex sha {sha!r}") 

1527 if not check_ref_format(Ref(name)): 

1528 raise PackedRefsException(f"invalid ref name {name!r}") 

1529 return (ObjectID(sha), Ref(name)) 

1530 

1531 

1532def read_packed_refs(f: IO[bytes]) -> Iterator[tuple[ObjectID, Ref]]: 

1533 """Read a packed refs file. 

1534 

1535 Args: 

1536 f: file-like object to read from 

1537 Returns: Iterator over tuples with SHA1s and ref names. 

1538 """ 

1539 for line in f: 

1540 if line.startswith(b"#"): 

1541 # Comment 

1542 continue 

1543 if line.startswith(b"^"): 

1544 raise PackedRefsException("found peeled ref in packed-refs without peeled") 

1545 yield _split_ref_line(line) 

1546 

1547 

1548def read_packed_refs_with_peeled( 

1549 f: IO[bytes], 

1550) -> Iterator[tuple[ObjectID, Ref, ObjectID | None]]: 

1551 """Read a packed refs file including peeled refs. 

1552 

1553 Assumes the "# pack-refs with: peeled" line was already read. Yields tuples 

1554 with ref names, SHA1s, and peeled SHA1s (or None). 

1555 

1556 Args: 

1557 f: file-like object to read from, seek'ed to the second line 

1558 """ 

1559 last = None 

1560 for line in f: 

1561 if line.startswith(b"#"): 

1562 continue 

1563 line = line.rstrip(b"\r\n") 

1564 if line.startswith(b"^"): 

1565 if not last: 

1566 raise PackedRefsException("unexpected peeled ref line") 

1567 if not valid_hexsha(line[1:]): 

1568 raise PackedRefsException(f"Invalid hex sha {line[1:]!r}") 

1569 sha, name = _split_ref_line(last) 

1570 last = None 

1571 yield (sha, name, ObjectID(line[1:])) 

1572 else: 

1573 if last: 

1574 sha, name = _split_ref_line(last) 

1575 yield (sha, name, None) 

1576 last = line 

1577 if last: 

1578 sha, name = _split_ref_line(last) 

1579 yield (sha, name, None) 

1580 

1581 

1582def write_packed_refs( 

1583 f: IO[bytes], 

1584 packed_refs: Mapping[Ref, ObjectID], 

1585 peeled_refs: Mapping[Ref, ObjectID] | None = None, 

1586) -> None: 

1587 """Write a packed refs file. 

1588 

1589 Args: 

1590 f: empty file-like object to write to 

1591 packed_refs: dict of refname to sha of packed refs to write 

1592 peeled_refs: dict of refname to peeled value of sha 

1593 """ 

1594 if peeled_refs is None: 

1595 peeled_refs = {} 

1596 else: 

1597 f.write(b"# pack-refs with: peeled\n") 

1598 for refname in sorted(packed_refs.keys()): 

1599 f.write(git_line(packed_refs[refname], refname)) 

1600 if refname in peeled_refs: 

1601 f.write(b"^" + peeled_refs[refname] + b"\n") 

1602 

1603 

1604def read_info_refs(f: BinaryIO) -> dict[Ref, ObjectID]: 

1605 """Read info/refs file. 

1606 

1607 Args: 

1608 f: File-like object to read from 

1609 

1610 Returns: 

1611 Dictionary mapping ref names to SHA1s 

1612 """ 

1613 ret: dict[Ref, ObjectID] = {} 

1614 for line_no, line in enumerate(f.readlines(), 1): 

1615 stripped = line.rstrip(b"\r\n") 

1616 parts = stripped.split(b"\t", 1) 

1617 if len(parts) != 2: 

1618 raise ValueError( 

1619 f"Invalid info/refs format at line {line_no}: " 

1620 f"expected '<sha>\\t<refname>', got {stripped[:100]!r}" 

1621 ) 

1622 (sha, name) = parts 

1623 ret[Ref(name)] = ObjectID(sha) 

1624 return ret 

1625 

1626 

1627def is_local_branch(x: bytes) -> bool: 

1628 """Check if a ref name is a local branch.""" 

1629 return x.startswith(LOCAL_BRANCH_PREFIX) 

1630 

1631 

1632def _strip_leading_slash(name: bytes, kind: str) -> bytes: 

1633 """Strip a leading slash from a short ref name, warning if one is present. 

1634 

1635 A leading slash here means the caller stripped a ref prefix incorrectly 

1636 (e.g. used ``ref[len(b"refs/tags"):]`` instead of ``len(b"refs/tags/")``). 

1637 Joining such a name with a prefix would produce a malformed ref with an 

1638 empty path component. Warn rather than raise for now; this will become an 

1639 error in a future release. 

1640 """ 

1641 if not name.startswith(b"/"): 

1642 return name 

1643 warnings.warn( 

1644 f"{kind} name must not start with a slash: {name!r}; " 

1645 "this will be rejected in a future version of Dulwich.", 

1646 DeprecationWarning, 

1647 stacklevel=3, 

1648 ) 

1649 return name.lstrip(b"/") 

1650 

1651 

1652def local_branch_name(name: bytes) -> Ref: 

1653 """Build a full branch ref from a short name. 

1654 

1655 Args: 

1656 name: Short branch name (e.g., b"master") or full ref 

1657 

1658 Returns: 

1659 Full branch ref name (e.g., b"refs/heads/master") 

1660 

1661 Examples: 

1662 >>> local_branch_name(b"master") 

1663 b'refs/heads/master' 

1664 >>> local_branch_name(b"refs/heads/master") 

1665 b'refs/heads/master' 

1666 """ 

1667 if name.startswith(LOCAL_BRANCH_PREFIX): 

1668 return Ref(name) 

1669 return Ref(LOCAL_BRANCH_PREFIX + _strip_leading_slash(name, "Branch")) 

1670 

1671 

1672def local_tag_name(name: bytes) -> Ref: 

1673 """Build a full tag ref from a short name. 

1674 

1675 Args: 

1676 name: Short tag name (e.g., b"v1.0") or full ref 

1677 

1678 Returns: 

1679 Full tag ref name (e.g., b"refs/tags/v1.0") 

1680 

1681 Examples: 

1682 >>> local_tag_name(b"v1.0") 

1683 b'refs/tags/v1.0' 

1684 >>> local_tag_name(b"refs/tags/v1.0") 

1685 b'refs/tags/v1.0' 

1686 """ 

1687 if name.startswith(LOCAL_TAG_PREFIX): 

1688 return Ref(name) 

1689 return Ref(LOCAL_TAG_PREFIX + _strip_leading_slash(name, "Tag")) 

1690 

1691 

1692def local_replace_name(name: bytes) -> Ref: 

1693 """Build a full replace ref from a short name. 

1694 

1695 Args: 

1696 name: Short replace name (object SHA) or full ref 

1697 

1698 Returns: 

1699 Full replace ref name (e.g., b"refs/replace/<sha>") 

1700 

1701 Examples: 

1702 >>> local_replace_name(b"abc123") 

1703 b'refs/replace/abc123' 

1704 >>> local_replace_name(b"refs/replace/abc123") 

1705 b'refs/replace/abc123' 

1706 """ 

1707 if name.startswith(LOCAL_REPLACE_PREFIX): 

1708 return Ref(name) 

1709 return Ref(LOCAL_REPLACE_PREFIX + _strip_leading_slash(name, "Replace")) 

1710 

1711 

1712def extract_branch_name(ref: bytes) -> bytes: 

1713 """Extract branch name from a full branch ref. 

1714 

1715 Args: 

1716 ref: Full branch ref (e.g., b"refs/heads/master") 

1717 

1718 Returns: 

1719 Short branch name (e.g., b"master") 

1720 

1721 Raises: 

1722 ValueError: If ref is not a local branch 

1723 

1724 Examples: 

1725 >>> extract_branch_name(b"refs/heads/master") 

1726 b'master' 

1727 >>> extract_branch_name(b"refs/heads/feature/foo") 

1728 b'feature/foo' 

1729 """ 

1730 if not ref.startswith(LOCAL_BRANCH_PREFIX): 

1731 raise ValueError(f"Not a local branch ref: {ref!r}") 

1732 return ref[len(LOCAL_BRANCH_PREFIX) :] 

1733 

1734 

1735def extract_tag_name(ref: bytes) -> bytes: 

1736 """Extract tag name from a full tag ref. 

1737 

1738 Args: 

1739 ref: Full tag ref (e.g., b"refs/tags/v1.0") 

1740 

1741 Returns: 

1742 Short tag name (e.g., b"v1.0") 

1743 

1744 Raises: 

1745 ValueError: If ref is not a local tag 

1746 

1747 Examples: 

1748 >>> extract_tag_name(b"refs/tags/v1.0") 

1749 b'v1.0' 

1750 """ 

1751 if not ref.startswith(LOCAL_TAG_PREFIX): 

1752 raise ValueError(f"Not a local tag ref: {ref!r}") 

1753 return ref[len(LOCAL_TAG_PREFIX) :] 

1754 

1755 

1756def shorten_ref_name(ref: bytes) -> bytes: 

1757 """Convert a full ref name to its short form. 

1758 

1759 Args: 

1760 ref: Full ref name (e.g., b"refs/heads/master") 

1761 

1762 Returns: 

1763 Short ref name (e.g., b"master") 

1764 

1765 Examples: 

1766 >>> shorten_ref_name(b"refs/heads/master") 

1767 b'master' 

1768 >>> shorten_ref_name(b"refs/remotes/origin/main") 

1769 b'origin/main' 

1770 >>> shorten_ref_name(b"refs/tags/v1.0") 

1771 b'v1.0' 

1772 >>> shorten_ref_name(b"HEAD") 

1773 b'HEAD' 

1774 """ 

1775 if ref.startswith(LOCAL_BRANCH_PREFIX): 

1776 return ref[len(LOCAL_BRANCH_PREFIX) :] 

1777 elif ref.startswith(LOCAL_REMOTE_PREFIX): 

1778 return ref[len(LOCAL_REMOTE_PREFIX) :] 

1779 elif ref.startswith(LOCAL_TAG_PREFIX): 

1780 return ref[len(LOCAL_TAG_PREFIX) :] 

1781 return ref 

1782 

1783 

1784def _set_origin_head( 

1785 refs: RefsContainer, origin: bytes, origin_head: bytes | None 

1786) -> None: 

1787 # set refs/remotes/origin/HEAD 

1788 origin_base = b"refs/remotes/" + origin + b"/" 

1789 if origin_head and origin_head.startswith(LOCAL_BRANCH_PREFIX): 

1790 origin_ref = Ref(origin_base + HEADREF) 

1791 target_ref = Ref(origin_base + extract_branch_name(origin_head)) 

1792 if target_ref in refs: 

1793 refs.set_symbolic_ref(origin_ref, target_ref) 

1794 

1795 

1796def _set_default_branch( 

1797 refs: RefsContainer, 

1798 origin: bytes, 

1799 origin_head: bytes | None, 

1800 branch: bytes | None, 

1801 ref_message: bytes | None, 

1802) -> bytes: 

1803 """Set the default branch.""" 

1804 origin_base = b"refs/remotes/" + origin + b"/" 

1805 if branch: 

1806 origin_ref = Ref(origin_base + branch) 

1807 if origin_ref in refs: 

1808 local_ref = Ref(local_branch_name(branch)) 

1809 refs.add_if_new(local_ref, refs[origin_ref], ref_message) 

1810 head_ref = local_ref 

1811 elif Ref(local_tag_name(branch)) in refs: 

1812 head_ref = Ref(local_tag_name(branch)) 

1813 else: 

1814 raise ValueError(f"{os.fsencode(branch)!r} is not a valid branch or tag") 

1815 elif origin_head: 

1816 head_ref = Ref(origin_head) 

1817 if origin_head.startswith(LOCAL_BRANCH_PREFIX): 

1818 origin_ref = Ref(origin_base + extract_branch_name(origin_head)) 

1819 else: 

1820 origin_ref = Ref(origin_head) 

1821 try: 

1822 refs.add_if_new(head_ref, refs[origin_ref], ref_message) 

1823 except KeyError: 

1824 pass 

1825 else: 

1826 raise ValueError("neither origin_head nor branch are provided") 

1827 return head_ref 

1828 

1829 

1830def _set_head( 

1831 refs: RefsContainer, head_ref: bytes, ref_message: bytes | None 

1832) -> ObjectID | None: 

1833 if head_ref.startswith(LOCAL_TAG_PREFIX): 

1834 # detach HEAD at specified tag 

1835 head = refs[Ref(head_ref)] 

1836 del refs[HEADREF] 

1837 refs.set_if_equals(HEADREF, None, head, message=ref_message) 

1838 else: 

1839 # set HEAD to specific branch 

1840 try: 

1841 head = refs[Ref(head_ref)] 

1842 refs.set_symbolic_ref(HEADREF, Ref(head_ref)) 

1843 refs.set_if_equals(HEADREF, None, head, message=ref_message) 

1844 except KeyError: 

1845 head = None 

1846 return head 

1847 

1848 

1849def _set_branch_tracking(config: "ConfigFile", head_ref: bytes, remote: bytes) -> None: 

1850 """Point the branch a clone checked out at its counterpart on the remote. 

1851 

1852 This is what lets a subsequent "git pull" with no arguments know what to 

1853 merge. Like git clone, nothing is written when HEAD ended up detached at a 

1854 tag rather than on a branch. 

1855 

1856 Args: 

1857 config: Config of the freshly cloned repository 

1858 head_ref: Local ref HEAD was pointed at, e.g. refs/heads/master 

1859 remote: Name of the remote the clone came from 

1860 """ 

1861 if not head_ref.startswith(LOCAL_BRANCH_PREFIX): 

1862 return 

1863 branch = extract_branch_name(Ref(head_ref)) 

1864 config.set((b"branch", branch), b"remote", remote) 

1865 config.set((b"branch", branch), b"merge", head_ref) 

1866 config.write_to_path() 

1867 

1868 

1869def _import_remote_refs( 

1870 refs_container: RefsContainer, 

1871 remote_name: str, 

1872 refs: Mapping[Ref, ObjectID | None], 

1873 message: bytes | None = None, 

1874 prune: bool = False, 

1875 prune_tags: bool = False, 

1876) -> None: 

1877 from .protocol import PEELED_TAG_SUFFIX, strip_peeled_refs 

1878 

1879 stripped_refs = strip_peeled_refs(refs) 

1880 branches: dict[Ref, ObjectID | None] = { 

1881 Ref(extract_branch_name(n)): v 

1882 for (n, v) in stripped_refs.items() 

1883 if n.startswith(LOCAL_BRANCH_PREFIX) 

1884 } 

1885 refs_container.import_refs( 

1886 Ref(b"refs/remotes/" + remote_name.encode()), 

1887 branches, 

1888 message=message, 

1889 prune=prune, 

1890 ) 

1891 tags: dict[Ref, ObjectID | None] = { 

1892 Ref(extract_tag_name(n)): v 

1893 for (n, v) in stripped_refs.items() 

1894 if n.startswith(LOCAL_TAG_PREFIX) and not n.endswith(PEELED_TAG_SUFFIX) 

1895 } 

1896 refs_container.import_refs( 

1897 Ref(b"refs/tags"), tags, message=message, prune=prune_tags 

1898 ) 

1899 

1900 

1901class locked_ref: 

1902 """Lock a ref while making modifications. 

1903 

1904 Works as a context manager. 

1905 """ 

1906 

1907 def __init__(self, refs_container: DiskRefsContainer, refname: Ref) -> None: 

1908 """Initialize a locked ref. 

1909 

1910 Args: 

1911 refs_container: The DiskRefsContainer to lock the ref in 

1912 refname: The ref name to lock 

1913 """ 

1914 self._refs_container = refs_container 

1915 self._refname = refname 

1916 self._file: _GitFile | None = None 

1917 self._realname: Ref | None = None 

1918 self._deleted = False 

1919 

1920 def __enter__(self) -> Self: 

1921 """Enter the context manager and acquire the lock. 

1922 

1923 Returns: 

1924 This locked_ref instance 

1925 

1926 Raises: 

1927 OSError: If the lock cannot be acquired 

1928 """ 

1929 self._refs_container._check_refname(self._refname) 

1930 try: 

1931 realnames, _ = self._refs_container.follow(self._refname) 

1932 self._realname = realnames[-1] 

1933 except (KeyError, IndexError, SymrefLoop): 

1934 self._realname = self._refname 

1935 

1936 # The resolved symref target may escape the ref store. 

1937 if not refname_is_safe(self._realname): 

1938 raise RefFormatError(self._realname) 

1939 filename = self._refs_container.refpath(self._realname) 

1940 ensure_dir_exists(os.path.dirname(filename)) 

1941 f = GitFile(filename, "wb") 

1942 self._file = f 

1943 return self 

1944 

1945 def __exit__( 

1946 self, 

1947 exc_type: type | None, 

1948 exc_value: BaseException | None, 

1949 traceback: types.TracebackType | None, 

1950 ) -> None: 

1951 """Exit the context manager and release the lock. 

1952 

1953 Args: 

1954 exc_type: Type of exception if one occurred 

1955 exc_value: Exception instance if one occurred 

1956 traceback: Traceback if an exception occurred 

1957 """ 

1958 if self._file: 

1959 if exc_type is not None or self._deleted: 

1960 self._file.abort() 

1961 else: 

1962 self._file.close() 

1963 

1964 def get(self) -> bytes | None: 

1965 """Get the current value of the ref.""" 

1966 if not self._file: 

1967 raise RuntimeError("locked_ref not in context") 

1968 

1969 assert self._realname is not None 

1970 current_ref = self._refs_container._read_loose_ref_file(self._realname) 

1971 if current_ref is None: 

1972 current_ref = self._refs_container.get_packed_refs().get( 

1973 self._realname, None 

1974 ) 

1975 return current_ref 

1976 

1977 def ensure_equals(self, expected_value: bytes | None) -> bool: 

1978 """Ensure the ref currently equals the expected value. 

1979 

1980 Args: 

1981 expected_value: The expected current value of the ref 

1982 Returns: 

1983 True if the ref equals the expected value, False otherwise 

1984 """ 

1985 current_value = self.get() 

1986 return current_value == expected_value 

1987 

1988 def set(self, new_ref: bytes) -> None: 

1989 """Set the ref to a new value. 

1990 

1991 Args: 

1992 new_ref: The new SHA1 or symbolic ref value 

1993 """ 

1994 if not self._file: 

1995 raise RuntimeError("locked_ref not in context") 

1996 

1997 if not (valid_hexsha(new_ref) or new_ref.startswith(SYMREF)): 

1998 raise ValueError(f"{new_ref!r} must be a valid sha or a symref") 

1999 

2000 self._file.seek(0) 

2001 self._file.truncate() 

2002 self._file.write(new_ref + b"\n") 

2003 self._deleted = False 

2004 

2005 def set_symbolic_ref(self, target: Ref) -> None: 

2006 """Make this ref point at another ref. 

2007 

2008 Args: 

2009 target: Name of the ref to point at 

2010 """ 

2011 if not self._file: 

2012 raise RuntimeError("locked_ref not in context") 

2013 

2014 self._refs_container._check_refname(target) 

2015 self._file.seek(0) 

2016 self._file.truncate() 

2017 self._file.write(SYMREF + target + b"\n") 

2018 self._deleted = False 

2019 

2020 def delete(self) -> None: 

2021 """Delete the ref file while holding the lock.""" 

2022 if not self._file: 

2023 raise RuntimeError("locked_ref not in context") 

2024 

2025 # Delete the actual ref file while holding the lock 

2026 if self._realname: 

2027 filename = self._refs_container.refpath(self._realname) 

2028 try: 

2029 if os.path.lexists(filename): 

2030 os.remove(filename) 

2031 except FileNotFoundError: 

2032 pass 

2033 self._refs_container._remove_packed_ref(self._realname) 

2034 

2035 self._deleted = True 

2036 

2037 

2038class NamespacedRefsContainer(RefsContainer): 

2039 """Wrapper that adds namespace prefix to all ref operations. 

2040 

2041 This implements Git's GIT_NAMESPACE feature, which stores refs under 

2042 refs/namespaces/<namespace>/ and filters operations to only show refs 

2043 within that namespace. 

2044 

2045 Example: 

2046 With namespace "foo", a ref "refs/heads/master" is stored as 

2047 "refs/namespaces/foo/refs/heads/master" in the underlying container. 

2048 """ 

2049 

2050 def __init__(self, refs: RefsContainer, namespace: bytes) -> None: 

2051 """Initialize NamespacedRefsContainer. 

2052 

2053 Args: 

2054 refs: The underlying refs container to wrap 

2055 namespace: The namespace prefix (e.g., b"foo" or b"foo/bar") 

2056 """ 

2057 super().__init__(logger=refs._logger) 

2058 self._refs = refs 

2059 # Build namespace prefix: refs/namespaces/<namespace>/ 

2060 # Support nested namespaces: foo/bar -> refs/namespaces/foo/refs/namespaces/bar/ 

2061 namespace_parts = namespace.split(b"/") 

2062 self._namespace_prefix = b"" 

2063 for part in namespace_parts: 

2064 self._namespace_prefix += b"refs/namespaces/" + part + b"/" 

2065 

2066 def _apply_namespace(self, name: bytes) -> bytes: 

2067 """Apply namespace prefix to a ref name.""" 

2068 # HEAD and other special refs are not namespaced 

2069 if name == HEADREF or not name.startswith(b"refs/"): 

2070 return name 

2071 return self._namespace_prefix + name 

2072 

2073 def _strip_namespace(self, name: bytes) -> bytes | None: 

2074 """Remove namespace prefix from a ref name. 

2075 

2076 Returns None if the ref is not in our namespace. 

2077 """ 

2078 # HEAD and other special refs are not namespaced 

2079 if name == HEADREF or not name.startswith(b"refs/"): 

2080 return name 

2081 if name.startswith(self._namespace_prefix): 

2082 return name[len(self._namespace_prefix) :] 

2083 return None 

2084 

2085 def allkeys(self) -> set[Ref]: 

2086 """Return all reference keys in this namespace.""" 

2087 keys: set[Ref] = set() 

2088 for key in self._refs.allkeys(): 

2089 stripped = self._strip_namespace(key) 

2090 if stripped is not None: 

2091 keys.add(Ref(stripped)) 

2092 return keys 

2093 

2094 def read_loose_ref(self, name: Ref) -> bytes | None: 

2095 """Read a loose reference.""" 

2096 return self._refs.read_loose_ref(Ref(self._apply_namespace(name))) 

2097 

2098 def get_packed_refs(self) -> dict[Ref, ObjectID]: 

2099 """Get packed refs within this namespace.""" 

2100 packed: dict[Ref, ObjectID] = {} 

2101 for name, value in self._refs.get_packed_refs().items(): 

2102 stripped = self._strip_namespace(name) 

2103 if stripped is not None: 

2104 packed[Ref(stripped)] = value 

2105 return packed 

2106 

2107 def add_packed_refs(self, new_refs: Mapping[Ref, ObjectID | None]) -> None: 

2108 """Add packed refs with namespace prefix.""" 

2109 namespaced_refs: dict[Ref, ObjectID | None] = { 

2110 Ref(self._apply_namespace(name)): value for name, value in new_refs.items() 

2111 } 

2112 self._refs.add_packed_refs(namespaced_refs) 

2113 

2114 def get_peeled(self, name: Ref) -> ObjectID | None: 

2115 """Return the cached peeled value of a ref.""" 

2116 return self._refs.get_peeled(Ref(self._apply_namespace(name))) 

2117 

2118 def set_symbolic_ref( 

2119 self, 

2120 name: Ref, 

2121 other: Ref, 

2122 committer: bytes | None = None, 

2123 timestamp: int | None = None, 

2124 timezone: int | None = None, 

2125 message: bytes | None = None, 

2126 ) -> None: 

2127 """Make a ref point at another ref.""" 

2128 self._refs.set_symbolic_ref( 

2129 Ref(self._apply_namespace(name)), 

2130 Ref(self._apply_namespace(other)), 

2131 committer=committer, 

2132 timestamp=timestamp, 

2133 timezone=timezone, 

2134 message=message, 

2135 ) 

2136 

2137 def set_if_equals( 

2138 self, 

2139 name: Ref, 

2140 old_ref: ObjectID | None, 

2141 new_ref: ObjectID, 

2142 committer: bytes | None = None, 

2143 timestamp: int | None = None, 

2144 timezone: int | None = None, 

2145 message: bytes | None = None, 

2146 ) -> bool: 

2147 """Set a refname to new_ref only if it currently equals old_ref.""" 

2148 return self._refs.set_if_equals( 

2149 Ref(self._apply_namespace(name)), 

2150 old_ref, 

2151 new_ref, 

2152 committer=committer, 

2153 timestamp=timestamp, 

2154 timezone=timezone, 

2155 message=message, 

2156 ) 

2157 

2158 def add_if_new( 

2159 self, 

2160 name: Ref, 

2161 ref: ObjectID, 

2162 committer: bytes | None = None, 

2163 timestamp: int | None = None, 

2164 timezone: int | None = None, 

2165 message: bytes | None = None, 

2166 ) -> bool: 

2167 """Add a new reference only if it does not already exist.""" 

2168 return self._refs.add_if_new( 

2169 Ref(self._apply_namespace(name)), 

2170 ref, 

2171 committer=committer, 

2172 timestamp=timestamp, 

2173 timezone=timezone, 

2174 message=message, 

2175 ) 

2176 

2177 def remove_if_equals( 

2178 self, 

2179 name: Ref, 

2180 old_ref: ObjectID | None, 

2181 committer: bytes | None = None, 

2182 timestamp: int | None = None, 

2183 timezone: int | None = None, 

2184 message: bytes | None = None, 

2185 ) -> bool: 

2186 """Remove a refname only if it currently equals old_ref.""" 

2187 return self._refs.remove_if_equals( 

2188 Ref(self._apply_namespace(name)), 

2189 old_ref, 

2190 committer=committer, 

2191 timestamp=timestamp, 

2192 timezone=timezone, 

2193 message=message, 

2194 ) 

2195 

2196 def pack_refs(self, all: bool = False) -> None: 

2197 """Pack loose refs into packed-refs file. 

2198 

2199 Note: This packs all refs in the underlying container, not just 

2200 those in the namespace. 

2201 """ 

2202 self._refs.pack_refs(all=all) 

2203 

2204 

2205@overload 

2206def filter_ref_prefix( 

2207 refs: dict[Ref, ObjectID], prefixes: Iterable[bytes] 

2208) -> dict[Ref, ObjectID]: ... 

2209 

2210 

2211@overload 

2212def filter_ref_prefix( 

2213 refs: dict[Ref, ObjectID | None], prefixes: Iterable[bytes] 

2214) -> dict[Ref, ObjectID | None]: ... 

2215 

2216 

2217def filter_ref_prefix( 

2218 refs: dict[Ref, ObjectID] | dict[Ref, ObjectID | None], 

2219 prefixes: Iterable[bytes], 

2220) -> dict[Ref, ObjectID] | dict[Ref, ObjectID | None]: 

2221 """Filter refs to only include those with a given prefix. 

2222 

2223 Args: 

2224 refs: A dictionary of refs. 

2225 prefixes: The prefixes to filter by. 

2226 """ 

2227 return {k: v for k, v in refs.items() if any(k.startswith(p) for p in prefixes)} 

2228 

2229 

2230def is_per_worktree_ref(ref: bytes) -> bool: 

2231 """Returns whether a reference is stored per worktree or not. 

2232 

2233 Per-worktree references are: 

2234 - all pseudorefs, e.g. HEAD 

2235 - all references stored inside "refs/bisect/", "refs/worktree/" and "refs/rewritten/" 

2236 

2237 All refs starting with "refs/" are shared, except for the ones listed above. 

2238 

2239 See https://git-scm.com/docs/git-worktree#_refs. 

2240 """ 

2241 return not ref.startswith(b"refs/") or ref.startswith( 

2242 (b"refs/bisect/", b"refs/worktree/", b"refs/rewritten/") 

2243 )