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

790 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 .file import _GitFile 

80 

81from .errors import PackedRefsException, RefFormatError 

82from .file import GitFile, ensure_dir_exists 

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

84 

85Ref = NewType("Ref", bytes) 

86 

87HEADREF = Ref(b"HEAD") 

88SYMREF = b"ref: " 

89LOCAL_BRANCH_PREFIX = b"refs/heads/" 

90LOCAL_TAG_PREFIX = b"refs/tags/" 

91LOCAL_REMOTE_PREFIX = b"refs/remotes/" 

92LOCAL_NOTES_PREFIX = b"refs/notes/" 

93LOCAL_REPLACE_PREFIX = b"refs/replace/" 

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

95 

96 

97class SymrefLoop(Exception): 

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

99 

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

101 """Initialize SymrefLoop exception.""" 

102 self.ref = ref 

103 self.depth = depth 

104 

105 

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

107 """Parse a symref value. 

108 

109 Args: 

110 contents: Contents to parse 

111 Returns: Destination 

112 """ 

113 if contents.startswith(SYMREF): 

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

115 raise ValueError(contents) 

116 

117 

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

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

120 

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

122 

123 [1] 

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

125 

126 Args: 

127 refname: The refname to check 

128 Returns: True if refname is valid, False otherwise 

129 """ 

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

131 # separately to parallel [1]. 

132 if refname == b"@": 

133 return False 

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

135 return False 

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

137 return False 

138 for i, c in enumerate(refname): 

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

140 return False 

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

142 return False 

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

144 return False 

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

146 return False 

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

148 if not component: 

149 return False 

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

151 return False 

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

153 return False 

154 return True 

155 

156 

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

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

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

160 

161 

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

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

164 

165 Args: 

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

167 

168 Returns: 

169 Tuple of (remote_name, branch_name) 

170 

171 Raises: 

172 ValueError: If ref is not a valid remote ref 

173 """ 

174 if not ref.startswith(LOCAL_REMOTE_PREFIX): 

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

176 

177 # Remove the prefix 

178 remainder = ref[len(LOCAL_REMOTE_PREFIX) :] 

179 

180 # Split into remote name and branch name 

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

182 if len(parts) != 2: 

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

184 

185 remote_name, branch_name = parts 

186 return (remote_name, branch_name) 

187 

188 

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

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

191 

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

193 

194 Args: 

195 refs: The RefsContainer to set the ref in 

196 name: The ref name to set 

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

198 """ 

199 if raw_ref.startswith(SYMREF): 

200 # It's a symbolic ref 

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

202 refs.set_symbolic_ref(name, target) 

203 else: 

204 # It's a direct ObjectID 

205 refs[name] = ObjectID(raw_ref) 

206 

207 

208class RefsContainer: 

209 """A container for refs.""" 

210 

211 def __init__( 

212 self, 

213 logger: Callable[ 

214 [bytes, bytes, bytes, bytes | None, int | None, int | None, bytes], None 

215 ] 

216 | None = None, 

217 ) -> None: 

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

219 self._logger = logger 

220 

221 def _log( 

222 self, 

223 ref: bytes, 

224 old_sha: bytes | None, 

225 new_sha: bytes | None, 

226 committer: bytes | None = None, 

227 timestamp: int | None = None, 

228 timezone: int | None = None, 

229 message: bytes | None = None, 

230 ) -> None: 

231 if self._logger is None: 

232 return 

233 if message is None: 

234 return 

235 # Use ZERO_SHA for None values, matching git behavior 

236 if old_sha is None: 

237 old_sha = ZERO_SHA 

238 if new_sha is None: 

239 new_sha = ZERO_SHA 

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

241 

242 def set_symbolic_ref( 

243 self, 

244 name: Ref, 

245 other: Ref, 

246 committer: bytes | None = None, 

247 timestamp: int | None = None, 

248 timezone: int | None = None, 

249 message: bytes | None = None, 

250 ) -> None: 

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

252 

253 Args: 

254 name: Name of the ref to set 

255 other: Name of the ref to point at 

256 committer: Optional committer name/email 

257 timestamp: Optional timestamp 

258 timezone: Optional timezone 

259 message: Optional message 

260 """ 

261 raise NotImplementedError(self.set_symbolic_ref) 

262 

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

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

265 

266 Returns: Dictionary mapping ref names to SHA1s 

267 

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

269 present. 

270 """ 

271 raise NotImplementedError(self.get_packed_refs) 

272 

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

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

275 

276 Args: 

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

278 means remove the ref 

279 """ 

280 raise NotImplementedError(self.add_packed_refs) 

281 

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

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

284 

285 Args: 

286 name: Name of the ref to peel 

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

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

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

290 """ 

291 return None 

292 

293 def import_refs( 

294 self, 

295 base: Ref, 

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

297 committer: bytes | None = None, 

298 timestamp: bytes | None = None, 

299 timezone: bytes | None = None, 

300 message: bytes | None = None, 

301 prune: bool = False, 

302 ) -> None: 

303 """Import refs from another repository. 

304 

305 Args: 

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

307 other: Dictionary of refs to import 

308 committer: Optional committer for reflog 

309 timestamp: Optional timestamp for reflog 

310 timezone: Optional timezone for reflog 

311 message: Optional message for reflog 

312 prune: If True, remove refs not in other 

313 """ 

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

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

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

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

318 if prune: 

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

320 else: 

321 to_delete = set() 

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

323 if value is None: 

324 to_delete.add(name) 

325 else: 

326 self.set_if_equals( 

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

328 ) 

329 if to_delete: 

330 try: 

331 to_delete.remove(name) 

332 except KeyError: 

333 pass 

334 for ref in to_delete: 

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

336 

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

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

339 raise NotImplementedError(self.allkeys) 

340 

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

342 """Iterate over all reference keys.""" 

343 return iter(self.allkeys()) 

344 

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

346 """Refs present in this container. 

