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

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

817 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 "HEADREF", 

27 "LOCAL_BRANCH_PREFIX", 

28 "LOCAL_NOTES_PREFIX", 

29 "LOCAL_REMOTE_PREFIX", 

30 "LOCAL_REPLACE_PREFIX", 

31 "LOCAL_TAG_PREFIX", 

32 "SYMREF", 

33 "DictRefsContainer", 

34 "DiskRefsContainer", 

35 "NamespacedRefsContainer", 

36 "Ref", 

37 "RefsContainer", 

38 "SymrefLoop", 

39 "check_ref_format", 

40 "extract_branch_name", 

41 "extract_tag_name", 

42 "filter_ref_prefix", 

43 "is_local_branch", 

44 "is_per_worktree_ref", 

45 "local_branch_name", 

46 "local_replace_name", 

47 "local_tag_name", 

48 "parse_remote_ref", 

49 "parse_symref_value", 

50 "read_info_refs", 

51 "read_packed_refs", 

52 "read_packed_refs_with_peeled", 

53 "set_ref_from_raw", 

54 "shorten_ref_name", 

55 "write_packed_refs", 

56] 

57 

58import os 

59import sys 

60import types 

61import warnings 

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

63from contextlib import suppress 

64from typing import ( 

65 IO, 

66 TYPE_CHECKING, 

67 Any, 

68 BinaryIO, 

69 NewType, 

70 overload, 

71) 

72 

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

74 from typing import Self 

75else: 

76 from typing_extensions import Self 

77 

78if TYPE_CHECKING: 

79 from .config import ConfigFile 

80 from .file import _GitFile 

81 

82from .errors import PackedRefsException, RefFormatError 

83from .file import GitFile, ensure_dir_exists 

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

85 

86Ref = NewType("Ref", bytes) 

87 

88HEADREF = Ref(b"HEAD") 

89SYMREF = b"ref: " 

90LOCAL_BRANCH_PREFIX = b"refs/heads/" 

91LOCAL_TAG_PREFIX = b"refs/tags/" 

92LOCAL_REMOTE_PREFIX = b"refs/remotes/" 

93LOCAL_NOTES_PREFIX = b"refs/notes/" 

94LOCAL_REPLACE_PREFIX = b"refs/replace/" 

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

96 

97 

98class SymrefLoop(Exception): 

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

100 

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

102 """Initialize SymrefLoop exception.""" 

103 self.ref = ref 

104 self.depth = depth 

105 

106 

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

108 """Parse a symref value. 

109 

110 Args: 

111 contents: Contents to parse 

112 Returns: Destination 

113 """ 

114 if contents.startswith(SYMREF): 

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

116 raise ValueError(contents) 

117 

118 

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

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

121 

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

123 

124 [1] 

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

126 

127 Args: 

128 refname: The refname to check 

129 Returns: True if refname is valid, False otherwise 

130 """ 

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

132 # separately to parallel [1]. 

133 if refname == b"@": 

134 return False 

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

136 return False 

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

138 return False 

139 for i, c in enumerate(refname): 

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

141 return False 

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

143 return False 

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

145 return False 

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

147 return False 

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

149 if not component: 

150 return False 

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

152 return False 

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

154 return False 

155 return True 

156 

157 

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

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

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

161 

162 

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

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

165 

166 Args: 

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

168 

169 Returns: 

170 Tuple of (remote_name, branch_name) 

171 

172 Raises: 

173 ValueError: If ref is not a valid remote ref 

174 """ 

175 if not ref.startswith(LOCAL_REMOTE_PREFIX): 

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

177 

178 # Remove the prefix 

179 remainder = ref[len(LOCAL_REMOTE_PREFIX) :] 

180 

181 # Split into remote name and branch name 

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

183 if len(parts) != 2: 

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

185 

186 remote_name, branch_name = parts 

187 return (remote_name, branch_name) 

188 

189 

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

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

192 

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

194 

195 Args: 

196 refs: The RefsContainer to set the ref in 

197 name: The ref name to set 

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

199 """ 

200 if raw_ref.startswith(SYMREF): 

201 # It's a symbolic ref 

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

203 refs.set_symbolic_ref(name, target) 

204 else: 

205 # It's a direct ObjectID 

206 refs[name] = ObjectID(raw_ref) 

207 

208 

209class RefsContainer: 

210 """A container for refs.""" 

211 

212 def __init__( 

213 self, 

214 logger: Callable[ 

215 [bytes, bytes, bytes, bytes | None, int | None, int | None, bytes], None 

216 ] 

217 | None = None, 

218 ) -> None: 

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

220 self._logger = logger 

221 

222 def _log( 

223 self, 

224 ref: bytes, 

225 old_sha: bytes | None, 

226 new_sha: bytes | None, 

227 committer: bytes | None = None, 

228 timestamp: int | None = None, 

229 timezone: int | None = None, 

230 message: bytes | None = None, 

231 ) -> None: 

232 if self._logger is None: 

233 return 

234 if message is None: 

235 return 

236 # Use ZERO_SHA for None values, matching git behavior 

237 if old_sha is None: 

238 old_sha = ZERO_SHA 

239 if new_sha is None: 

240 new_sha = ZERO_SHA 

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

242 

243 def set_symbolic_ref( 

244 self, 

245 name: Ref, 

246 other: Ref, 

247 committer: bytes | None = None, 

248 timestamp: int | None = None, 

249 timezone: int | None = None, 

250 message: bytes | None = None, 

251 ) -> None: 

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

253 

254 Args: 

255 name: Name of the ref to set 

256 other: Name of the ref to point at 

257 committer: Optional committer name/email 

258 timestamp: Optional timestamp 

259 timezone: Optional timezone 

260 message: Optional message 

261 """ 

262 raise NotImplementedError(self.set_symbolic_ref) 

263 

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

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

266 

267 Returns: Dictionary mapping ref names to SHA1s 

268 

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

270 present. 

271 """ 

272 raise NotImplementedError(self.get_packed_refs) 

273 

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

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

276 

277 Args: 

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

279 means remove the ref 

280 """ 

281 raise NotImplementedError(self.add_packed_refs) 

