refs.py 55 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726
  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. """Ref handling."""
  22. import os
  23. import types
  24. import warnings
  25. from collections.abc import Iterable, Iterator
  26. from contextlib import suppress
  27. from typing import (
  28. IO,
  29. TYPE_CHECKING,
  30. Any,
  31. BinaryIO,
  32. Callable,
  33. Optional,
  34. TypeVar,
  35. Union,
  36. cast,
  37. )
  38. if TYPE_CHECKING:
  39. from .file import _GitFile
  40. from .errors import PackedRefsException, RefFormatError
  41. from .file import GitFile, ensure_dir_exists
  42. from .objects import ZERO_SHA, ObjectID, Tag, git_line, valid_hexsha
  43. from .pack import ObjectContainer
  44. Ref = bytes
  45. HEADREF = b"HEAD"
  46. SYMREF = b"ref: "
  47. LOCAL_BRANCH_PREFIX = b"refs/heads/"
  48. LOCAL_TAG_PREFIX = b"refs/tags/"
  49. LOCAL_REMOTE_PREFIX = b"refs/remotes/"
  50. LOCAL_NOTES_PREFIX = b"refs/notes/"
  51. BAD_REF_CHARS = set(b"\177 ~^:?*[")
  52. PEELED_TAG_SUFFIX = b"^{}"
  53. # For backwards compatibility
  54. ANNOTATED_TAG_SUFFIX = PEELED_TAG_SUFFIX
  55. class SymrefLoop(Exception):
  56. """There is a loop between one or more symrefs."""
  57. def __init__(self, ref: bytes, depth: int) -> None:
  58. self.ref = ref
  59. self.depth = depth
  60. def parse_symref_value(contents: bytes) -> bytes:
  61. """Parse a symref value.
  62. Args:
  63. contents: Contents to parse
  64. Returns: Destination
  65. """
  66. if contents.startswith(SYMREF):
  67. return contents[len(SYMREF) :].rstrip(b"\r\n")
  68. raise ValueError(contents)
  69. def check_ref_format(refname: Ref) -> bool:
  70. """Check if a refname is correctly formatted.
  71. Implements all the same rules as git-check-ref-format[1].
  72. [1]
  73. http://www.kernel.org/pub/software/scm/git/docs/git-check-ref-format.html
  74. Args:
  75. refname: The refname to check
  76. Returns: True if refname is valid, False otherwise
  77. """
  78. # These could be combined into one big expression, but are listed
  79. # separately to parallel [1].
  80. if b"/." in refname or refname.startswith(b"."):
  81. return False
  82. if b"/" not in refname:
  83. return False
  84. if b".." in refname:
  85. return False
  86. for i, c in enumerate(refname):
  87. if ord(refname[i : i + 1]) < 0o40 or c in BAD_REF_CHARS:
  88. return False
  89. if refname[-1] in b"/.":
  90. return False
  91. if refname.endswith(b".lock"):
  92. return False
  93. if b"@{" in refname:
  94. return False
  95. if b"\\" in refname:
  96. return False
  97. return True
  98. def parse_remote_ref(ref: bytes) -> tuple[bytes, bytes]:
  99. """Parse a remote ref into remote name and branch name.
  100. Args:
  101. ref: Remote ref like b"refs/remotes/origin/main"
  102. Returns:
  103. Tuple of (remote_name, branch_name)
  104. Raises:
  105. ValueError: If ref is not a valid remote ref
  106. """
  107. if not ref.startswith(LOCAL_REMOTE_PREFIX):
  108. raise ValueError(f"Not a remote ref: {ref!r}")
  109. # Remove the prefix
  110. remainder = ref[len(LOCAL_REMOTE_PREFIX) :]
  111. # Split into remote name and branch name
  112. parts = remainder.split(b"/", 1)
  113. if len(parts) != 2:
  114. raise ValueError(f"Invalid remote ref format: {ref!r}")
  115. remote_name, branch_name = parts
  116. return (remote_name, branch_name)
  117. class RefsContainer:
  118. """A container for refs."""
  119. def __init__(
  120. self,
  121. logger: Optional[
  122. Callable[
  123. [
  124. bytes,
  125. Optional[bytes],
  126. Optional[bytes],
  127. Optional[bytes],
  128. Optional[int],
  129. Optional[int],
  130. Optional[bytes],
  131. ],
  132. None,
  133. ]
  134. ] = None,
  135. ) -> None:
  136. self._logger = logger
  137. def _log(
  138. self,
  139. ref: bytes,
  140. old_sha: Optional[bytes],
  141. new_sha: Optional[bytes],
  142. committer: Optional[bytes] = None,
  143. timestamp: Optional[int] = None,
  144. timezone: Optional[int] = None,
  145. message: Optional[bytes] = None,
  146. ) -> None:
  147. if self._logger is None:
  148. return
  149. if message is None:
  150. return
  151. self._logger(ref, old_sha, new_sha, committer, timestamp, timezone, message)
  152. def set_symbolic_ref(
  153. self,
  154. name: bytes,
  155. other: bytes,
  156. committer: Optional[bytes] = None,
  157. timestamp: Optional[int] = None,
  158. timezone: Optional[int] = None,
  159. message: Optional[bytes] = None,
  160. ) -> None:
  161. """Make a ref point at another ref.
  162. Args:
  163. name: Name of the ref to set
  164. other: Name of the ref to point at
  165. committer: Optional committer name/email
  166. timestamp: Optional timestamp
  167. timezone: Optional timezone
  168. message: Optional message
  169. """
  170. raise NotImplementedError(self.set_symbolic_ref)
  171. def get_packed_refs(self) -> dict[Ref, ObjectID]:
  172. """Get contents of the packed-refs file.
  173. Returns: Dictionary mapping ref names to SHA1s
  174. Note: Will return an empty dictionary when no packed-refs file is
  175. present.
  176. """
  177. raise NotImplementedError(self.get_packed_refs)
  178. def add_packed_refs(self, new_refs: dict[Ref, Optional[ObjectID]]) -> None:
  179. """Add the given refs as packed refs.
  180. Args:
  181. new_refs: A mapping of ref names to targets; if a target is None that
  182. means remove the ref
  183. """
  184. raise NotImplementedError(self.add_packed_refs)
  185. def get_peeled(self, name: bytes) -> Optional[ObjectID]:
  186. """Return the cached peeled value of a ref, if available.
  187. Args:
  188. name: Name of the ref to peel
  189. Returns: The peeled value of the ref. If the ref is known not point to
  190. a tag, this will be the SHA the ref refers to. If the ref may point
  191. to a tag, but no cached information is available, None is returned.
  192. """
  193. return None
  194. def import_refs(
  195. self,
  196. base: Ref,
  197. other: dict[Ref, ObjectID],
  198. committer: Optional[bytes] = None,
  199. timestamp: Optional[bytes] = None,
  200. timezone: Optional[bytes] = None,
  201. message: Optional[bytes] = None,
  202. prune: bool = False,
  203. ) -> None:
  204. """Import refs from another repository.
  205. Args:
  206. base: Base ref to import into (e.g., b'refs/remotes/origin')
  207. other: Dictionary of refs to import
  208. committer: Optional committer for reflog
  209. timestamp: Optional timestamp for reflog
  210. timezone: Optional timezone for reflog
  211. message: Optional message for reflog
  212. prune: If True, remove refs not in other
  213. """
  214. if prune:
  215. to_delete = set(self.subkeys(base))
  216. else:
  217. to_delete = set()
  218. for name, value in other.items():
  219. if value is None:
  220. to_delete.add(name)
  221. else:
  222. self.set_if_equals(
  223. b"/".join((base, name)), None, value, message=message
  224. )
  225. if to_delete:
  226. try:
  227. to_delete.remove(name)
  228. except KeyError:
  229. pass
  230. for ref in to_delete:
  231. self.remove_if_equals(b"/".join((base, ref)), None, message=message)
  232. def allkeys(self) -> set[Ref]:
  233. """All refs present in this container."""
  234. raise NotImplementedError(self.allkeys)
  235. def __iter__(self) -> Iterator[Ref]:
  236. return iter(self.allkeys())
  237. def keys(self, base=None):
  238. """Refs present in this container.
  239. Args:
  240. base: An optional base to return refs under.
  241. Returns: An unsorted set of valid refs in this container, including
  242. packed refs.
  243. """
  244. if base is not None:
  245. return self.subkeys(base)
  246. else:
  247. return self.allkeys()
  248. def subkeys(self, base: bytes) -> set[bytes]:
  249. """Refs present in this container under a base.
  250. Args:
  251. base: The base to return refs under.
  252. Returns: A set of valid refs in this container under the base; the base
  253. prefix is stripped from the ref names returned.
  254. """
  255. keys = set()
  256. base_len = len(base) + 1
  257. for refname in self.allkeys():
  258. if refname.startswith(base):
  259. keys.add(refname[base_len:])
  260. return keys
  261. def as_dict(self, base: Optional[bytes] = None) -> dict[Ref, ObjectID]:
  262. """Return the contents of this container as a dictionary."""
  263. ret = {}
  264. keys = self.keys(base)
  265. if base is None:
  266. base = b""
  267. else:
  268. base = base.rstrip(b"/")
  269. for key in keys:
  270. try:
  271. ret[key] = self[(base + b"/" + key).strip(b"/")]
  272. except (SymrefLoop, KeyError):
  273. continue # Unable to resolve
  274. return ret
  275. def _check_refname(self, name: bytes) -> None:
  276. """Ensure a refname is valid and lives in refs or is HEAD.
  277. HEAD is not a valid refname according to git-check-ref-format, but this
  278. class needs to be able to touch HEAD. Also, check_ref_format expects
  279. refnames without the leading 'refs/', but this class requires that
  280. so it cannot touch anything outside the refs dir (or HEAD).
  281. Args:
  282. name: The name of the reference.
  283. Raises:
  284. KeyError: if a refname is not HEAD or is otherwise not valid.
  285. """
  286. if name in (HEADREF, b"refs/stash"):
  287. return
  288. if not name.startswith(b"refs/") or not check_ref_format(name[5:]):
  289. raise RefFormatError(name)
  290. def read_ref(self, refname: bytes) -> Optional[bytes]:
  291. """Read a reference without following any references.
  292. Args:
  293. refname: The name of the reference
  294. Returns: The contents of the ref file, or None if it does
  295. not exist.
  296. """
  297. contents = self.read_loose_ref(refname)
  298. if not contents:
  299. contents = self.get_packed_refs().get(refname, None)
  300. return contents
  301. def read_loose_ref(self, name: bytes) -> Optional[bytes]:
  302. """Read a loose reference and return its contents.
  303. Args:
  304. name: the refname to read
  305. Returns: The contents of the ref file, or None if it does
  306. not exist.
  307. """
  308. raise NotImplementedError(self.read_loose_ref)
  309. def follow(self, name: bytes) -> tuple[list[bytes], Optional[bytes]]:
  310. """Follow a reference name.
  311. Returns: a tuple of (refnames, sha), wheres refnames are the names of
  312. references in the chain
  313. """
  314. contents: Optional[bytes] = SYMREF + name
  315. depth = 0
  316. refnames = []
  317. while contents and contents.startswith(SYMREF):
  318. refname = contents[len(SYMREF) :]
  319. refnames.append(refname)
  320. contents = self.read_ref(refname)
  321. if not contents:
  322. break
  323. depth += 1
  324. if depth > 5:
  325. raise SymrefLoop(name, depth)
  326. return refnames, contents
  327. def __contains__(self, refname: bytes) -> bool:
  328. if self.read_ref(refname):
  329. return True
  330. return False
  331. def __getitem__(self, name: bytes) -> ObjectID:
  332. """Get the SHA1 for a reference name.
  333. This method follows all symbolic references.
  334. """
  335. _, sha = self.follow(name)
  336. if sha is None:
  337. raise KeyError(name)
  338. return sha
  339. def set_if_equals(
  340. self,
  341. name: bytes,
  342. old_ref: Optional[bytes],
  343. new_ref: bytes,
  344. committer: Optional[bytes] = None,
  345. timestamp: Optional[int] = None,
  346. timezone: Optional[int] = None,
  347. message: Optional[bytes] = None,
  348. ) -> bool:
  349. """Set a refname to new_ref only if it currently equals old_ref.
  350. This method follows all symbolic references if applicable for the
  351. subclass, and can be used to perform an atomic compare-and-swap
  352. operation.
  353. Args:
  354. name: The refname to set.
  355. old_ref: The old sha the refname must refer to, or None to set
  356. unconditionally.
  357. new_ref: The new sha the refname will refer to.
  358. committer: Optional committer name/email
  359. timestamp: Optional timestamp
  360. timezone: Optional timezone
  361. message: Message for reflog
  362. Returns: True if the set was successful, False otherwise.
  363. """
  364. raise NotImplementedError(self.set_if_equals)
  365. def add_if_new(
  366. self,
  367. name: bytes,
  368. ref: bytes,
  369. committer: Optional[bytes] = None,
  370. timestamp: Optional[int] = None,
  371. timezone: Optional[int] = None,
  372. message: Optional[bytes] = None,
  373. ) -> bool:
  374. """Add a new reference only if it does not already exist.
  375. Args:
  376. name: Ref name
  377. ref: Ref value
  378. committer: Optional committer name/email
  379. timestamp: Optional timestamp
  380. timezone: Optional timezone
  381. message: Optional message for reflog
  382. """
  383. raise NotImplementedError(self.add_if_new)
  384. def __setitem__(self, name: bytes, ref: bytes) -> None:
  385. """Set a reference name to point to the given SHA1.
  386. This method follows all symbolic references if applicable for the
  387. subclass.
  388. Note: This method unconditionally overwrites the contents of a
  389. reference. To update atomically only if the reference has not
  390. changed, use set_if_equals().
  391. Args:
  392. name: The refname to set.
  393. ref: The new sha the refname will refer to.
  394. """
  395. if not (valid_hexsha(ref) or ref.startswith(SYMREF)):
  396. raise ValueError(f"{ref!r} must be a valid sha (40 chars) or a symref")
  397. self.set_if_equals(name, None, ref)
  398. def remove_if_equals(
  399. self,
  400. name: bytes,
  401. old_ref: Optional[bytes],
  402. committer: Optional[bytes] = None,
  403. timestamp: Optional[int] = None,
  404. timezone: Optional[int] = None,
  405. message: Optional[bytes] = None,
  406. ) -> bool:
  407. """Remove a refname only if it currently equals old_ref.
  408. This method does not follow symbolic references, even if applicable for
  409. the subclass. It can be used to perform an atomic compare-and-delete
  410. operation.
  411. Args:
  412. name: The refname to delete.
  413. old_ref: The old sha the refname must refer to, or None to
  414. delete unconditionally.
  415. committer: Optional committer name/email
  416. timestamp: Optional timestamp
  417. timezone: Optional timezone
  418. message: Message for reflog
  419. Returns: True if the delete was successful, False otherwise.
  420. """
  421. raise NotImplementedError(self.remove_if_equals)
  422. def __delitem__(self, name: bytes) -> None:
  423. """Remove a refname.
  424. This method does not follow symbolic references, even if applicable for
  425. the subclass.
  426. Note: This method unconditionally deletes the contents of a reference.
  427. To delete atomically only if the reference has not changed, use
  428. remove_if_equals().
  429. Args:
  430. name: The refname to delete.
  431. """
  432. self.remove_if_equals(name, None)
  433. def get_symrefs(self) -> dict[bytes, bytes]:
  434. """Get a dict with all symrefs in this container.
  435. Returns: Dictionary mapping source ref to target ref
  436. """
  437. ret = {}
  438. for src in self.allkeys():
  439. try:
  440. ref_value = self.read_ref(src)
  441. assert ref_value is not None
  442. dst = parse_symref_value(ref_value)
  443. except ValueError:
  444. pass
  445. else:
  446. ret[src] = dst
  447. return ret
  448. def pack_refs(self, all: bool = False) -> None:
  449. """Pack loose refs into packed-refs file.
  450. Args:
  451. all: If True, pack all refs. If False, only pack tags.
  452. """
  453. raise NotImplementedError(self.pack_refs)
  454. class DictRefsContainer(RefsContainer):
  455. """RefsContainer backed by a simple dict.
  456. This container does not support symbolic or packed references and is not
  457. threadsafe.
  458. """
  459. def __init__(
  460. self,
  461. refs: dict[bytes, bytes],
  462. logger: Optional[
  463. Callable[
  464. [
  465. bytes,
  466. Optional[bytes],
  467. Optional[bytes],
  468. Optional[bytes],
  469. Optional[int],
  470. Optional[int],
  471. Optional[bytes],
  472. ],
  473. None,
  474. ]
  475. ] = None,
  476. ) -> None:
  477. super().__init__(logger=logger)
  478. self._refs = refs
  479. self._peeled: dict[bytes, ObjectID] = {}
  480. self._watchers: set[Any] = set()
  481. def allkeys(self) -> set[bytes]:
  482. return set(self._refs.keys())
  483. def read_loose_ref(self, name: bytes) -> Optional[bytes]:
  484. return self._refs.get(name, None)
  485. def get_packed_refs(self) -> dict[bytes, bytes]:
  486. return {}
  487. def _notify(self, ref: bytes, newsha: Optional[bytes]) -> None:
  488. for watcher in self._watchers:
  489. watcher._notify((ref, newsha))
  490. def set_symbolic_ref(
  491. self,
  492. name: Ref,
  493. other: Ref,
  494. committer: Optional[bytes] = None,
  495. timestamp: Optional[int] = None,
  496. timezone: Optional[int] = None,
  497. message: Optional[bytes] = None,
  498. ) -> None:
  499. """Make a ref point at another ref.
  500. Args:
  501. name: Name of the ref to set
  502. other: Name of the ref to point at
  503. committer: Optional committer name for reflog
  504. timestamp: Optional timestamp for reflog
  505. timezone: Optional timezone for reflog
  506. message: Optional message for reflog
  507. """
  508. old = self.follow(name)[-1]
  509. new = SYMREF + other
  510. self._refs[name] = new
  511. self._notify(name, new)
  512. self._log(
  513. name,
  514. old,
  515. new,
  516. committer=committer,
  517. timestamp=timestamp,
  518. timezone=timezone,
  519. message=message,
  520. )
  521. def set_if_equals(
  522. self,
  523. name: bytes,
  524. old_ref: Optional[bytes],
  525. new_ref: bytes,
  526. committer: Optional[bytes] = None,
  527. timestamp: Optional[int] = None,
  528. timezone: Optional[int] = None,
  529. message: Optional[bytes] = None,
  530. ) -> bool:
  531. """Set a refname to new_ref only if it currently equals old_ref.
  532. This method follows all symbolic references, and can be used to perform
  533. an atomic compare-and-swap operation.
  534. Args:
  535. name: The refname to set.
  536. old_ref: The old sha the refname must refer to, or None to set
  537. unconditionally.
  538. new_ref: The new sha the refname will refer to.
  539. committer: Optional committer name for reflog
  540. timestamp: Optional timestamp for reflog
  541. timezone: Optional timezone for reflog
  542. message: Optional message for reflog
  543. Returns:
  544. True if the set was successful, False otherwise.
  545. """
  546. if old_ref is not None and self._refs.get(name, ZERO_SHA) != old_ref:
  547. return False
  548. # Only update the specific ref requested, not the whole chain
  549. self._check_refname(name)
  550. old = self._refs.get(name)
  551. self._refs[name] = new_ref
  552. self._notify(name, new_ref)
  553. self._log(
  554. name,
  555. old,
  556. new_ref,
  557. committer=committer,
  558. timestamp=timestamp,
  559. timezone=timezone,
  560. message=message,
  561. )
  562. return True
  563. def add_if_new(
  564. self,
  565. name: Ref,
  566. ref: ObjectID,
  567. committer: Optional[bytes] = None,
  568. timestamp: Optional[int] = None,
  569. timezone: Optional[int] = None,
  570. message: Optional[bytes] = None,
  571. ) -> bool:
  572. """Add a new reference only if it does not already exist.
  573. Args:
  574. name: Ref name
  575. ref: Ref value
  576. committer: Optional committer name for reflog
  577. timestamp: Optional timestamp for reflog
  578. timezone: Optional timezone for reflog
  579. message: Optional message for reflog
  580. Returns:
  581. True if the add was successful, False otherwise.
  582. """
  583. if name in self._refs:
  584. return False
  585. self._refs[name] = ref
  586. self._notify(name, ref)
  587. self._log(
  588. name,
  589. None,
  590. ref,
  591. committer=committer,
  592. timestamp=timestamp,
  593. timezone=timezone,
  594. message=message,
  595. )
  596. return True
  597. def remove_if_equals(
  598. self,
  599. name: bytes,
  600. old_ref: Optional[bytes],
  601. committer: Optional[bytes] = None,
  602. timestamp: Optional[int] = None,
  603. timezone: Optional[int] = None,
  604. message: Optional[bytes] = None,
  605. ) -> bool:
  606. """Remove a refname only if it currently equals old_ref.
  607. This method does not follow symbolic references. It can be used to
  608. perform an atomic compare-and-delete operation.
  609. Args:
  610. name: The refname to delete.
  611. old_ref: The old sha the refname must refer to, or None to
  612. delete unconditionally.
  613. committer: Optional committer name for reflog
  614. timestamp: Optional timestamp for reflog
  615. timezone: Optional timezone for reflog
  616. message: Optional message for reflog
  617. Returns:
  618. True if the delete was successful, False otherwise.
  619. """
  620. if old_ref is not None and self._refs.get(name, ZERO_SHA) != old_ref:
  621. return False
  622. try:
  623. old = self._refs.pop(name)
  624. except KeyError:
  625. pass
  626. else:
  627. self._notify(name, None)
  628. self._log(
  629. name,
  630. old,
  631. None,
  632. committer=committer,
  633. timestamp=timestamp,
  634. timezone=timezone,
  635. message=message,
  636. )
  637. return True
  638. def get_peeled(self, name: bytes) -> Optional[bytes]:
  639. return self._peeled.get(name)
  640. def _update(self, refs: dict[bytes, bytes]) -> None:
  641. """Update multiple refs; intended only for testing."""
  642. # TODO(dborowitz): replace this with a public function that uses
  643. # set_if_equal.
  644. for ref, sha in refs.items():
  645. self.set_if_equals(ref, None, sha)
  646. def _update_peeled(self, peeled: dict[bytes, bytes]) -> None:
  647. """Update cached peeled refs; intended only for testing."""
  648. self._peeled.update(peeled)
  649. class InfoRefsContainer(RefsContainer):
  650. """Refs container that reads refs from a info/refs file."""
  651. def __init__(self, f: BinaryIO) -> None:
  652. self._refs: dict[bytes, bytes] = {}
  653. self._peeled: dict[bytes, bytes] = {}
  654. refs = read_info_refs(f)
  655. (self._refs, self._peeled) = split_peeled_refs(refs)
  656. def allkeys(self) -> set[bytes]:
  657. return set(self._refs.keys())
  658. def read_loose_ref(self, name: bytes) -> Optional[bytes]:
  659. return self._refs.get(name, None)
  660. def get_packed_refs(self) -> dict[bytes, bytes]:
  661. return {}
  662. def get_peeled(self, name: bytes) -> Optional[bytes]:
  663. try:
  664. return self._peeled[name]
  665. except KeyError:
  666. return self._refs[name]
  667. class DiskRefsContainer(RefsContainer):
  668. """Refs container that reads refs from disk."""
  669. def __init__(
  670. self,
  671. path: Union[str, bytes, os.PathLike],
  672. worktree_path: Optional[Union[str, bytes, os.PathLike]] = None,
  673. logger: Optional[
  674. Callable[
  675. [
  676. bytes,
  677. Optional[bytes],
  678. Optional[bytes],
  679. Optional[bytes],
  680. Optional[int],
  681. Optional[int],
  682. Optional[bytes],
  683. ],
  684. None,
  685. ]
  686. ] = None,
  687. ) -> None:
  688. """Initialize DiskRefsContainer."""
  689. super().__init__(logger=logger)
  690. # Convert path-like objects to strings, then to bytes for Git compatibility
  691. self.path = os.fsencode(os.fspath(path))
  692. if worktree_path is None:
  693. self.worktree_path = self.path
  694. else:
  695. self.worktree_path = os.fsencode(os.fspath(worktree_path))
  696. self._packed_refs: Optional[dict[bytes, bytes]] = None
  697. self._peeled_refs: Optional[dict[bytes, bytes]] = None
  698. def __repr__(self) -> str:
  699. """Return string representation of DiskRefsContainer."""
  700. return f"{self.__class__.__name__}({self.path!r})"
  701. def subkeys(self, base: bytes) -> set[bytes]:
  702. subkeys = set()
  703. path = self.refpath(base)
  704. for root, unused_dirs, files in os.walk(path):
  705. directory = root[len(path) :]
  706. if os.path.sep != "/":
  707. directory = directory.replace(os.fsencode(os.path.sep), b"/")
  708. directory = directory.strip(b"/")
  709. for filename in files:
  710. refname = b"/".join(([directory] if directory else []) + [filename])
  711. # check_ref_format requires at least one /, so we prepend the
  712. # base before calling it.
  713. if check_ref_format(base + b"/" + refname):
  714. subkeys.add(refname)
  715. for key in self.get_packed_refs():
  716. if key.startswith(base):
  717. subkeys.add(key[len(base) :].strip(b"/"))
  718. return subkeys
  719. def allkeys(self) -> set[bytes]:
  720. allkeys = set()
  721. if os.path.exists(self.refpath(HEADREF)):
  722. allkeys.add(HEADREF)
  723. path = self.refpath(b"")
  724. refspath = self.refpath(b"refs")
  725. for root, unused_dirs, files in os.walk(refspath):
  726. directory = root[len(path) :]
  727. if os.path.sep != "/":
  728. directory = directory.replace(os.fsencode(os.path.sep), b"/")
  729. for filename in files:
  730. refname = b"/".join([directory, filename])
  731. if check_ref_format(refname):
  732. allkeys.add(refname)
  733. allkeys.update(self.get_packed_refs())
  734. return allkeys
  735. def refpath(self, name: bytes) -> bytes:
  736. """Return the disk path of a ref."""
  737. if os.path.sep != "/":
  738. name = name.replace(b"/", os.fsencode(os.path.sep))
  739. # TODO: as the 'HEAD' reference is working tree specific, it
  740. # should actually not be a part of RefsContainer
  741. if name == HEADREF:
  742. return os.path.join(self.worktree_path, name)
  743. else:
  744. return os.path.join(self.path, name)
  745. def get_packed_refs(self) -> dict[bytes, bytes]:
  746. """Get contents of the packed-refs file.
  747. Returns: Dictionary mapping ref names to SHA1s
  748. Note: Will return an empty dictionary when no packed-refs file is
  749. present.
  750. """
  751. # TODO: invalidate the cache on repacking
  752. if self._packed_refs is None:
  753. # set both to empty because we want _peeled_refs to be
  754. # None if and only if _packed_refs is also None.
  755. self._packed_refs = {}
  756. self._peeled_refs = {}
  757. path = os.path.join(self.path, b"packed-refs")
  758. try:
  759. f = GitFile(path, "rb")
  760. except FileNotFoundError:
  761. return {}
  762. with f:
  763. first_line = next(iter(f)).rstrip()
  764. if first_line.startswith(b"# pack-refs") and b" peeled" in first_line:
  765. for sha, name, peeled in read_packed_refs_with_peeled(f):
  766. self._packed_refs[name] = sha
  767. if peeled:
  768. self._peeled_refs[name] = peeled
  769. else:
  770. f.seek(0)
  771. for sha, name in read_packed_refs(f):
  772. self._packed_refs[name] = sha
  773. return self._packed_refs
  774. def add_packed_refs(self, new_refs: dict[Ref, Optional[ObjectID]]) -> None:
  775. """Add the given refs as packed refs.
  776. Args:
  777. new_refs: A mapping of ref names to targets; if a target is None that
  778. means remove the ref
  779. """
  780. if not new_refs:
  781. return
  782. path = os.path.join(self.path, b"packed-refs")
  783. with GitFile(path, "wb") as f:
  784. # reread cached refs from disk, while holding the lock
  785. packed_refs = self.get_packed_refs().copy()
  786. for ref, target in new_refs.items():
  787. # sanity check
  788. if ref == HEADREF:
  789. raise ValueError("cannot pack HEAD")
  790. # remove any loose refs pointing to this one -- please
  791. # note that this bypasses remove_if_equals as we don't
  792. # want to affect packed refs in here
  793. with suppress(OSError):
  794. os.remove(self.refpath(ref))
  795. if target is not None:
  796. packed_refs[ref] = target
  797. else:
  798. packed_refs.pop(ref, None)
  799. write_packed_refs(f, packed_refs, self._peeled_refs)
  800. self._packed_refs = packed_refs
  801. def get_peeled(self, name: bytes) -> Optional[bytes]:
  802. """Return the cached peeled value of a ref, if available.
  803. Args:
  804. name: Name of the ref to peel
  805. Returns: The peeled value of the ref. If the ref is known not point to
  806. a tag, this will be the SHA the ref refers to. If the ref may point
  807. to a tag, but no cached information is available, None is returned.
  808. """
  809. self.get_packed_refs()
  810. if (
  811. self._peeled_refs is None
  812. or self._packed_refs is None
  813. or name not in self._packed_refs
  814. ):
  815. # No cache: no peeled refs were read, or this ref is loose
  816. return None
  817. if name in self._peeled_refs:
  818. return self._peeled_refs[name]
  819. else:
  820. # Known not peelable
  821. return self[name]
  822. def read_loose_ref(self, name: bytes) -> Optional[bytes]:
  823. """Read a reference file and return its contents.
  824. If the reference file a symbolic reference, only read the first line of
  825. the file. Otherwise, only read the first 40 bytes.
  826. Args:
  827. name: the refname to read, relative to refpath
  828. Returns: The contents of the ref file, or None if the file does not
  829. exist.
  830. Raises:
  831. IOError: if any other error occurs
  832. """
  833. filename = self.refpath(name)
  834. try:
  835. with GitFile(filename, "rb") as f:
  836. header = f.read(len(SYMREF))
  837. if header == SYMREF:
  838. # Read only the first line
  839. return header + next(iter(f)).rstrip(b"\r\n")
  840. else:
  841. # Read only the first 40 bytes
  842. return header + f.read(40 - len(SYMREF))
  843. except (OSError, UnicodeError):
  844. # don't assume anything specific about the error; in
  845. # particular, invalid or forbidden paths can raise weird
  846. # errors depending on the specific operating system
  847. return None
  848. def _remove_packed_ref(self, name: bytes) -> None:
  849. if self._packed_refs is None:
  850. return
  851. filename = os.path.join(self.path, b"packed-refs")
  852. # reread cached refs from disk, while holding the lock
  853. f = GitFile(filename, "wb")
  854. try:
  855. self._packed_refs = None
  856. self.get_packed_refs()
  857. if self._packed_refs is None or name not in self._packed_refs:
  858. f.abort()
  859. return
  860. del self._packed_refs[name]
  861. if self._peeled_refs is not None:
  862. with suppress(KeyError):
  863. del self._peeled_refs[name]
  864. write_packed_refs(f, self._packed_refs, self._peeled_refs)
  865. f.close()
  866. except BaseException:
  867. f.abort()
  868. raise
  869. def set_symbolic_ref(
  870. self,
  871. name: bytes,
  872. other: bytes,
  873. committer: Optional[bytes] = None,
  874. timestamp: Optional[int] = None,
  875. timezone: Optional[int] = None,
  876. message: Optional[bytes] = None,
  877. ) -> None:
  878. """Make a ref point at another ref.
  879. Args:
  880. name: Name of the ref to set
  881. other: Name of the ref to point at
  882. committer: Optional committer name
  883. timestamp: Optional timestamp
  884. timezone: Optional timezone
  885. message: Optional message to describe the change
  886. """
  887. self._check_refname(name)
  888. self._check_refname(other)
  889. filename = self.refpath(name)
  890. f = GitFile(filename, "wb")
  891. try:
  892. f.write(SYMREF + other + b"\n")
  893. sha = self.follow(name)[-1]
  894. self._log(
  895. name,
  896. sha,
  897. sha,
  898. committer=committer,
  899. timestamp=timestamp,
  900. timezone=timezone,
  901. message=message,
  902. )
  903. except BaseException:
  904. f.abort()
  905. raise
  906. else:
  907. f.close()
  908. def set_if_equals(
  909. self,
  910. name: bytes,
  911. old_ref: Optional[bytes],
  912. new_ref: bytes,
  913. committer: Optional[bytes] = None,
  914. timestamp: Optional[int] = None,
  915. timezone: Optional[int] = None,
  916. message: Optional[bytes] = None,
  917. ) -> bool:
  918. """Set a refname to new_ref only if it currently equals old_ref.
  919. This method follows all symbolic references, and can be used to perform
  920. an atomic compare-and-swap operation.
  921. Args:
  922. name: The refname to set.
  923. old_ref: The old sha the refname must refer to, or None to set
  924. unconditionally.
  925. new_ref: The new sha the refname will refer to.
  926. committer: Optional committer name
  927. timestamp: Optional timestamp
  928. timezone: Optional timezone
  929. message: Set message for reflog
  930. Returns: True if the set was successful, False otherwise.
  931. """
  932. self._check_refname(name)
  933. try:
  934. realnames, _ = self.follow(name)
  935. realname = realnames[-1]
  936. except (KeyError, IndexError, SymrefLoop):
  937. realname = name
  938. filename = self.refpath(realname)
  939. # make sure none of the ancestor folders is in packed refs
  940. probe_ref = os.path.dirname(realname)
  941. packed_refs = self.get_packed_refs()
  942. while probe_ref:
  943. if packed_refs.get(probe_ref, None) is not None:
  944. raise NotADirectoryError(filename)
  945. probe_ref = os.path.dirname(probe_ref)
  946. ensure_dir_exists(os.path.dirname(filename))
  947. with GitFile(filename, "wb") as f:
  948. if old_ref is not None:
  949. try:
  950. # read again while holding the lock to handle race conditions
  951. orig_ref = self.read_loose_ref(realname)
  952. if orig_ref is None:
  953. orig_ref = self.get_packed_refs().get(realname, ZERO_SHA)
  954. if orig_ref != old_ref:
  955. f.abort()
  956. return False
  957. except OSError:
  958. f.abort()
  959. raise
  960. # Check if ref already has the desired value while holding the lock
  961. # This avoids fsync when ref is unchanged but still detects lock conflicts
  962. current_ref = self.read_loose_ref(realname)
  963. if current_ref is None:
  964. current_ref = packed_refs.get(realname, None)
  965. if current_ref is not None and current_ref == new_ref:
  966. # Ref already has desired value, abort write to avoid fsync
  967. f.abort()
  968. return True
  969. try:
  970. f.write(new_ref + b"\n")
  971. except OSError:
  972. f.abort()
  973. raise
  974. self._log(
  975. realname,
  976. old_ref,
  977. new_ref,
  978. committer=committer,
  979. timestamp=timestamp,
  980. timezone=timezone,
  981. message=message,
  982. )
  983. return True
  984. def add_if_new(
  985. self,
  986. name: bytes,
  987. ref: bytes,
  988. committer: Optional[bytes] = None,
  989. timestamp: Optional[int] = None,
  990. timezone: Optional[int] = None,
  991. message: Optional[bytes] = None,
  992. ) -> bool:
  993. """Add a new reference only if it does not already exist.
  994. This method follows symrefs, and only ensures that the last ref in the
  995. chain does not exist.
  996. Args:
  997. name: The refname to set.
  998. ref: The new sha the refname will refer to.
  999. committer: Optional committer name
  1000. timestamp: Optional timestamp
  1001. timezone: Optional timezone
  1002. message: Optional message for reflog
  1003. Returns: True if the add was successful, False otherwise.
  1004. """
  1005. try:
  1006. realnames, contents = self.follow(name)
  1007. if contents is not None:
  1008. return False
  1009. realname = realnames[-1]
  1010. except (KeyError, IndexError):
  1011. realname = name
  1012. self._check_refname(realname)
  1013. filename = self.refpath(realname)
  1014. ensure_dir_exists(os.path.dirname(filename))
  1015. with GitFile(filename, "wb") as f:
  1016. if os.path.exists(filename) or name in self.get_packed_refs():
  1017. f.abort()
  1018. return False
  1019. try:
  1020. f.write(ref + b"\n")
  1021. except OSError:
  1022. f.abort()
  1023. raise
  1024. else:
  1025. self._log(
  1026. name,
  1027. None,
  1028. ref,
  1029. committer=committer,
  1030. timestamp=timestamp,
  1031. timezone=timezone,
  1032. message=message,
  1033. )
  1034. return True
  1035. def remove_if_equals(
  1036. self,
  1037. name: bytes,
  1038. old_ref: Optional[bytes],
  1039. committer: Optional[bytes] = None,
  1040. timestamp: Optional[int] = None,
  1041. timezone: Optional[int] = None,
  1042. message: Optional[bytes] = None,
  1043. ) -> bool:
  1044. """Remove a refname only if it currently equals old_ref.
  1045. This method does not follow symbolic references. It can be used to
  1046. perform an atomic compare-and-delete operation.
  1047. Args:
  1048. name: The refname to delete.
  1049. old_ref: The old sha the refname must refer to, or None to
  1050. delete unconditionally.
  1051. committer: Optional committer name
  1052. timestamp: Optional timestamp
  1053. timezone: Optional timezone
  1054. message: Optional message
  1055. Returns: True if the delete was successful, False otherwise.
  1056. """
  1057. self._check_refname(name)
  1058. filename = self.refpath(name)
  1059. ensure_dir_exists(os.path.dirname(filename))
  1060. f = GitFile(filename, "wb")
  1061. try:
  1062. if old_ref is not None:
  1063. orig_ref = self.read_loose_ref(name)
  1064. if orig_ref is None:
  1065. orig_ref = self.get_packed_refs().get(name, ZERO_SHA)
  1066. if orig_ref != old_ref:
  1067. return False
  1068. # remove the reference file itself
  1069. try:
  1070. found = os.path.lexists(filename)
  1071. except OSError:
  1072. # may only be packed, or otherwise unstorable
  1073. found = False
  1074. if found:
  1075. os.remove(filename)
  1076. self._remove_packed_ref(name)
  1077. self._log(
  1078. name,
  1079. old_ref,
  1080. None,
  1081. committer=committer,
  1082. timestamp=timestamp,
  1083. timezone=timezone,
  1084. message=message,
  1085. )
  1086. finally:
  1087. # never write, we just wanted the lock
  1088. f.abort()
  1089. # outside of the lock, clean-up any parent directory that might now
  1090. # be empty. this ensures that re-creating a reference of the same
  1091. # name of what was previously a directory works as expected
  1092. parent = name
  1093. while True:
  1094. try:
  1095. parent, _ = parent.rsplit(b"/", 1)
  1096. except ValueError:
  1097. break
  1098. if parent == b"refs":
  1099. break
  1100. parent_filename = self.refpath(parent)
  1101. try:
  1102. os.rmdir(parent_filename)
  1103. except OSError:
  1104. # this can be caused by the parent directory being
  1105. # removed by another process, being not empty, etc.
  1106. # in any case, this is non fatal because we already
  1107. # removed the reference, just ignore it
  1108. break
  1109. return True
  1110. def pack_refs(self, all: bool = False) -> None:
  1111. """Pack loose refs into packed-refs file.
  1112. Args:
  1113. all: If True, pack all refs. If False, only pack tags.
  1114. """
  1115. refs_to_pack: dict[Ref, Optional[ObjectID]] = {}
  1116. for ref in self.allkeys():
  1117. if ref == HEADREF:
  1118. # Never pack HEAD
  1119. continue
  1120. if all or ref.startswith(LOCAL_TAG_PREFIX):
  1121. try:
  1122. sha = self[ref]
  1123. if sha:
  1124. refs_to_pack[ref] = sha
  1125. except KeyError:
  1126. # Broken ref, skip it
  1127. pass
  1128. if refs_to_pack:
  1129. self.add_packed_refs(refs_to_pack)
  1130. def _split_ref_line(line: bytes) -> tuple[bytes, bytes]:
  1131. """Split a single ref line into a tuple of SHA1 and name."""
  1132. fields = line.rstrip(b"\n\r").split(b" ")
  1133. if len(fields) != 2:
  1134. raise PackedRefsException(f"invalid ref line {line!r}")
  1135. sha, name = fields
  1136. if not valid_hexsha(sha):
  1137. raise PackedRefsException(f"Invalid hex sha {sha!r}")
  1138. if not check_ref_format(name):
  1139. raise PackedRefsException(f"invalid ref name {name!r}")
  1140. return (sha, name)
  1141. def read_packed_refs(f: IO[bytes]) -> Iterator[tuple[bytes, bytes]]:
  1142. """Read a packed refs file.
  1143. Args:
  1144. f: file-like object to read from
  1145. Returns: Iterator over tuples with SHA1s and ref names.
  1146. """
  1147. for line in f:
  1148. if line.startswith(b"#"):
  1149. # Comment
  1150. continue
  1151. if line.startswith(b"^"):
  1152. raise PackedRefsException("found peeled ref in packed-refs without peeled")
  1153. yield _split_ref_line(line)
  1154. def read_packed_refs_with_peeled(
  1155. f: IO[bytes],
  1156. ) -> Iterator[tuple[bytes, bytes, Optional[bytes]]]:
  1157. """Read a packed refs file including peeled refs.
  1158. Assumes the "# pack-refs with: peeled" line was already read. Yields tuples
  1159. with ref names, SHA1s, and peeled SHA1s (or None).
  1160. Args:
  1161. f: file-like object to read from, seek'ed to the second line
  1162. """
  1163. last = None
  1164. for line in f:
  1165. if line[0] == b"#":
  1166. continue
  1167. line = line.rstrip(b"\r\n")
  1168. if line.startswith(b"^"):
  1169. if not last:
  1170. raise PackedRefsException("unexpected peeled ref line")
  1171. if not valid_hexsha(line[1:]):
  1172. raise PackedRefsException(f"Invalid hex sha {line[1:]!r}")
  1173. sha, name = _split_ref_line(last)
  1174. last = None
  1175. yield (sha, name, line[1:])
  1176. else:
  1177. if last:
  1178. sha, name = _split_ref_line(last)
  1179. yield (sha, name, None)
  1180. last = line
  1181. if last:
  1182. sha, name = _split_ref_line(last)
  1183. yield (sha, name, None)
  1184. def write_packed_refs(
  1185. f: IO[bytes],
  1186. packed_refs: dict[bytes, bytes],
  1187. peeled_refs: Optional[dict[bytes, bytes]] = None,
  1188. ) -> None:
  1189. """Write a packed refs file.
  1190. Args:
  1191. f: empty file-like object to write to
  1192. packed_refs: dict of refname to sha of packed refs to write
  1193. peeled_refs: dict of refname to peeled value of sha
  1194. """
  1195. if peeled_refs is None:
  1196. peeled_refs = {}
  1197. else:
  1198. f.write(b"# pack-refs with: peeled\n")
  1199. for refname in sorted(packed_refs.keys()):
  1200. f.write(git_line(packed_refs[refname], refname))
  1201. if refname in peeled_refs:
  1202. f.write(b"^" + peeled_refs[refname] + b"\n")
  1203. def read_info_refs(f: BinaryIO) -> dict[bytes, bytes]:
  1204. """Read info/refs file.
  1205. Args:
  1206. f: File-like object to read from
  1207. Returns:
  1208. Dictionary mapping ref names to SHA1s
  1209. """
  1210. ret = {}
  1211. for line in f.readlines():
  1212. (sha, name) = line.rstrip(b"\r\n").split(b"\t", 1)
  1213. ret[name] = sha
  1214. return ret
  1215. def write_info_refs(
  1216. refs: dict[bytes, bytes], store: ObjectContainer
  1217. ) -> Iterator[bytes]:
  1218. """Generate info refs."""
  1219. # TODO: Avoid recursive import :(
  1220. from .object_store import peel_sha
  1221. for name, sha in sorted(refs.items()):
  1222. # get_refs() includes HEAD as a special case, but we don't want to
  1223. # advertise it
  1224. if name == HEADREF:
  1225. continue
  1226. try:
  1227. o = store[sha]
  1228. except KeyError:
  1229. continue
  1230. unpeeled, peeled = peel_sha(store, sha)
  1231. yield o.id + b"\t" + name + b"\n"
  1232. if o.id != peeled.id:
  1233. yield peeled.id + b"\t" + name + PEELED_TAG_SUFFIX + b"\n"
  1234. def is_local_branch(x: bytes) -> bool:
  1235. return x.startswith(LOCAL_BRANCH_PREFIX)
  1236. T = TypeVar("T", dict[bytes, bytes], dict[bytes, Optional[bytes]])
  1237. def strip_peeled_refs(refs: T) -> T:
  1238. """Remove all peeled refs."""
  1239. return {
  1240. ref: sha for (ref, sha) in refs.items() if not ref.endswith(PEELED_TAG_SUFFIX)
  1241. }
  1242. def split_peeled_refs(refs: T) -> tuple[T, dict[bytes, bytes]]:
  1243. """Split peeled refs from regular refs."""
  1244. peeled: dict[bytes, bytes] = {}
  1245. regular = {k: v for k, v in refs.items() if not k.endswith(PEELED_TAG_SUFFIX)}
  1246. for ref, sha in refs.items():
  1247. if ref.endswith(PEELED_TAG_SUFFIX):
  1248. # Only add to peeled dict if sha is not None
  1249. if sha is not None:
  1250. peeled[ref[: -len(PEELED_TAG_SUFFIX)]] = sha
  1251. return regular, peeled
  1252. def _set_origin_head(
  1253. refs: RefsContainer, origin: bytes, origin_head: Optional[bytes]
  1254. ) -> None:
  1255. # set refs/remotes/origin/HEAD
  1256. origin_base = b"refs/remotes/" + origin + b"/"
  1257. if origin_head and origin_head.startswith(LOCAL_BRANCH_PREFIX):
  1258. origin_ref = origin_base + HEADREF
  1259. target_ref = origin_base + origin_head[len(LOCAL_BRANCH_PREFIX) :]
  1260. if target_ref in refs:
  1261. refs.set_symbolic_ref(origin_ref, target_ref)
  1262. def _set_default_branch(
  1263. refs: RefsContainer,
  1264. origin: bytes,
  1265. origin_head: Optional[bytes],
  1266. branch: bytes,
  1267. ref_message: Optional[bytes],
  1268. ) -> bytes:
  1269. """Set the default branch."""
  1270. origin_base = b"refs/remotes/" + origin + b"/"
  1271. if branch:
  1272. origin_ref = origin_base + branch
  1273. if origin_ref in refs:
  1274. local_ref = LOCAL_BRANCH_PREFIX + branch
  1275. refs.add_if_new(local_ref, refs[origin_ref], ref_message)
  1276. head_ref = local_ref
  1277. elif LOCAL_TAG_PREFIX + branch in refs:
  1278. head_ref = LOCAL_TAG_PREFIX + branch
  1279. else:
  1280. raise ValueError(f"{os.fsencode(branch)!r} is not a valid branch or tag")
  1281. elif origin_head:
  1282. head_ref = origin_head
  1283. if origin_head.startswith(LOCAL_BRANCH_PREFIX):
  1284. origin_ref = origin_base + origin_head[len(LOCAL_BRANCH_PREFIX) :]
  1285. else:
  1286. origin_ref = origin_head
  1287. try:
  1288. refs.add_if_new(head_ref, refs[origin_ref], ref_message)
  1289. except KeyError:
  1290. pass
  1291. else:
  1292. raise ValueError("neither origin_head nor branch are provided")
  1293. return head_ref
  1294. def _set_head(
  1295. refs: RefsContainer, head_ref: bytes, ref_message: Optional[bytes]
  1296. ) -> Optional[bytes]:
  1297. if head_ref.startswith(LOCAL_TAG_PREFIX):
  1298. # detach HEAD at specified tag
  1299. head = refs[head_ref]
  1300. if isinstance(head, Tag):
  1301. _cls, obj = head.object
  1302. head = obj.get_object(obj).id
  1303. del refs[HEADREF]
  1304. refs.set_if_equals(HEADREF, None, head, message=ref_message)
  1305. else:
  1306. # set HEAD to specific branch
  1307. try:
  1308. head = refs[head_ref]
  1309. refs.set_symbolic_ref(HEADREF, head_ref)
  1310. refs.set_if_equals(HEADREF, None, head, message=ref_message)
  1311. except KeyError:
  1312. head = None
  1313. return head
  1314. def _import_remote_refs(
  1315. refs_container: RefsContainer,
  1316. remote_name: str,
  1317. refs: dict[bytes, Optional[bytes]],
  1318. message: Optional[bytes] = None,
  1319. prune: bool = False,
  1320. prune_tags: bool = False,
  1321. ) -> None:
  1322. stripped_refs = strip_peeled_refs(refs)
  1323. branches = {
  1324. n[len(LOCAL_BRANCH_PREFIX) :]: v
  1325. for (n, v) in stripped_refs.items()
  1326. if n.startswith(LOCAL_BRANCH_PREFIX) and v is not None
  1327. }
  1328. refs_container.import_refs(
  1329. b"refs/remotes/" + remote_name.encode(),
  1330. branches,
  1331. message=message,
  1332. prune=prune,
  1333. )
  1334. tags = {
  1335. n[len(LOCAL_TAG_PREFIX) :]: v
  1336. for (n, v) in stripped_refs.items()
  1337. if n.startswith(LOCAL_TAG_PREFIX)
  1338. and not n.endswith(PEELED_TAG_SUFFIX)
  1339. and v is not None
  1340. }
  1341. refs_container.import_refs(
  1342. LOCAL_TAG_PREFIX, tags, message=message, prune=prune_tags
  1343. )
  1344. def serialize_refs(
  1345. store: ObjectContainer, refs: dict[bytes, bytes]
  1346. ) -> dict[bytes, bytes]:
  1347. """Serialize refs with peeled refs.
  1348. Args:
  1349. store: Object store to peel refs from
  1350. refs: Dictionary of ref names to SHAs
  1351. Returns:
  1352. Dictionary with refs and peeled refs (marked with ^{})
  1353. """
  1354. # TODO: Avoid recursive import :(
  1355. from .object_store import peel_sha
  1356. ret = {}
  1357. for ref, sha in refs.items():
  1358. try:
  1359. unpeeled, peeled = peel_sha(store, sha)
  1360. except KeyError:
  1361. warnings.warn(
  1362. "ref {} points at non-present sha {}".format(
  1363. ref.decode("utf-8", "replace"), sha.decode("ascii")
  1364. ),
  1365. UserWarning,
  1366. )
  1367. continue
  1368. else:
  1369. if isinstance(unpeeled, Tag):
  1370. ret[ref + PEELED_TAG_SUFFIX] = peeled.id
  1371. ret[ref] = unpeeled.id
  1372. return ret
  1373. class locked_ref:
  1374. """Lock a ref while making modifications.
  1375. Works as a context manager.
  1376. """
  1377. def __init__(self, refs_container: DiskRefsContainer, refname: Ref) -> None:
  1378. """Initialize a locked ref.
  1379. Args:
  1380. refs_container: The DiskRefsContainer to lock the ref in
  1381. refname: The ref name to lock
  1382. """
  1383. self._refs_container = refs_container
  1384. self._refname = refname
  1385. self._file: Optional[_GitFile] = None
  1386. self._realname: Optional[Ref] = None
  1387. self._deleted = False
  1388. def __enter__(self) -> "locked_ref":
  1389. """Enter the context manager and acquire the lock.
  1390. Returns:
  1391. This locked_ref instance
  1392. Raises:
  1393. OSError: If the lock cannot be acquired
  1394. """
  1395. self._refs_container._check_refname(self._refname)
  1396. try:
  1397. realnames, _ = self._refs_container.follow(self._refname)
  1398. self._realname = realnames[-1]
  1399. except (KeyError, IndexError, SymrefLoop):
  1400. self._realname = self._refname
  1401. filename = self._refs_container.refpath(self._realname)
  1402. ensure_dir_exists(os.path.dirname(filename))
  1403. f = GitFile(filename, "wb")
  1404. self._file = f
  1405. return self
  1406. def __exit__(
  1407. self,
  1408. exc_type: Optional[type],
  1409. exc_value: Optional[BaseException],
  1410. traceback: Optional[types.TracebackType],
  1411. ) -> None:
  1412. """Exit the context manager and release the lock.
  1413. Args:
  1414. exc_type: Type of exception if one occurred
  1415. exc_value: Exception instance if one occurred
  1416. traceback: Traceback if an exception occurred
  1417. """
  1418. if self._file:
  1419. if exc_type is not None or self._deleted:
  1420. self._file.abort()
  1421. else:
  1422. self._file.close()
  1423. def get(self) -> Optional[bytes]:
  1424. """Get the current value of the ref."""
  1425. if not self._file:
  1426. raise RuntimeError("locked_ref not in context")
  1427. assert self._realname is not None
  1428. current_ref = self._refs_container.read_loose_ref(self._realname)
  1429. if current_ref is None:
  1430. current_ref = self._refs_container.get_packed_refs().get(
  1431. self._realname, None
  1432. )
  1433. return current_ref
  1434. def ensure_equals(self, expected_value: Optional[bytes]) -> bool:
  1435. """Ensure the ref currently equals the expected value.
  1436. Args:
  1437. expected_value: The expected current value of the ref
  1438. Returns:
  1439. True if the ref equals the expected value, False otherwise
  1440. """
  1441. current_value = self.get()
  1442. return current_value == expected_value
  1443. def set(self, new_ref: bytes) -> None:
  1444. """Set the ref to a new value.
  1445. Args:
  1446. new_ref: The new SHA1 or symbolic ref value
  1447. """
  1448. if not self._file:
  1449. raise RuntimeError("locked_ref not in context")
  1450. if not (valid_hexsha(new_ref) or new_ref.startswith(SYMREF)):
  1451. raise ValueError(f"{new_ref!r} must be a valid sha (40 chars) or a symref")
  1452. self._file.seek(0)
  1453. self._file.truncate()
  1454. self._file.write(new_ref + b"\n")
  1455. self._deleted = False
  1456. def set_symbolic_ref(self, target: Ref) -> None:
  1457. """Make this ref point at another ref.
  1458. Args:
  1459. target: Name of the ref to point at
  1460. """
  1461. if not self._file:
  1462. raise RuntimeError("locked_ref not in context")
  1463. self._refs_container._check_refname(target)
  1464. self._file.seek(0)
  1465. self._file.truncate()
  1466. self._file.write(SYMREF + target + b"\n")
  1467. self._deleted = False
  1468. def delete(self) -> None:
  1469. """Delete the ref file while holding the lock."""
  1470. if not self._file:
  1471. raise RuntimeError("locked_ref not in context")
  1472. # Delete the actual ref file while holding the lock
  1473. if self._realname:
  1474. filename = self._refs_container.refpath(self._realname)
  1475. try:
  1476. if os.path.lexists(filename):
  1477. os.remove(filename)
  1478. except FileNotFoundError:
  1479. pass
  1480. self._refs_container._remove_packed_ref(self._realname)
  1481. self._deleted = True
  1482. def filter_ref_prefix(refs: T, prefixes: Iterable[bytes]) -> T:
  1483. """Filter refs to only include those with a given prefix.
  1484. Args:
  1485. refs: A dictionary of refs.
  1486. prefixes: The prefixes to filter by.
  1487. """
  1488. filtered = {k: v for k, v in refs.items() if any(k.startswith(p) for p in prefixes)}
  1489. return cast(T, filtered)