347 

348 Args: 

349 base: An optional base to return refs under. 

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

351 packed refs. 

352 """ 

353 if base is not None: 

354 return self.subkeys(base) 

355 else: 

356 return self.allkeys() 

357 

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

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

360 

361 Args: 

362 base: The base to return refs under. 

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

364 prefix is stripped from the ref names returned. 

365 """ 

366 keys: set[Ref] = set() 

367 base_len = len(base) + 1 

368 for refname in self.allkeys(): 

369 if refname.startswith(base): 

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

371 return keys 

372 

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

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

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

376 keys = self.keys(base) 

377 base_bytes: bytes 

378 if base is None: 

379 base_bytes = b"" 

380 else: 

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

382 for key in keys: 

383 try: 

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

385 except (SymrefLoop, KeyError): 

386 continue # Unable to resolve 

387 

388 return ret 

389 

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

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

392 

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

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

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

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

397 

398 Args: 

399 name: The name of the reference. 

400 

401 Raises: 

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

403 """ 

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

405 return 

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

407 raise RefFormatError(name) 

408 rest = Ref(name[5:]) 

409 if check_ref_format(rest): 

410 return 

411 # As of Dulwich 1.2.3 check_ref_format rejects empty path components 

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

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

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

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

416 if ( 

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

418 and check_ref_format(Ref(_collapse_slashes(rest))) 

419 ): 

420 warnings.warn( 

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

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

423 DeprecationWarning, 

424 stacklevel=3, 

425 ) 

426 return 

427 raise RefFormatError(name) 

428 

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

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

431 

432 Args: 

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

434 

435 Raises: 

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

437 """ 

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

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

440 

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

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

443 

444 Args: 

445 refname: The name of the reference 

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

447 not exist. 

448 """ 

449 contents = self.read_loose_ref(refname) 

450 if not contents: 

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

452 return contents 

453 

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

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

456 

457 Args: 

458 name: the refname to read 

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

460 not exist. 

461 """ 

462 raise NotImplementedError(self.read_loose_ref) 

463 

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

465 """Follow a reference name. 

466 

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

468 references in the chain 

469 """ 

470 contents: bytes | None = SYMREF + name 

471 depth = 0 

472 refnames: list[Ref] = [] 

473 while contents and contents.startswith(SYMREF): 

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

475 refnames.append(refname) 

476 contents = self.read_ref(refname) 

477 if not contents: 

478 break 

479 depth += 1 

480 if depth > 5: 

481 raise SymrefLoop(name, depth) 

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

483 

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

485 """Check if a reference exists.""" 

486 if self.read_ref(refname): 

487 return True 

488 return False 

489 

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

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

492 

493 This method follows all symbolic references. 

494 """ 

495 _, sha = self.follow(name) 

496 if sha is None: 

497 raise KeyError(name) 

498 return sha 

499 

500 def set_if_equals( 

501 self, 

502 name: Ref, 

503 old_ref: ObjectID | None, 

504 new_ref: ObjectID, 

505 committer: bytes | None = None, 

506 timestamp: int | None = None, 

507 timezone: int | None = None, 

508 message: bytes | None = None, 

509 ) -> bool: 

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

511 

512 This method follows all symbolic references if applicable for the 

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

514 operation. 

515 

516 Args: 

517 name: The refname to set. 

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

519 unconditionally. 

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

521 committer: Optional committer name/email 

522 timestamp: Optional timestamp 

523 timezone: Optional timezone 

524 message: Message for reflog 

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

526 """ 

527 raise NotImplementedError(self.set_if_equals) 

528 

529 def add_if_new( 

530 self, 

531 name: Ref, 

532 ref: ObjectID, 

533 committer: bytes | None = None, 

534 timestamp: int | None = None, 

535 timezone: int | None = None, 

536 message: bytes | None = None, 

537 ) -> bool: 

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

539 

540 Args: 

541 name: Ref name 

542 ref: Ref value 

543 committer: Optional committer name/email 

544 timestamp: Optional timestamp 

545 timezone: Optional timezone 

546 message: Optional message for reflog 

547 """ 

548 raise NotImplementedError(self.add_if_new) 

549 

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

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

552 

553 This method follows all symbolic references if applicable for the 

554 subclass. 

555 

556 Note: This method unconditionally overwrites the contents of a 

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

558 changed, use set_if_equals(). 

559 

560 Args: 

561 name: The refname to set. 

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

563 """ 

564 self._check_ref_value(ref) 

565 self.set_if_equals(name, None, ref) 

566 

567 def remove_if_equals( 

568 self, 

569 name: Ref, 

570 old_ref: ObjectID | None, 

571 committer: bytes | None = None, 

572 timestamp: int | None = None, 

573 timezone: int | None = None, 

574 message: bytes | None = None, 

575 ) -> bool: 

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

577 

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

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

580 operation. 

581 

582 Args: 

583 name: The refname to delete. 

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

585 delete unconditionally. 

586 committer: Optional committer name/email 

587 timestamp: Optional timestamp 

588 timezone: Optional timezone 

589 message: Message for reflog 

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

591 """ 

592 raise NotImplementedError(self.remove_if_equals) 

593 

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

595 """Remove a refname. 

596 

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

598 the subclass. 

599 

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

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

602 remove_if_equals(). 

603 

604 Args: 

605 name: The refname to delete. 

606 """ 

607 self.remove_if_equals(name, None) 

608 

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

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

611 

612 Returns: Dictionary mapping source ref to target ref 

613 """ 

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