282 

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

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

285 

286 Args: 

287 name: Name of the ref to peel 

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

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

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

291 """ 

292 return None 

293 

294 def import_refs( 

295 self, 

296 base: Ref, 

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

298 committer: bytes | None = None, 

299 timestamp: bytes | None = None, 

300 timezone: bytes | None = None, 

301 message: bytes | None = None, 

302 prune: bool = False, 

303 ) -> None: 

304 """Import refs from another repository. 

305 

306 Args: 

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

308 other: Dictionary of refs to import 

309 committer: Optional committer for reflog 

310 timestamp: Optional timestamp for reflog 

311 timezone: Optional timezone for reflog 

312 message: Optional message for reflog 

313 prune: If True, remove refs not in other 

314 """ 

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

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

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

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

319 if prune: 

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

321 else: 

322 to_delete = set() 

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

324 if value is None: 

325 to_delete.add(name) 

326 else: 

327 self.set_if_equals( 

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

329 ) 

330 if to_delete: 

331 try: 

332 to_delete.remove(name) 

333 except KeyError: 

334 pass 

335 for ref in to_delete: 

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

337 

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

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

340 raise NotImplementedError(self.allkeys) 

341 

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

343 """Iterate over all reference keys.""" 

344 return iter(self.allkeys()) 

345 

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

347 """Refs present in this container. 

348 

349 Args: 

350 base: An optional base to return refs under. 

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

352 packed refs. 

353 """ 

354 if base is not None: 

355 return self.subkeys(base) 

356 else: 

357 return self.allkeys() 

358 

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

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

361 

362 Args: 

363 base: The base to return refs under. 

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

365 prefix is stripped from the ref names returned. 

366 """ 

367 keys: set[Ref] = set() 

368 base_len = len(base) + 1 

369 for refname in self.allkeys(): 

370 if refname.startswith(base): 

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

372 return keys 

373 

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

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

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

377 keys = self.keys(base) 

378 base_bytes: bytes 

379 if base is None: 

380 base_bytes = b"" 

381 else: 

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

383 for key in keys: 

384 try: 

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

386 except (SymrefLoop, KeyError): 

387 continue # Unable to resolve 

388 

389 return ret 

390 

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

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

393 

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

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

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

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

398 

399 Args: 

400 name: The name of the reference. 

401 

402 Raises: 

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

404 """ 

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

406 return 

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

408 raise RefFormatError(name) 

409 rest = Ref(name[5:]) 

410 if check_ref_format(rest): 

411 return 

412 # As of Dulwich 1.2.3 check_ref_format rejects empty path components 

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

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

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

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

417 if ( 

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

419 and check_ref_format(Ref(_collapse_slashes(rest))) 

420 ): 

421 warnings.warn( 

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

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

424 DeprecationWarning, 

425 stacklevel=3, 

426 ) 

427 return 

428 raise RefFormatError(name) 

429 

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

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

432 

433 Args: 

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

435 

436 Raises: 

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

438 """ 

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

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

441 

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

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

444 

445 Args: 

446 refname: The name of the reference 

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

448 not exist. 

449 """ 

450 contents = self.read_loose_ref(refname) 

451 if not contents: 

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

453 return contents 

454 

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

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

457 

458 Args: 

459 name: the refname to read 

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

461 not exist. 

462 """ 

463 raise NotImplementedError(self.read_loose_ref) 

464 

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

466 """Follow a reference name. 

467 

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

469 references in the chain 

470 """ 

471 contents: bytes | None = SYMREF + name 

472 depth = 0 

473 refnames: list[Ref] = [] 

474 while contents and contents.startswith(SYMREF): 

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

476 refnames.append(refname) 

477 contents = self.read_ref(refname) 

478 if not contents: 

479 break 

480 depth += 1 

481 if depth > 5: 

482 raise SymrefLoop(name, depth) 

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

484 

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

486 """Check if a reference exists.""" 

487 if self.read_ref(refname): 

488 return True 

489 return False 

490 

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

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

493 

494 This method follows all symbolic references. 

495 """ 

496 _, sha = self.follow(name) 

497 if sha is None: 

498 raise KeyError(name) 

499 return sha 

500 

501 def set_if_equals( 

502 self, 

503 name: Ref, 

504 old_ref: ObjectID | None, 

505 new_ref: ObjectID, 

506 committer: bytes | None = None, 

507 timestamp: int | None = None, 

508 timezone: int | None = None, 

509 message: bytes | None = None, 

510 ) -> bool: 

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

512 

513 This method follows all symbolic references if applicable for the 

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

515 operation. 

516 

517 Args: 

518 name: The refname to set. 

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

520 unconditionally. 

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

522 committer: Optional committer name/email 

523 timestamp: Optional timestamp 

524 timezone: Optional timezone 

525 message: Message for reflog 

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

527 """ 

528 raise NotImplementedError(self.set_if_equals) 

529 

530 def add_if_new( 

531 self, 

532 name: Ref, 

533 ref: ObjectID, 

534 committer: bytes | None = None, 

535 timestamp: int | None = None, 

536 timezone: int | None = None, 

537 message: bytes | None = None, 

538 ) -> bool: 

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

540 

541 Args: 

542 name: Ref name 

543 ref: Ref value 

544 committer: Optional committer name/email 

545 timestamp: Optional timestamp 

546 timezone: Optional timezone 

547 message: Optional message for reflog 

548 """ 

549 raise NotImplementedError(self.add_if_new) 

550 

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

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

553 

554 This method follows all symbolic references if applicable for the 

555 subclass. 

556 

557 Note: This method unconditionally overwrites the contents of a 

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

559 changed, use set_if_equals(). 

560 

561 Args: 

562 name: The refname to set. 

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

564 """ 

565 self._check_ref_value(ref) 

566 self.set_if_equals(name, None, ref) 

567 

568 def remove_if_equals( 

569 self, 

570 name: Ref, 

571 old_ref: ObjectID | None, 

572 committer: bytes | None = None, 

573 timestamp: int | None = None, 

574 timezone: int | None = None, 

575 message: bytes | None = None, 

576 ) -> bool: 

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

578 

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

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

581 operation. 

582 

583 Args: 

584 name: The refname to delete. 

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

586 delete unconditionally. 

587 committer: Optional committer name/email 

588 timestamp: Optional timestamp 

589 timezone: Optional timezone 

590 message: Message for reflog 

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

592 """ 

593 raise NotImplementedError(self.remove_if_equals) 

594 

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

596 """Remove a refname. 

597 

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

599 the subclass. 

600 

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

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

603 remove_if_equals(). 

604 

605 Args: 

606 name: The refname to delete. 

607 """ 

608 self.remove_if_equals(name, None) 

609 

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

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

612 

613 Returns: Dictionary mapping source ref to target ref 

614 """ 

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

616 for src in self.allkeys(): 

617 try: 

618 ref_value = self.read_ref(src) 

619 assert ref_value is not None 

620 dst = parse_symref_value(ref_value) 

621 except ValueError: 

622 pass 

623 else: 

624 ret[src] = Ref(dst) 

625 return ret 

626 

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

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

629 

630 Args: 

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

632 """ 

633 raise NotImplementedError(self.pack_refs) 

634 

635 

636class DictRefsContainer(RefsContainer): 

637 """RefsContainer backed by a simple dict. 

638 

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

640 threadsafe. 

641 """ 

642 

643 def __init__( 

644 self, 

645 refs: dict[Ref, bytes], 

646 logger: Callable[ 

647 [ 

648 bytes, 

649 bytes | None, 

650 bytes | None, 

651 bytes | None, 

652 int | None, 

653 int | None, 

654 bytes | None, 

655 ], 

656 None, 

657 ] 

658 | None = None, 

659 ) -> None: 

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

661 super().__init__(logger=logger) 

662 self._refs = refs 

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

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

665 

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

667 """Return all reference keys.""" 

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

669 

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

671 """Read a loose reference.""" 

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

673 

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

675 """Get packed references.""" 

676 return {} 

677 

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

679 for watcher in self._watchers: 

680 watcher._notify((ref, newsha)) 

681 

682 def set_symbolic_ref( 

683 self, 

684 name: Ref, 

685 other: Ref, 

686 committer: bytes | None = None, 

687 timestamp: int | None = None, 

688 timezone: int | None = None, 

689 message: bytes | None = None, 

690 ) -> None: 

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

692 

693 Args: 

694 name: Name of the ref to set 

695 other: Name of the ref to point at 

696 committer: Optional committer name for reflog 

697 timestamp: Optional timestamp for reflog 

698 timezone: Optional timezone for reflog 

699 message: Optional message for reflog 

700 """ 

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

702 new = SYMREF + other 

703 self._refs[name] = new 

704 self._notify(name, new) 

705 self._log( 

706 name, 

707 old, 

708 new, 

709 committer=committer, 

710 timestamp=timestamp, 

711 timezone=timezone, 

712 message=message, 

713 ) 

714 

715 def set_if_equals( 

716 self, 

717 name: Ref, 

718 old_ref: ObjectID | None, 

719 new_ref: ObjectID, 

720 committer: bytes | None = None, 

721 timestamp: int | None = None, 

722 timezone: int | None = None, 

723 message: bytes | None = None, 

724 ) -> bool: 

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

726 

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

728 an atomic compare-and-swap operation. 

729 

730 Args: 

731 name: The refname to set. 

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

733 unconditionally. 

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

735 committer: Optional committer name for reflog 

736 timestamp: Optional timestamp for reflog 

737 timezone: Optional timezone for reflog 

738 message: Optional message for reflog 

739 

740 Returns: 

741 True if the set was successful, False otherwise. 

742 """ 

743 self._check_ref_value(new_ref) 

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

745 return False 

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

747 self._check_refname(name) 

748 old = self._refs.get(name) 

749 self._refs[name] = new_ref 

750 self._notify(name, new_ref) 

751 self._log( 

752 name, 

753 old, 

754 new_ref, 

755 committer=committer, 

756 timestamp=timestamp, 

757 timezone=timezone, 

758 message=message, 

759 ) 

760 return True 

761 

762 def add_if_new( 

763 self, 

764 name: Ref, 

765 ref: ObjectID, 

766 committer: bytes | None = None, 

767 timestamp: int | None = None, 

768 timezone: int | None = None, 

769 message: bytes | None = None, 

770 ) -> bool: 

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

772 

773 Args: 

774 name: Ref name 

775 ref: Ref value 

776 committer: Optional committer name for reflog 

777 timestamp: Optional timestamp for reflog 

778 timezone: Optional timezone for reflog 

779 message: Optional message for reflog 

780 

781 Returns: 

782 True if the add was successful, False otherwise. 

783 """ 

784 self._check_ref_value(ref) 

785 if name in self._refs: 

786 return False 

787 self._refs[name] = ref 

788 self._notify(name, ref) 

789 self._log( 

790 name, 

791 None, 

792 ref, 

793 committer=committer, 

794 timestamp=timestamp, 

795 timezone=timezone, 

796 message=message, 

797 ) 

798 return True 

799 

800 def remove_if_equals( 

801 self, 

802 name: Ref, 

803 old_ref: ObjectID | None, 

804 committer: bytes | None = None, 

805 timestamp: int | None = None, 

806 timezone: int | None = None, 

807 message: bytes | None = None, 

808 ) -> bool: 

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

810 

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

812 perform an atomic compare-and-delete operation. 

813 

814 Args: 

815 name: The refname to delete. 

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

817 delete unconditionally. 

818 committer: Optional committer name for reflog 

819 timestamp: Optional timestamp for reflog 

820 timezone: Optional timezone for reflog 

821 message: Optional message for reflog 

822 

823 Returns: 

824 True if the delete was successful, False otherwise. 