615 for src in self.allkeys(): 

616 try: 

617 ref_value = self.read_ref(src) 

618 assert ref_value is not None 

619 dst = parse_symref_value(ref_value) 

620 except ValueError: 

621 pass 

622 else: 

623 ret[src] = Ref(dst) 

624 return ret 

625 

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

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

628 

629 Args: 

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

631 """ 

632 raise NotImplementedError(self.pack_refs) 

633 

634 

635class DictRefsContainer(RefsContainer): 

636 """RefsContainer backed by a simple dict. 

637 

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

639 threadsafe. 

640 """ 

641 

642 def __init__( 

643 self, 

644 refs: dict[Ref, bytes], 

645 logger: Callable[ 

646 [ 

647 bytes, 

648 bytes | None, 

649 bytes | None, 

650 bytes | None, 

651 int | None, 

652 int | None, 

653 bytes | None, 

654 ], 

655 None, 

656 ] 

657 | None = None, 

658 ) -> None: 

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

660 super().__init__(logger=logger) 

661 self._refs = refs 

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

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

664 

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

666 """Return all reference keys.""" 

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

668 

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

670 """Read a loose reference.""" 

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

672 

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

674 """Get packed references.""" 

675 return {} 

676 

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

678 for watcher in self._watchers: 

679 watcher._notify((ref, newsha)) 

680 

681 def set_symbolic_ref( 

682 self, 

683 name: Ref, 

684 other: Ref, 

685 committer: bytes | None = None, 

686 timestamp: int | None = None, 

687 timezone: int | None = None, 

688 message: bytes | None = None, 

689 ) -> None: 

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

691 

692 Args: 

693 name: Name of the ref to set 

694 other: Name of the ref to point at 

695 committer: Optional committer name for reflog 

696 timestamp: Optional timestamp for reflog 

697 timezone: Optional timezone for reflog 

698 message: Optional message for reflog 

699 """ 

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

701 new = SYMREF + other 

702 self._refs[name] = new 

703 self._notify(name, new) 

704 self._log( 

705 name, 

706 old, 

707 new, 

708 committer=committer, 

709 timestamp=timestamp, 

710 timezone=timezone, 

711 message=message, 

712 ) 

713 

714 def set_if_equals( 

715 self, 

716 name: Ref, 

717 old_ref: ObjectID | None, 

718 new_ref: ObjectID, 

719 committer: bytes | None = None, 

720 timestamp: int | None = None, 

721 timezone: int | None = None, 

722 message: bytes | None = None, 

723 ) -> bool: 

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

725 

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

727 an atomic compare-and-swap operation. 

728 

729 Args: 

730 name: The refname to set. 

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

732 unconditionally. 

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

734 committer: Optional committer name for reflog 

735 timestamp: Optional timestamp for reflog 

736 timezone: Optional timezone for reflog 

737 message: Optional message for reflog 

738 

739 Returns: 

740 True if the set was successful, False otherwise. 

741 """ 

742 self._check_ref_value(new_ref) 

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

744 return False 

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

746 self._check_refname(name) 

747 old = self._refs.get(name) 

748 self._refs[name] = new_ref 

749 self._notify(name, new_ref) 

750 self._log( 

751 name, 

752 old, 

753 new_ref, 

754 committer=committer, 

755 timestamp=timestamp, 

756 timezone=timezone, 

757 message=message, 

758 ) 

759 return True 

760 

761 def add_if_new( 

762 self, 

763 name: Ref, 

764 ref: ObjectID, 

765 committer: bytes | None = None, 

766 timestamp: int | None = None, 

767 timezone: int | None = None, 

768 message: bytes | None = None, 

769 ) -> bool: 

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

771 

772 Args: 

773 name: Ref name 

774 ref: Ref value 

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 add was successful, False otherwise. 

782 """ 

783 self._check_ref_value(ref) 

784 if name in self._refs: 

785 return False 

786 self._refs[name] = ref 

787 self._notify(name, ref) 

788 self._log( 

789 name, 

790 None, 

791 ref, 

792 committer=committer, 

793 timestamp=timestamp, 

794 timezone=timezone, 

795 message=message, 

796 ) 

797 return True 

798 

799 def remove_if_equals( 

800 self, 

801 name: Ref, 

802 old_ref: ObjectID | None, 

803 committer: bytes | None = None, 

804 timestamp: int | None = None, 

805 timezone: int | None = None, 

806 message: bytes | None = None, 

807 ) -> bool: 

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

809 

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

811 perform an atomic compare-and-delete operation. 

812 

813 Args: 

814 name: The refname to delete. 

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

816 delete unconditionally. 

817 committer: Optional committer name for reflog 

818 timestamp: Optional timestamp for reflog 

819 timezone: Optional timezone for reflog 

820 message: Optional message for reflog 

821 

822 Returns: 

823 True if the delete was successful, False otherwise. 