825 """ 

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

827 return False 

828 try: 

829 old = self._refs.pop(name) 

830 except KeyError: 

831 pass 

832 else: 

833 self._notify(name, None) 

834 self._log( 

835 name, 

836 old, 

837 None, 

838 committer=committer, 

839 timestamp=timestamp, 

840 timezone=timezone, 

841 message=message, 

842 ) 

843 return True 

844 

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

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

847 return self._peeled.get(name) 

848 

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

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

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

852 # set_if_equal. 

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

854 self.set_if_equals(ref, None, sha) 

855 

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

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

858 self._peeled.update(peeled) 

859 

860 

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

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

863_PackedRefsKey = tuple[int, int, int, int, int] 

864 

865 

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

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

868 

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

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

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

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

873 

874 Args: 

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

876 Returns: An opaque, comparable key. 

877 """ 

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

879 

880 

881class DiskRefsContainer(RefsContainer): 

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

883 

884 def __init__( 

885 self, 

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

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

888 logger: Callable[ 

889 [bytes, bytes, bytes, bytes | None, int | None, int | None, bytes], None 

890 ] 

891 | None = None, 

892 ) -> None: 

893 """Initialize DiskRefsContainer.""" 

894 super().__init__(logger=logger) 

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

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

897 if worktree_path is None: 

898 self.worktree_path = self.path 

899 else: 

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

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

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

903 self._packed_refs_key: _PackedRefsKey | None = None 

904 

905 def __repr__(self) -> str: 

906 """Return string representation of DiskRefsContainer.""" 

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

908 

909 def _iter_dir( 

910 self, 

911 path: bytes, 

912 base: bytes, 

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

914 ) -> Iterator[Ref]: 

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

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

917 

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

919 directory = root[prefix_len:] 

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

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

922 if dir_filter is not None: 

923 dirs[:] = [ 

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

925 ] 

926 

927 for filename in files: 

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

929 if check_ref_format(Ref(refname)): 

930 yield Ref(refname) 

931 

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

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

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

935 if base != b"refs/": 

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

937 search_paths.append((path, None)) 

938 elif self.worktree_path == self.path: 

939 # Iterate through all the refs from the main worktree 

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

941 else: 

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

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

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

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

946 

947 for path, dir_filter in search_paths: 

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

949 

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

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

952 subkeys: set[Ref] = set() 

953 

954 for key in self._iter_loose_refs(base): 

955 if key.startswith(base): 

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

957 

958 for key in self.get_packed_refs(): 

959 if key.startswith(base): 

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

961 return subkeys 

962 

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

964 """Return all reference keys.""" 

965 allkeys: set[Ref] = set() 

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

967 allkeys.add(Ref(HEADREF)) 

968 

969 allkeys.update(self._iter_loose_refs()) 

970 allkeys.update(self.get_packed_refs()) 

971 return allkeys 

972 

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

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

975 path = name 

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

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

978 

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

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

981 

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

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

984 

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

986 no packed-refs file is present. 

987 """ 

988 try: 

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

990 except FileNotFoundError: 

991 return None 

992 return _packed_refs_key(st) 

993 

994 def _invalidate_packed_refs_cache(self) -> None: 

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

996 self._packed_refs = None 

997 self._peeled_refs = None 

998 self._packed_refs_key = None 

999 

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

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

1002 

1003 Returns: Dictionary mapping ref names to SHA1s 

1004 

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

1006 present. 

1007 """ 

1008 if ( 

1009 self._packed_refs is not None 

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

1011 ): 

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

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

1014 # so that it is reloaded below. 

1015 self._invalidate_packed_refs_cache() 

1016 

1017 if self._packed_refs is None: 

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

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

1020 self._packed_refs = {} 

1021 self._peeled_refs = {} 

1022 self._packed_refs_key = None 

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

1024 try: 

1025 f = GitFile(path, "rb") 

1026 except FileNotFoundError: 

1027 return {} 

1028 with f: 

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

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

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

1032 self._packed_refs[name] = sha 

1033 if peeled: 

1034 self._peeled_refs[name] = peeled 

1035 else: 

1036 f.seek(0) 

1037 for sha, name in read_packed_refs(f): 

1038 self._packed_refs[name] = sha 

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

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

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

1042 # was renamed into place while we were reading. 

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

1044 return self._packed_refs 

1045 

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

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

1048 

1049 Args: 

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

1051 means remove the ref 

1052 """ 

1053 if not new_refs: 

1054 return 

1055 

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

1057 

1058 try: 

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

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

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

1062 

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

1064 # sanity check 

1065 if ref == HEADREF: 

1066 raise ValueError("cannot pack HEAD") 

1067 

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

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

1070 # want to affect packed refs in here 

1071 with suppress(OSError): 

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

1073 

1074 if target is not None: 

1075 packed_refs[ref] = target 

1076 else: 

1077 packed_refs.pop(ref, None) 

1078 

1079 write_packed_refs(f, packed_refs, self._peeled_refs) 

1080 finally: 

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

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

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

1084 # instead. 

1085 self._invalidate_packed_refs_cache() 

1086 

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

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

1089 

1090 Args: 

1091 name: Name of the ref to peel 

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

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

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

1095 """ 

1096 self.get_packed_refs() 

1097 if ( 

1098 self._peeled_refs is None 

1099 or self._packed_refs is None 

1100 or name not in self._packed_refs 

1101 ): 

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

1103 return None 

1104 if name in self._peeled_refs: 

1105 return self._peeled_refs[name] 

1106 else: 

1107 # Known not peelable 

1108 return self[name] 

1109 

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

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

1112 

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

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

1115 

1116 Args: 

1117 name: the refname to read, relative to refpath 

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

1119 exist. 

1120 

1121 Raises: 

1122 IOError: if any other error occurs 