824 """ 

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

826 return False 

827 try: 

828 old = self._refs.pop(name) 

829 except KeyError: 

830 pass 

831 else: 

832 self._notify(name, None) 

833 self._log( 

834 name, 

835 old, 

836 None, 

837 committer=committer, 

838 timestamp=timestamp, 

839 timezone=timezone, 

840 message=message, 

841 ) 

842 return True 

843 

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

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

846 return self._peeled.get(name) 

847 

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

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

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

851 # set_if_equal. 

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

853 self.set_if_equals(ref, None, sha) 

854 

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

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

857 self._peeled.update(peeled) 

858 

859 

860class DiskRefsContainer(RefsContainer): 

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

862 

863 def __init__( 

864 self, 

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

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

867 logger: Callable[ 

868 [bytes, bytes, bytes, bytes | None, int | None, int | None, bytes], None 

869 ] 

870 | None = None, 

871 ) -> None: 

872 """Initialize DiskRefsContainer.""" 

873 super().__init__(logger=logger) 

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

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

876 if worktree_path is None: 

877 self.worktree_path = self.path 

878 else: 

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

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

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

882 

883 def __repr__(self) -> str: 

884 """Return string representation of DiskRefsContainer.""" 

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

886 

887 def _iter_dir( 

888 self, 

889 path: bytes, 

890 base: bytes, 

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

892 ) -> Iterator[Ref]: 

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

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

895 

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

897 directory = root[prefix_len:] 

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

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

900 if dir_filter is not None: 

901 dirs[:] = [ 

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

903 ] 

904 

905 for filename in files: 

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

907 if check_ref_format(Ref(refname)): 

908 yield Ref(refname) 

909 

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

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

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

913 if base != b"refs/": 

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

915 search_paths.append((path, None)) 

916 elif self.worktree_path == self.path: 

917 # Iterate through all the refs from the main worktree 

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

919 else: 

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

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

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

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

924 

925 for path, dir_filter in search_paths: 

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

927 

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

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

930 subkeys: set[Ref] = set() 

931 

932 for key in self._iter_loose_refs(base): 

933 if key.startswith(base): 

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

935 

936 for key in self.get_packed_refs(): 

937 if key.startswith(base): 

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

939 return subkeys 

940 

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

942 """Return all reference keys.""" 

943 allkeys: set[Ref] = set() 

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

945 allkeys.add(Ref(HEADREF)) 

946 

947 allkeys.update(self._iter_loose_refs()) 

948 allkeys.update(self.get_packed_refs()) 

949 return allkeys 

950 

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

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

953 path = name 

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

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

956 

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

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

959 

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

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

962 

963 Returns: Dictionary mapping ref names to SHA1s 

964 

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

966 present. 

967 """ 

968 # TODO: invalidate the cache on repacking 

969 if self._packed_refs is None: 

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

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

972 self._packed_refs = {} 

973 self._peeled_refs = {} 

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

975 try: 

976 f = GitFile(path, "rb") 

977 except FileNotFoundError: 

978 return {} 

979 with f: 

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

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

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

983 self._packed_refs[name] = sha 

984 if peeled: 

985 self._peeled_refs[name] = peeled 

986 else: 

987 f.seek(0) 

988 for sha, name in read_packed_refs(f): 

989 self._packed_refs[name] = sha 

990 return self._packed_refs 

991 

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

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

994 

995 Args: 

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

997 means remove the ref 

998 """ 

999 if not new_refs: 

1000 return 

1001 

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

1003 

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

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

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

1007 

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

1009 # sanity check 

1010 if ref == HEADREF: 

1011 raise ValueError("cannot pack HEAD") 

1012 

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

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

1015 # want to affect packed refs in here 

1016 with suppress(OSError): 

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

1018 

1019 if target is not None: 

1020 packed_refs[ref] = target 

1021 else: 

1022 packed_refs.pop(ref, None) 

1023 

1024 write_packed_refs(f, packed_refs, self._peeled_refs) 

1025 

1026 self._packed_refs = packed_refs 

1027 

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

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

1030 

1031 Args: 

1032 name: Name of the ref to peel 

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

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

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

1036 """ 

1037 self.get_packed_refs() 

1038 if ( 

1039 self._peeled_refs is None 

1040 or self._packed_refs is None 

1041 or name not in self._packed_refs 

1042 ): 

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

1044 return None 

1045 if name in self._peeled_refs: 

1046 return self._peeled_refs[name] 

1047 else: 

1048 # Known not peelable 

1049 return self[name] 

1050 

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

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

1053 

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

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

1056 

1057 Args: 

1058 name: the refname to read, relative to refpath 

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

1060 exist. 

1061 

1062 Raises: 

1063 IOError: if any other error occurs 

1064 """ 

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

1066 try: 

1067 self._check_refname(name) 

1068 except RefFormatError: 

1069 return None 

1070 filename = self.refpath(name) 

1071 try: 

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

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

1074 if header == SYMREF: 

1075 # Read only the first line 

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

1077 else: 

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

1079 f.seek(0) 

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

1081 return line 

1082 except (OSError, UnicodeError): 

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

1084 # particular, invalid or forbidden paths can raise weird 

1085 # errors depending on the specific operating system 

1086 return None 

1087 

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

1089 if self._packed_refs is None: 

1090 return 

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

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

1093 f = GitFile(filename, "wb") 

1094 try: 

1095 self._packed_refs = None 

1096 self.get_packed_refs() 

1097 

1098 if self._packed_refs is None or name not in self._packed_refs: 

1099 f.abort() 

1100 return 

1101 

1102 del self._packed_refs[name] 

1103 if self._peeled_refs is not None: 

1104 with suppress(KeyError): 

1105 del self._peeled_refs[name] 

1106 write_packed_refs(f, self._packed_refs, self._peeled_refs) 

1107 f.close() 

1108 except BaseException: 

1109 f.abort() 

1110 raise 

1111 

1112 def set_symbolic_ref( 

1113 self, 

1114 name: Ref, 

1115 other: Ref, 

1116 committer: bytes | None = None, 

1117 timestamp: int | None = None, 

1118 timezone: int | None = None, 

1119 message: bytes | None = None, 

1120 ) -> None: 

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

1122 

1123 Args: 

1124 name: Name of the ref to set 

1125 other: Name of the ref to point at 

1126 committer: Optional committer name 

1127 timestamp: Optional timestamp 

1128 timezone: Optional timezone 

1129 message: Optional message to describe the change 

1130 """ 

1131 self._check_refname(name) 