1123 """ 

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

1125 try: 

1126 self._check_refname(name) 

1127 except RefFormatError: 

1128 return None 

1129 filename = self.refpath(name) 

1130 try: 

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

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

1133 if header == SYMREF: 

1134 # Read only the first line 

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

1136 else: 

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

1138 f.seek(0) 

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

1140 return line 

1141 except (OSError, UnicodeError): 

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

1143 # particular, invalid or forbidden paths can raise weird 

1144 # errors depending on the specific operating system 

1145 return None 

1146 

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

1148 if name not in self.get_packed_refs(): 

1149 return 

1150 

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

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

1153 f = GitFile(filename, "wb") 

1154 try: 

1155 self._invalidate_packed_refs_cache() 

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

1157 peeled_refs = ( 

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

1159 ) 

1160 

1161 if name not in packed_refs: 

1162 f.abort() 

1163 return 

1164 

1165 del packed_refs[name] 

1166 if peeled_refs is not None: 

1167 peeled_refs.pop(name, None) 

1168 write_packed_refs(f, packed_refs, peeled_refs) 

1169 f.close() 

1170 finally: 

1171 if not f.closed: 

1172 f.abort() 

1173 self._invalidate_packed_refs_cache() 

1174 

1175 def set_symbolic_ref( 

1176 self, 

1177 name: Ref, 

1178 other: Ref, 

1179 committer: bytes | None = None, 

1180 timestamp: int | None = None, 

1181 timezone: int | None = None, 

1182 message: bytes | None = None, 

1183 ) -> None: 

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

1185 

1186 Args: 

1187 name: Name of the ref to set 

1188 other: Name of the ref to point at 

1189 committer: Optional committer name 

1190 timestamp: Optional timestamp 

1191 timezone: Optional timezone 

1192 message: Optional message to describe the change 

1193 """ 

1194 self._check_refname(name) 

1195 self._check_refname(other) 

1196 filename = self.refpath(name) 

1197 f = GitFile(filename, "wb") 

1198 try: 

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

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

1201 self._log( 

1202 name, 

1203 sha, 

1204 sha, 

1205 committer=committer, 

1206 timestamp=timestamp, 

1207 timezone=timezone, 

1208 message=message, 

1209 ) 

1210 except BaseException: 

1211 f.abort() 

1212 raise 

1213 else: 

1214 f.close() 

1215 

1216 def set_if_equals( 

1217 self, 

1218 name: Ref, 

1219 old_ref: ObjectID | None, 

1220 new_ref: ObjectID, 

1221 committer: bytes | None = None, 

1222 timestamp: int | None = None, 

1223 timezone: int | None = None, 

1224 message: bytes | None = None, 

1225 ) -> bool: 

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

1227 

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

1229 an atomic compare-and-swap operation. 

1230 

1231 Args: 

1232 name: The refname to set. 

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

1234 unconditionally. 

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

1236 committer: Optional committer name 

1237 timestamp: Optional timestamp 

1238 timezone: Optional timezone 

1239 message: Set message for reflog 

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

1241 """ 

1242 self._check_refname(name) 

1243 self._check_ref_value(new_ref) 

1244 try: 

1245 realnames, _ = self.follow(name) 

1246 realname = realnames[-1] 

1247 except (KeyError, IndexError, SymrefLoop): 

1248 realname = name 

1249 filename = self.refpath(realname) 

1250 

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

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

1253 packed_refs = self.get_packed_refs() 

1254 while probe_ref: 

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

1256 raise NotADirectoryError(filename) 

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

1258 

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

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

1261 if old_ref is not None: 

1262 try: 

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

1264 orig_ref = self.read_loose_ref(realname) 

1265 if orig_ref is None: 

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

1267 if orig_ref != old_ref: 

1268 f.abort() 

1269 return False 

1270 except OSError: 

1271 f.abort() 

1272 raise 

1273 

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

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

1276 current_ref = self.read_loose_ref(realname) 

1277 if current_ref is None: 

1278 current_ref = packed_refs.get(realname, None) 

1279 

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

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

1282 f.abort() 

1283 return True 

1284 

1285 try: 

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

1287 except OSError: 

1288 f.abort() 

1289 raise 

1290 self._log( 

1291 realname, 

1292 old_ref, 

1293 new_ref, 

1294 committer=committer, 

1295 timestamp=timestamp, 

1296 timezone=timezone, 

1297 message=message, 

1298 ) 

1299 return True 

1300 

1301 def add_if_new( 

1302 self, 

1303 name: Ref, 

1304 ref: ObjectID, 

1305 committer: bytes | None = None, 

1306 timestamp: int | None = None, 

1307 timezone: int | None = None, 

1308 message: bytes | None = None, 

1309 ) -> bool: 

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

1311 

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

1313 chain does not exist. 

1314 

1315 Args: 

1316 name: The refname to set. 

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

1318 committer: Optional committer name 

1319 timestamp: Optional timestamp 

1320 timezone: Optional timezone 

1321 message: Optional message for reflog 

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

1323 """ 

1324 self._check_ref_value(ref) 

1325 try: 

1326 realnames, contents = self.follow(name) 

1327 if contents is not None: 

1328 return False 

1329 realname = realnames[-1] 

1330 except (KeyError, IndexError): 

1331 realname = name 

1332 self._check_refname(realname) 

1333 filename = self.refpath(realname) 

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

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

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

1337 f.abort() 

1338 return False 

1339 try: 

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

1341 except OSError: 

1342 f.abort() 

1343 raise 

1344 else: 

1345 self._log( 

1346 name, 

1347 None, 

1348 ref, 

1349 committer=committer, 

1350 timestamp=timestamp, 

1351 timezone=timezone, 

1352 message=message, 

1353 ) 

1354 return True 

1355 

1356 def remove_if_equals( 

1357 self, 

1358 name: Ref, 

1359 old_ref: ObjectID | None, 

1360 committer: bytes | None = None, 

1361 timestamp: int | None = None, 

1362 timezone: int | None = None, 

1363 message: bytes | None = None, 

1364 ) -> bool: 

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

1366 

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

1368 perform an atomic compare-and-delete operation. 

1369 

1370 Args: 

1371 name: The refname to delete. 

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

1373 delete unconditionally. 

1374 committer: Optional committer name 

1375 timestamp: Optional timestamp 

1376 timezone: Optional timezone 

1377 message: Optional message 

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