1132 self._check_refname(other) 

1133 filename = self.refpath(name) 

1134 f = GitFile(filename, "wb") 

1135 try: 

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

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

1138 self._log( 

1139 name, 

1140 sha, 

1141 sha, 

1142 committer=committer, 

1143 timestamp=timestamp, 

1144 timezone=timezone, 

1145 message=message, 

1146 ) 

1147 except BaseException: 

1148 f.abort() 

1149 raise 

1150 else: 

1151 f.close() 

1152 

1153 def set_if_equals( 

1154 self, 

1155 name: Ref, 

1156 old_ref: ObjectID | None, 

1157 new_ref: ObjectID, 

1158 committer: bytes | None = None, 

1159 timestamp: int | None = None, 

1160 timezone: int | None = None, 

1161 message: bytes | None = None, 

1162 ) -> bool: 

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

1164 

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

1166 an atomic compare-and-swap operation. 

1167 

1168 Args: 

1169 name: The refname to set. 

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

1171 unconditionally. 

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

1173 committer: Optional committer name 

1174 timestamp: Optional timestamp 

1175 timezone: Optional timezone 

1176 message: Set message for reflog 

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

1178 """ 

1179 self._check_refname(name) 

1180 self._check_ref_value(new_ref) 

1181 try: 

1182 realnames, _ = self.follow(name) 

1183 realname = realnames[-1] 

1184 except (KeyError, IndexError, SymrefLoop): 

1185 realname = name 

1186 filename = self.refpath(realname) 

1187 

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

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

1190 packed_refs = self.get_packed_refs() 

1191 while probe_ref: 

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

1193 raise NotADirectoryError(filename) 

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

1195 

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

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

1198 if old_ref is not None: 

1199 try: 

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

1201 orig_ref = self.read_loose_ref(realname) 

1202 if orig_ref is None: 

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

1204 if orig_ref != old_ref: 

1205 f.abort() 

1206 return False 

1207 except OSError: 

1208 f.abort() 

1209 raise 

1210 

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

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

1213 current_ref = self.read_loose_ref(realname) 

1214 if current_ref is None: 

1215 current_ref = packed_refs.get(realname, None) 

1216 

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

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

1219 f.abort() 

1220 return True 

1221 

1222 try: 

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

1224 except OSError: 

1225 f.abort() 

1226 raise 

1227 self._log( 

1228 realname, 

1229 old_ref, 

1230 new_ref, 

1231 committer=committer, 

1232 timestamp=timestamp, 

1233 timezone=timezone, 

1234 message=message, 

1235 ) 

1236 return True 

1237 

1238 def add_if_new( 

1239 self, 

1240 name: Ref, 

1241 ref: ObjectID, 

1242 committer: bytes | None = None, 

1243 timestamp: int | None = None, 

1244 timezone: int | None = None, 

1245 message: bytes | None = None, 

1246 ) -> bool: 

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

1248 

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

1250 chain does not exist. 

1251 

1252 Args: 

1253 name: The refname to set. 

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

1255 committer: Optional committer name 

1256 timestamp: Optional timestamp 

1257 timezone: Optional timezone 

1258 message: Optional message for reflog 

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

1260 """ 

1261 self._check_ref_value(ref) 

1262 try: 

1263 realnames, contents = self.follow(name) 

1264 if contents is not None: 

1265 return False 

1266 realname = realnames[-1] 

1267 except (KeyError, IndexError): 

1268 realname = name 

1269 self._check_refname(realname) 

1270 filename = self.refpath(realname) 

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

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

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

1274 f.abort() 

1275 return False 

1276 try: 

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

1278 except OSError: 

1279 f.abort() 

1280 raise 

1281 else: 

1282 self._log( 

1283 name, 

1284 None, 

1285 ref, 

1286 committer=committer, 

1287 timestamp=timestamp, 

1288 timezone=timezone, 

1289 message=message, 

1290 ) 

1291 return True 

1292 

1293 def remove_if_equals( 

1294 self, 

1295 name: Ref, 

1296 old_ref: ObjectID | None, 

1297 committer: bytes | None = None, 

1298 timestamp: int | None = None, 

1299 timezone: int | None = None, 

1300 message: bytes | None = None, 

1301 ) -> bool: 

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

1303 

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

1305 perform an atomic compare-and-delete operation. 

1306 

1307 Args: 

1308 name: The refname to delete. 

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

1310 delete unconditionally. 

1311 committer: Optional committer name 

1312 timestamp: Optional timestamp 

1313 timezone: Optional timezone 

1314 message: Optional message 

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

1316 """ 

1317 self._check_refname(name) 

1318 filename = self.refpath(name) 

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

1320 f = GitFile(filename, "wb") 

1321 try: 

1322 if old_ref is not None: 

1323 orig_ref = self.read_loose_ref(name) 

1324 if orig_ref is None: 

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

1326 if orig_ref is None: 

1327 orig_ref = ZERO_SHA 

1328 if orig_ref != old_ref: 

1329 return False 

1330 

1331 # remove the reference file itself 

1332 try: 

1333 found = os.path.lexists(filename) 

1334 except OSError: 

1335 # may only be packed, or otherwise unstorable 

1336 found = False 

1337 

1338 if found: 

1339 os.remove(filename) 

1340 

1341 self._remove_packed_ref(name) 

1342 self._log( 

1343 name, 

1344 old_ref, 

1345 None, 

1346 committer=committer, 

1347 timestamp=timestamp, 

1348 timezone=timezone, 

1349 message=message, 

1350 ) 

1351 finally: 

1352 # never write, we just wanted the lock 

1353 f.abort() 

1354 

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

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

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

1358 parent = name 

1359 while True: 

1360 try: 

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

1362 parent = Ref(parent_bytes) 

1363 except ValueError: 

1364 break 

1365 

1366 if parent == b"refs": 

1367 break 

1368 parent_filename = self.refpath(parent) 

1369 try: 

1370 os.rmdir(parent_filename) 

1371 except OSError: 

1372 # this can be caused by the parent directory being 

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

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

1375 # removed the reference, just ignore it 

1376 break 

1377 

1378 return True 

1379 

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

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

1382 

1383 Args: 

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

1385 """ 

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