1379 """ 

1380 self._check_refname(name) 

1381 filename = self.refpath(name) 

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

1383 f = GitFile(filename, "wb") 

1384 try: 

1385 if old_ref is not None: 

1386 orig_ref = self.read_loose_ref(name) 

1387 if orig_ref is None: 

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

1389 if orig_ref is None: 

1390 orig_ref = ZERO_SHA 

1391 if orig_ref != old_ref: 

1392 return False 

1393 

1394 # remove the reference file itself 

1395 try: 

1396 found = os.path.lexists(filename) 

1397 except OSError: 

1398 # may only be packed, or otherwise unstorable 

1399 found = False 

1400 

1401 if found: 

1402 os.remove(filename) 

1403 

1404 self._remove_packed_ref(name) 

1405 self._log( 

1406 name, 

1407 old_ref, 

1408 None, 

1409 committer=committer, 

1410 timestamp=timestamp, 

1411 timezone=timezone, 

1412 message=message, 

1413 ) 

1414 finally: 

1415 # never write, we just wanted the lock 

1416 f.abort() 

1417 

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

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

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

1421 parent = name 

1422 while True: 

1423 try: 

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

1425 parent = Ref(parent_bytes) 

1426 except ValueError: 

1427 break 

1428 

1429 if parent == b"refs": 

1430 break 

1431 parent_filename = self.refpath(parent) 

1432 try: 

1433 os.rmdir(parent_filename) 

1434 except OSError: 

1435 # this can be caused by the parent directory being 

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

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

1438 # removed the reference, just ignore it 

1439 break 

1440 

1441 return True 

1442 

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

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

1445 

1446 Args: 

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

1448 """ 

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

1450 for ref in self.allkeys(): 

1451 if ref == HEADREF: 

1452 # Never pack HEAD 

1453 continue 

1454 if all or ref.startswith(LOCAL_TAG_PREFIX): 

1455 try: 

1456 sha = self[ref] 

1457 if sha: 

1458 refs_to_pack[ref] = sha 

1459 except KeyError: 

1460 # Broken ref, skip it 

1461 pass 

1462 

1463 if refs_to_pack: 

1464 self.add_packed_refs(refs_to_pack) 

1465 

1466 

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

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

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

1470 if len(fields) != 2: 

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

1472 sha, name = fields 

1473 if not valid_hexsha(sha): 

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

1475 if not check_ref_format(Ref(name)): 

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

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

1478 

1479 

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

1481 """Read a packed refs file. 

1482 

1483 Args: 

1484 f: file-like object to read from 

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

1486 """ 

1487 for line in f: 

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

1489 # Comment 

1490 continue 

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

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

1493 yield _split_ref_line(line) 

1494 

1495 