1387 for ref in self.allkeys(): 

1388 if ref == HEADREF: 

1389 # Never pack HEAD 

1390 continue 

1391 if all or ref.startswith(LOCAL_TAG_PREFIX): 

1392 try: 

1393 sha = self[ref] 

1394 if sha: 

1395 refs_to_pack[ref] = sha 

1396 except KeyError: 

1397 # Broken ref, skip it 

1398 pass 

1399 

1400 if refs_to_pack: 

1401 self.add_packed_refs(refs_to_pack) 

1402 

1403 

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

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

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

1407 if len(fields) != 2: 

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

1409 sha, name = fields 

1410 if not valid_hexsha(sha): 

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

1412 if not check_ref_format(Ref(name)): 

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

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

1415 

1416 

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

1418 """Read a packed refs file. 

1419 

1420 Args: 

1421 f: file-like object to read from 

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

1423 """ 

1424 for line in f: 

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

1426 # Comment 

1427 continue 

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

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

1430 yield _split_ref_line(line) 

1431 

1432 

1433def read_packed_refs_with_peeled( 

1434 f: IO[bytes], 

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

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

1437 

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

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

1440 

1441 Args: 

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

1443 """ 

1444 last = None 

1445 for line in f: 

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

1447 continue 

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

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

1450 if not last: 

1451 raise PackedRefsException("unexpected peeled ref line") 

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

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

1454 sha, name = _split_ref_line(last) 

1455 last = None 

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

1457 else: 

1458 if last: 

1459 sha, name = _split_ref_line(last) 

1460 yield (sha, name, None) 

1461 last = line 

1462 if last: 

1463 sha, name = _split_ref_line(last) 

1464 yield (sha, name, None) 

1465 

1466 

1467def write_packed_refs( 

1468 f: IO[bytes], 

1469 packed_refs: Mapping[Ref, ObjectID], 

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

1471) -> None: 

1472 """Write a packed refs file. 

1473 

1474 Args: 

1475 f: empty file-like object to write to 

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

1477 peeled_refs: dict of refname to peeled value of sha 

1478 """ 

1479 if peeled_refs is None: 

1480 peeled_refs = {} 

1481 else: 

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

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

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

1485 if refname in peeled_refs: 

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

1487 

1488 

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

1490 """Read info/refs file. 

1491 

1492 Args: 

1493 f: File-like object to read from 

1494 

1495 Returns: 

1496 Dictionary mapping ref names to SHA1s 

1497 """ 

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

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

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

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

1502 if len(parts) != 2: 

1503 raise ValueError( 

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

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

1506 ) 

1507 (sha, name) = parts 

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

1509 return ret 

1510 

1511 

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

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

1514 return x.startswith(LOCAL_BRANCH_PREFIX) 

1515 

1516 

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

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

1519 

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

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

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

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

1524 error in a future release. 

1525 """ 

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

1527 return name 

1528 warnings.warn( 

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

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

1531 DeprecationWarning, 

1532 stacklevel=3, 

1533 ) 

1534 return name.lstrip(b"/") 

1535 

1536 

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

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

1539 

1540 Args: 

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

1542 

1543 Returns: 

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

1545 

1546 Examples: 

1547 >>> local_branch_name(b"master") 

1548 b'refs/heads/master' 

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

1550 b'refs/heads/master' 

1551 """ 

1552 if name.startswith(LOCAL_BRANCH_PREFIX): 

1553 return Ref(name) 

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

1555 

1556 

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

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

1559 

1560 Args: 

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

1562 

1563 Returns: 

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

1565 

1566 Examples: 

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

1568 b'refs/tags/v1.0' 

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

1570 b'refs/tags/v1.0' 

1571 """ 

1572 if name.startswith(LOCAL_TAG_PREFIX): 

1573 return Ref(name) 

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

1575 

1576 

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

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

1579 

1580 Args: 

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

1582 

1583 Returns: 

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

1585 

1586 Examples: 

1587 >>> local_replace_name(b"abc123") 

1588 b'refs/replace/abc123' 

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

1590 b'refs/replace/abc123' 

1591 """ 

1592 if name.startswith(LOCAL_REPLACE_PREFIX): 

1593 return Ref(name) 

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

1595 

1596 

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

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

1599 

1600 Args: 

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

1602 

1603 Returns: 

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

1605 

1606 Raises: 

1607 ValueError: If ref is not a local branch 

1608 

1609 Examples: 

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

1611 b'master' 

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

1613 b'feature/foo' 

1614 """ 

1615 if not ref.startswith(LOCAL_BRANCH_PREFIX): 

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

1617 return ref[len(LOCAL_BRANCH_PREFIX) :] 

1618 

1619 

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

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

1622 

1623 Args: 

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

1625 

1626 Returns: 

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

1628 

1629 Raises: 

1630 ValueError: If ref is not a local tag 

1631 

1632 Examples: 

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

1634 b'v1.0' 

1635 """ 

1636 if not ref.startswith(LOCAL_TAG_PREFIX): 

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

1638 return ref[len(LOCAL_TAG_PREFIX) :] 

1639 

1640 

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

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

1643 

1644 Args: 

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

1646 

1647 Returns: 

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

1649 

1650 Examples: 

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

1652 b'master' 

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

1654 b'origin/main' 

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

1656 b'v1.0' 

1657 >>> shorten_ref_name(b"HEAD") 

1658 b'HEAD' 

1659 """ 

1660 if ref.startswith(LOCAL_BRANCH_PREFIX): 

1661 return ref[len(LOCAL_BRANCH_PREFIX) :] 

1662 elif ref.startswith(LOCAL_REMOTE_PREFIX): 

1663 return ref[len(LOCAL_REMOTE_PREFIX) :] 

1664 elif ref.startswith(LOCAL_TAG_PREFIX): 

1665 return ref[len(LOCAL_TAG_PREFIX) :] 

1666 return ref 

1667 

1668 

1669def _set_origin_head( 

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

1671) -> None: 

1672 # set refs/remotes/origin/HEAD 

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

1674 if origin_head and origin_head.startswith(LOCAL_BRANCH_PREFIX): 

1675 origin_ref = Ref(origin_base + HEADREF) 

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

1677 if target_ref in refs: 

1678 refs.set_symbolic_ref(origin_ref, target_ref) 

1679 

1680 

1681def _set_default_branch( 

1682 refs: RefsContainer, 

1683 origin: bytes, 

1684 origin_head: bytes | None, 

1685 branch: bytes | None, 

1686 ref_message: bytes | None, 

1687) -> bytes: 

1688 """Set the default branch.""" 

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

1690 if branch: 

1691 origin_ref = Ref(origin_base + branch) 

1692 if origin_ref in refs: 

1693 local_ref = Ref(local_branch_name(branch)) 

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

1695 head_ref = local_ref 

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

1697 head_ref = Ref(local_tag_name(branch)) 

1698 else: 

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

1700 elif origin_head: 

1701 head_ref = Ref(origin_head) 

1702 if origin_head.startswith(LOCAL_BRANCH_PREFIX): 

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

1704 else: 

1705 origin_ref = Ref(origin_head) 

1706 try: 

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

1708 except KeyError: 

1709 pass 

1710 else: 

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

1712 return head_ref 

1713 

1714 

1715def _set_head( 

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

1717) -> ObjectID | None: 

1718 if head_ref.startswith(LOCAL_TAG_PREFIX): 

1719 # detach HEAD at specified tag 

1720 head = refs[Ref(head_ref)] 

1721 del refs[HEADREF] 

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

1723 else: 

1724 # set HEAD to specific branch 

1725 try: 

1726 head = refs[Ref(head_ref)] 

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

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

1729 except KeyError: 

1730 head = None 

1731 return head 

1732 

1733 

1734def _import_remote_refs( 

1735 refs_container: RefsContainer, 

1736 remote_name: str, 

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

1738 message: bytes | None = None, 

1739 prune: bool = False, 

1740 prune_tags: bool = False, 

1741) -> None: 

1742 from .protocol import PEELED_TAG_SUFFIX, strip_peeled_refs 

1743 

1744 stripped_refs = strip_peeled_refs(refs) 

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

1746 Ref(extract_branch_name(n)): v 

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

1748 if n.startswith(LOCAL_BRANCH_PREFIX) 

1749 } 

1750 refs_container.import_refs( 

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

1752 branches, 

1753 message=message, 

1754 prune=prune, 

1755 ) 

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

1757 Ref(extract_tag_name(n)): v 

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

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

1760 } 

1761 refs_container.import_refs( 

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

1763 ) 

1764 

1765 

1766class locked_ref: 

1767 """Lock a ref while making modifications. 

1768 

1769 Works as a context manager. 

1770 """ 

1771 

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

1773 """Initialize a locked ref. 

1774 

1775 Args: 

1776 refs_container: The DiskRefsContainer to lock the ref in 

1777 refname: The ref name to lock 

1778 """ 

1779 self._refs_container = refs_container 

1780 self._refname = refname 

1781 self._file: _GitFile | None = None 

1782 self._realname: Ref | None = None 

1783 self._deleted = False 

1784 

1785 def __enter__(self) -> Self: 

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

1787 

1788 Returns: 

1789 This locked_ref instance 

1790 

1791 Raises: 

1792 OSError: If the lock cannot be acquired 

1793 """ 

1794 self._refs_container._check_refname(self._refname) 

1795 try: 

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

1797 self._realname = realnames[-1] 

1798 except (KeyError, IndexError, SymrefLoop): 

1799 self._realname = self._refname 

1800 

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

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

1803 f = GitFile(filename, "wb") 

1804 self._file = f 

1805 return self 

1806 

1807 def __exit__( 

1808 self, 

1809 exc_type: type | None, 

1810 exc_value: BaseException | None, 

1811 traceback: types.TracebackType | None, 

1812 ) -> None: 

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

1814 

1815 Args: 

1816 exc_type: Type of exception if one occurred 

1817 exc_value: Exception instance if one occurred 

1818 traceback: Traceback if an exception occurred 

1819 """ 

1820 if self._file: 

1821 if exc_type is not None or self._deleted: 

1822 self._file.abort() 

1823 else: 

1824 self._file.close() 

1825 

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

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

1828 if not self._file: 

1829 raise RuntimeError("locked_ref not in context") 

1830 

1831 assert self._realname is not None 

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

1833 if current_ref is None: 

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

1835 self._realname, None 

1836 ) 

1837 return current_ref 

1838 

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

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

1841 

1842 Args: 

1843 expected_value: The expected current value of the ref 

1844 Returns: 

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

1846 """ 

1847 current_value = self.get() 

1848 return current_value == expected_value 

1849 

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

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

1852 

1853 Args: 

1854 new_ref: The new SHA1 or symbolic ref value 

1855 """ 