1496def read_packed_refs_with_peeled( 

1497 f: IO[bytes], 

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

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

1500 

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

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

1503 

1504 Args: 

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

1506 """ 

1507 last = None 

1508 for line in f: 

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

1510 continue 

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

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

1513 if not last: 

1514 raise PackedRefsException("unexpected peeled ref line") 

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

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

1517 sha, name = _split_ref_line(last) 

1518 last = None 

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

1520 else: 

1521 if last: 

1522 sha, name = _split_ref_line(last) 

1523 yield (sha, name, None) 

1524 last = line 

1525 if last: 

1526 sha, name = _split_ref_line(last) 

1527 yield (sha, name, None) 

1528 

1529 

1530def write_packed_refs( 

1531 f: IO[bytes], 

1532 packed_refs: Mapping[Ref, ObjectID], 

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

1534) -> None: 

1535 """Write a packed refs file. 

1536 

1537 Args: 

1538 f: empty file-like object to write to 

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

1540 peeled_refs: dict of refname to peeled value of sha 

1541 """ 

1542 if peeled_refs is None: 

1543 peeled_refs = {} 

1544 else: 

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

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

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

1548 if refname in peeled_refs: 

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

1550 

1551 

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

1553 """Read info/refs file. 

1554 

1555 Args: 

1556 f: File-like object to read from 

1557 

1558 Returns: 

1559 Dictionary mapping ref names to SHA1s 

1560 """ 

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

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

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

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

1565 if len(parts) != 2: 

1566 raise ValueError( 

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

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

1569 ) 

1570 (sha, name) = parts 

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

1572 return ret 

1573 

1574 

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

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

1577 return x.startswith(LOCAL_BRANCH_PREFIX) 

1578 

1579 

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

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

1582 

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

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

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

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

1587 error in a future release. 

1588 """ 

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

1590 return name 

1591 warnings.warn( 

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

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

1594 DeprecationWarning, 

1595 stacklevel=3, 

1596 ) 

1597 return name.lstrip(b"/") 

1598 

1599 

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

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

1602 

1603 Args: 

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

1605 

1606 Returns: 

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

1608 

1609 Examples: 

1610 >>> local_branch_name(b"master") 

1611 b'refs/heads/master' 

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

1613 b'refs/heads/master' 

1614 """ 

1615 if name.startswith(LOCAL_BRANCH_PREFIX): 

1616 return Ref(name) 

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

1618 

1619 

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

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

1622 

1623 Args: 

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

1625 

1626 Returns: 

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

1628 

1629 Examples: 

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

1631 b'refs/tags/v1.0' 

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

1633 b'refs/tags/v1.0' 

1634 """ 

1635 if name.startswith(LOCAL_TAG_PREFIX): 

1636 return Ref(name) 

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

1638 

1639 

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

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

1642 

1643 Args: 

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

1645 

1646 Returns: 

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

1648 

1649 Examples: 

1650 >>> local_replace_name(b"abc123") 

1651 b'refs/replace/abc123' 

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

1653 b'refs/replace/abc123' 

1654 """ 

1655 if name.startswith(LOCAL_REPLACE_PREFIX): 

1656 return Ref(name) 

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

1658 

1659 

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

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

1662 

1663 Args: 

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

1665 

1666 Returns: 

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

1668 

1669 Raises: 

1670 ValueError: If ref is not a local branch 

1671 

1672 Examples: 

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

1674 b'master' 

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

1676 b'feature/foo' 

1677 """ 

1678 if not ref.startswith(LOCAL_BRANCH_PREFIX): 

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

1680 return ref[len(LOCAL_BRANCH_PREFIX) :] 

1681 

1682 

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

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

1685 

1686 Args: 

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

1688 

1689 Returns: 

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

1691 

1692 Raises: 

1693 ValueError: If ref is not a local tag 

1694 

1695 Examples: 

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

1697 b'v1.0' 

1698 """ 

1699 if not ref.startswith(LOCAL_TAG_PREFIX): 

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

1701 return ref[len(LOCAL_TAG_PREFIX) :] 

1702 

1703 

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

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

1706 

1707 Args: 

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

1709 

1710 Returns: 

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

1712 

1713 Examples: 

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

1715 b'master' 

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

1717 b'origin/main' 

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

1719 b'v1.0' 

1720 >>> shorten_ref_name(b"HEAD") 

1721 b'HEAD' 

1722 """ 

1723 if ref.startswith(LOCAL_BRANCH_PREFIX): 

1724 return ref[len(LOCAL_BRANCH_PREFIX) :] 

1725 elif ref.startswith(LOCAL_REMOTE_PREFIX): 

1726 return ref[len(LOCAL_REMOTE_PREFIX) :] 

1727 elif ref.startswith(LOCAL_TAG_PREFIX): 

1728 return ref[len(LOCAL_TAG_PREFIX) :] 

1729 return ref 

1730 

1731 

1732def _set_origin_head( 

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

1734) -> None: 

1735 # set refs/remotes/origin/HEAD 

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

1737 if origin_head and origin_head.startswith(LOCAL_BRANCH_PREFIX): 

1738 origin_ref = Ref(origin_base + HEADREF) 

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

1740 if target_ref in refs: 

1741 refs.set_symbolic_ref(origin_ref, target_ref) 

1742 

1743 

1744def _set_default_branch( 

1745 refs: RefsContainer, 

1746 origin: bytes, 

1747 origin_head: bytes | None, 

1748 branch: bytes | None, 

1749 ref_message: bytes | None, 

1750) -> bytes: 

1751 """Set the default branch.""" 

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

1753 if branch: 

1754 origin_ref = Ref(origin_base + branch) 

1755 if origin_ref in refs: 

1756 local_ref = Ref(local_branch_name(branch)) 

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

1758 head_ref = local_ref 

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

1760 head_ref = Ref(local_tag_name(branch)) 

1761 else: 

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

1763 elif origin_head: 

1764 head_ref = Ref(origin_head) 

1765 if origin_head.startswith(LOCAL_BRANCH_PREFIX): 

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

1767 else: 

1768 origin_ref = Ref(origin_head) 

1769 try: 

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

1771 except KeyError: 

1772 pass 

1773 else: 

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

1775 return head_ref 

1776 

1777 

1778def _set_head( 

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

1780) -> ObjectID | None: 

1781 if head_ref.startswith(LOCAL_TAG_PREFIX): 

1782 # detach HEAD at specified tag 

1783 head = refs[Ref(head_ref)] 

1784 del refs[HEADREF] 

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

1786 else: 

1787 # set HEAD to specific branch 

1788 try: 

1789 head = refs[Ref(head_ref)] 

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

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

1792 except KeyError: 

1793 head = None 

1794 return head 

1795 

1796 

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

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

1799 

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

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

1802 tag rather than on a branch. 

1803 

1804 Args: 

1805 config: Config of the freshly cloned repository 

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

1807 remote: Name of the remote the clone came from 

1808 """ 

1809 if not head_ref.startswith(LOCAL_BRANCH_PREFIX): 

1810 return 

1811 branch = extract_branch_name(Ref(head_ref)) 

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

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

1814 config.write_to_path() 

1815 

1816 

1817def _import_remote_refs( 

1818 refs_container: RefsContainer, 

1819 remote_name: str, 

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

1821 message: bytes | None = None, 

1822 prune: bool = False, 

1823 prune_tags: bool = False, 

1824) -> None: 

1825 from .protocol import PEELED_TAG_SUFFIX, strip_peeled_refs 

1826 

1827 stripped_refs = strip_peeled_refs(refs) 

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

1829 Ref(extract_branch_name(n)): v 

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

1831 if n.startswith(LOCAL_BRANCH_PREFIX) 

1832 } 

1833 refs_container.import_refs( 

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

1835 branches, 

1836 message=message, 

1837 prune=prune, 

1838 ) 

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

1840 Ref(extract_tag_name(n)): v 

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

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

1843 } 

1844 refs_container.import_refs( 

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

1846 ) 

1847 

1848 

1849class locked_ref: 

1850 """Lock a ref while making modifications. 

1851 

1852 Works as a context manager. 

1853 """ 

1854 

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

1856 """Initialize a locked ref. 

1857 

1858 Args: 

1859 refs_container: The DiskRefsContainer to lock the ref in 

1860 refname: The ref name to lock 

1861 """ 

1862 self._refs_container = refs_container 

1863 self._refname = refname 

1864 self._file: _GitFile | None = None 

1865 self._realname: Ref | None = None 

1866 self._deleted = False 

1867 

1868 def __enter__(self) -> Self: 

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

1870 

1871 Returns: 

1872 This locked_ref instance 

1873 

1874 Raises: 

1875 OSError: If the lock cannot be acquired 

1876 """ 

1877 self._refs_container._check_refname(self._refname) 

1878 try: 

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

1880 self._realname = realnames[-1] 

1881 except (KeyError, IndexError, SymrefLoop): 

1882 self._realname = self._refname 

1883 

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

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

1886 f = GitFile(filename, "wb") 

1887 self._file = f 

1888 return self 

1889 

1890 def __exit__( 

1891 self, 

1892 exc_type: type | None, 

1893 exc_value: BaseException | None, 

1894 traceback: types.TracebackType | None, 

1895 ) -> None: 

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

1897 

1898 Args: 

1899 exc_type: Type of exception if one occurred 

1900 exc_value: Exception instance if one occurred 

1901 traceback: Traceback if an exception occurred 

1902 """ 

1903 if self._file: 

1904 if exc_type is not None or self._deleted: 

1905 self._file.abort() 

1906 else: 

1907 self._file.close() 

1908 

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

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

1911 if not self._file: 

1912 raise RuntimeError("locked_ref not in context") 

1913 

1914 assert self._realname is not None 

1915 current_ref = self._refs_container.read_loose_ref(self._realname) 

1916 if current_ref is None: 

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

1918 self._realname, None 

1919 ) 

1920 return current_ref 

1921 

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

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

1924 

1925 Args: 

1926 expected_value: The expected current value of the ref 

1927 Returns: 

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

1929 """ 

1930 current_value = self.get() 

1931 return current_value == expected_value 

1932 

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

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

1935 

1936 Args: 

1937 new_ref: The new SHA1 or symbolic ref value 

1938 """ 

1939 if not self._file: 

1940 raise RuntimeError("locked_ref not in context") 

1941 

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

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

1944 

1945 self._file.seek(0) 

1946 self._file.truncate() 

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

1948 self._deleted = False 

1949 

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

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

1952 

1953 Args: 

1954 target: Name of the ref to point at 

1955 """ 

1956 if not self._file: 

1957 raise RuntimeError("locked_ref not in context") 

1958 

1959 self._refs_container._check_refname(target) 

1960 self._file.seek(0) 

1961 self._file.truncate() 

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

1963 self._deleted = False 

1964 

1965 def delete(self) -> None: 

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

1967 if not self._file: 

1968 raise RuntimeError("locked_ref not in context") 

1969 

1970 # Delete the actual ref file while holding the lock 

1971 if self._realname: 

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

1973 try: 

1974 if os.path.lexists(filename): 

1975 os.remove(filename) 

1976 except FileNotFoundError: 

1977 pass 

1978 self._refs_container._remove_packed_ref(self._realname) 

1979 

1980 self._deleted = True 

1981 

1982 

1983class NamespacedRefsContainer(RefsContainer): 

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

1985 

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

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

1988 within that namespace. 

1989 

1990 Example: 

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

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

1993 """ 

1994 

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

1996 """Initialize NamespacedRefsContainer. 

1997 

1998 Args: 

1999 refs: The underlying refs container to wrap 

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

2001 """ 

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

2003 self._refs = refs 

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

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

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

2007 self._namespace_prefix = b"" 

2008 for part in namespace_parts: 

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

2010 

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

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

2013 # HEAD and other special refs are not namespaced 

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

2015 return name 

2016 return self._namespace_prefix + name 

2017 

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

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

2020 

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

2022 """ 

2023 # HEAD and other special refs are not namespaced 

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

2025 return name 

2026 if name.startswith(self._namespace_prefix): 

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

2028 return None 

2029 

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

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

2032 keys: set[Ref] = set() 

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

2034 stripped = self._strip_namespace(key) 

2035 if stripped is not None: 

2036 keys.add(Ref(stripped)) 

2037 return keys 

2038 

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

2040 """Read a loose reference.""" 

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

2042 

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

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

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

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

2047 stripped = self._strip_namespace(name) 

2048 if stripped is not None: 

2049 packed[Ref(stripped)] = value 

2050 return packed 

2051 

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

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

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

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

2056 } 

2057 self._refs.add_packed_refs(namespaced_refs) 

2058 

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

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

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

2062 

2063 def set_symbolic_ref( 

2064 self, 

2065 name: Ref, 

2066 other: Ref, 

2067 committer: bytes | None = None, 

2068 timestamp: int | None = None, 

2069 timezone: int | None = None, 

2070 message: bytes | None = None, 

2071 ) -> None: 

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

2073 self._refs.set_symbolic_ref( 

2074 Ref(self._apply_namespace(name)), 

2075 Ref(self._apply_namespace(other)), 

2076 committer=committer, 

2077 timestamp=timestamp, 

2078 timezone=timezone, 

2079 message=message, 

2080 ) 

2081 

2082 def set_if_equals( 

2083 self, 

2084 name: Ref, 

2085 old_ref: ObjectID | None, 

2086 new_ref: ObjectID, 

2087 committer: bytes | None = None, 

2088 timestamp: int | None = None, 

2089 timezone: int | None = None, 

2090 message: bytes | None = None, 

2091 ) -> bool: 

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

2093 return self._refs.set_if_equals( 

2094 Ref(self._apply_namespace(name)), 

2095 old_ref, 

2096 new_ref, 

2097 committer=committer, 

2098 timestamp=timestamp, 

2099 timezone=timezone, 

2100 message=message, 

2101 ) 

2102 

2103 def add_if_new( 

2104 self, 

2105 name: Ref, 

2106 ref: ObjectID, 

2107 committer: bytes | None = None, 

2108 timestamp: int | None = None, 

2109 timezone: int | None = None, 

2110 message: bytes | None = None, 

2111 ) -> bool: 

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

2113 return self._refs.add_if_new( 

2114 Ref(self._apply_namespace(name)), 

2115 ref, 

2116 committer=committer, 

2117 timestamp=timestamp, 

2118 timezone=timezone, 

2119 message=message, 

2120 ) 

2121 

2122 def remove_if_equals( 

2123 self, 

2124 name: Ref, 

2125 old_ref: ObjectID | None, 

2126 committer: bytes | None = None, 

2127 timestamp: int | None = None, 

2128 timezone: int | None = None, 

2129 message: bytes | None = None, 

2130 ) -> bool: 

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

2132 return self._refs.remove_if_equals( 

2133 Ref(self._apply_namespace(name)), 

2134 old_ref, 

2135 committer=committer, 

2136 timestamp=timestamp, 

2137 timezone=timezone, 

2138 message=message, 

2139 ) 

2140 

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

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

2143 

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

2145 those in the namespace. 

2146 """ 

2147 self._refs.pack_refs(all=all) 

2148 

2149 

2150@overload 

2151def filter_ref_prefix( 

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

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

2154 

2155 

2156@overload 

2157def filter_ref_prefix( 

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

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

2160 

2161 

2162def filter_ref_prefix( 

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

2164 prefixes: Iterable[bytes], 

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

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

2167 

2168 Args: 

2169 refs: A dictionary of refs. 

2170 prefixes: The prefixes to filter by. 

2171 """ 

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

2173 

2174 

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

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

2177 

2178 Per-worktree references are: 

2179 - all pseudorefs, e.g. HEAD 

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

2181 

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

2183 

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

2185 """ 

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

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

2188 )