1856 if not self._file: 

1857 raise RuntimeError("locked_ref not in context") 

1858 

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

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

1861 

1862 self._file.seek(0) 

1863 self._file.truncate() 

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

1865 self._deleted = False 

1866 

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

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

1869 

1870 Args: 

1871 target: Name of the ref to point at 

1872 """ 

1873 if not self._file: 

1874 raise RuntimeError("locked_ref not in context") 

1875 

1876 self._refs_container._check_refname(target) 

1877 self._file.seek(0) 

1878 self._file.truncate() 

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

1880 self._deleted = False 

1881 

1882 def delete(self) -> None: 

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

1884 if not self._file: 

1885 raise RuntimeError("locked_ref not in context") 

1886 

1887 # Delete the actual ref file while holding the lock 

1888 if self._realname: 

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

1890 try: 

1891 if os.path.lexists(filename): 

1892 os.remove(filename) 

1893 except FileNotFoundError: 

1894 pass 

1895 self._refs_container._remove_packed_ref(self._realname) 

1896 

1897 self._deleted = True 

1898 

1899 

1900class NamespacedRefsContainer(RefsContainer): 

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

1902 

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

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

1905 within that namespace. 

1906 

1907 Example: 

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

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

1910 """ 

1911 

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

1913 """Initialize NamespacedRefsContainer. 

1914 

1915 Args: 

1916 refs: The underlying refs container to wrap 

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

1918 """ 

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

1920 self._refs = refs 

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

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

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

1924 self._namespace_prefix = b"" 

1925 for part in namespace_parts: 

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

1927 

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

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

1930 # HEAD and other special refs are not namespaced 

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

1932 return name 

1933 return self._namespace_prefix + name 

1934 

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

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

1937 

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

1939 """ 

1940 # HEAD and other special refs are not namespaced 

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

1942 return name 

1943 if name.startswith(self._namespace_prefix): 

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

1945 return None 

1946 

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

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

1949 keys: set[Ref] = set() 

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

1951 stripped = self._strip_namespace(key) 

1952 if stripped is not None: 

1953 keys.add(Ref(stripped)) 

1954 return keys 

1955 

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

1957 """Read a loose reference.""" 

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

1959 

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

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

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

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

1964 stripped = self._strip_namespace(name) 

1965 if stripped is not None: 

1966 packed[Ref(stripped)] = value 

1967 return packed 

1968 

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

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

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

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

1973 } 

1974 self._refs.add_packed_refs(namespaced_refs) 

1975 

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

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

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

1979 

1980 def set_symbolic_ref( 

1981 self, 

1982 name: Ref, 

1983 other: Ref, 

1984 committer: bytes | None = None, 

1985 timestamp: int | None = None, 

1986 timezone: int | None = None, 

1987 message: bytes | None = None, 

1988 ) -> None: 

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

1990 self._refs.set_symbolic_ref( 

1991 Ref(self._apply_namespace(name)), 

1992 Ref(self._apply_namespace(other)), 

1993 committer=committer, 

1994 timestamp=timestamp, 

1995 timezone=timezone, 

1996 message=message, 

1997 ) 

1998 

1999 def set_if_equals( 

2000 self, 

2001 name: Ref, 

2002 old_ref: ObjectID | None, 

2003 new_ref: ObjectID, 

2004 committer: bytes | None = None, 

2005 timestamp: int | None = None, 

2006 timezone: int | None = None, 

2007 message: bytes | None = None, 

2008 ) -> bool: 

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

2010 return self._refs.set_if_equals( 

2011 Ref(self._apply_namespace(name)), 

2012 old_ref, 

2013 new_ref, 

2014 committer=committer, 

2015 timestamp=timestamp, 

2016 timezone=timezone, 

2017 message=message, 

2018 ) 

2019 

2020 def add_if_new( 

2021 self, 

2022 name: Ref, 

2023 ref: ObjectID, 

2024 committer: bytes | None = None, 

2025 timestamp: int | None = None, 

2026 timezone: int | None = None, 

2027 message: bytes | None = None, 

2028 ) -> bool: 

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

2030 return self._refs.add_if_new( 

2031 Ref(self._apply_namespace(name)), 

2032 ref, 

2033 committer=committer, 

2034 timestamp=timestamp, 

2035 timezone=timezone, 

2036 message=message, 

2037 ) 

2038 

2039 def remove_if_equals( 

2040 self, 

2041 name: Ref, 

2042 old_ref: ObjectID | None, 

2043 committer: bytes | None = None, 

2044 timestamp: int | None = None, 

2045 timezone: int | None = None, 

2046 message: bytes | None = None, 

2047 ) -> bool: 

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

2049 return self._refs.remove_if_equals( 

2050 Ref(self._apply_namespace(name)), 

2051 old_ref, 

2052 committer=committer, 

2053 timestamp=timestamp, 

2054 timezone=timezone, 

2055 message=message, 

2056 ) 

2057 

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

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

2060 

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

2062 those in the namespace. 

2063 """ 

2064 self._refs.pack_refs(all=all) 

2065 

2066 

2067@overload 

2068def filter_ref_prefix( 

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

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

2071 

2072 

2073@overload 

2074def filter_ref_prefix( 

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

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

2077 

2078 

2079def filter_ref_prefix( 

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

2081 prefixes: Iterable[bytes], 

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

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

2084 

2085 Args: 

2086 refs: A dictionary of refs. 

2087 prefixes: The prefixes to filter by. 

2088 """ 

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

2090 

2091 

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

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

2094 

2095 Per-worktree references are: 

2096 - all pseudorefs, e.g. HEAD 

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

2098 

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

2100 

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

2102 """ 

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

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

2105 )