refs.py 39 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280
  1. # refs.py -- For dealing with git refs
  2. # Copyright (C) 2008-2013 Jelmer Vernooij <jelmer@jelmer.uk>
  3. #
  4. # Dulwich is dual-licensed under the Apache License, Version 2.0 and the GNU
  5. # General Public License as public by the Free Software Foundation; version 2.0
  6. # or (at your option) any later version. You can redistribute it and/or
  7. # modify it under the terms of either of these two licenses.
  8. #
  9. # Unless required by applicable law or agreed to in writing, software
  10. # distributed under the License is distributed on an "AS IS" BASIS,
  11. # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  12. # See the License for the specific language governing permissions and
  13. # limitations under the License.
  14. #
  15. # You should have received a copy of the licenses; if not, see
  16. # <http://www.gnu.org/licenses/> for a copy of the GNU General Public License
  17. # and <http://www.apache.org/licenses/LICENSE-2.0> for a copy of the Apache
  18. # License, Version 2.0.
  19. #
  20. """Ref handling.
  21. """
  22. from contextlib import suppress
  23. import os
  24. from typing import Dict, Optional
  25. from dulwich.errors import (
  26. PackedRefsException,
  27. RefFormatError,
  28. )
  29. from dulwich.objects import (
  30. git_line,
  31. valid_hexsha,
  32. ZERO_SHA,
  33. Tag,
  34. ObjectID,
  35. )
  36. from dulwich.pack import ObjectContainer
  37. from dulwich.file import (
  38. GitFile,
  39. ensure_dir_exists,
  40. )
  41. Ref = bytes
  42. HEADREF = b"HEAD"
  43. SYMREF = b"ref: "
  44. LOCAL_BRANCH_PREFIX = b"refs/heads/"
  45. LOCAL_TAG_PREFIX = b"refs/tags/"
  46. BAD_REF_CHARS = set(b"\177 ~^:?*[")
  47. ANNOTATED_TAG_SUFFIX = b"^{}"
  48. class SymrefLoop(Exception):
  49. """There is a loop between one or more symrefs."""
  50. def __init__(self, ref, depth):
  51. self.ref = ref
  52. self.depth = depth
  53. def parse_symref_value(contents):
  54. """Parse a symref value.
  55. Args:
  56. contents: Contents to parse
  57. Returns: Destination
  58. """
  59. if contents.startswith(SYMREF):
  60. return contents[len(SYMREF) :].rstrip(b"\r\n")
  61. raise ValueError(contents)
  62. def check_ref_format(refname: Ref):
  63. """Check if a refname is correctly formatted.
  64. Implements all the same rules as git-check-ref-format[1].
  65. [1]
  66. http://www.kernel.org/pub/software/scm/git/docs/git-check-ref-format.html
  67. Args:
  68. refname: The refname to check
  69. Returns: True if refname is valid, False otherwise
  70. """
  71. # These could be combined into one big expression, but are listed
  72. # separately to parallel [1].
  73. if b"/." in refname or refname.startswith(b"."):
  74. return False
  75. if b"/" not in refname:
  76. return False
  77. if b".." in refname:
  78. return False
  79. for i, c in enumerate(refname):
  80. if ord(refname[i : i + 1]) < 0o40 or c in BAD_REF_CHARS:
  81. return False
  82. if refname[-1] in b"/.":
  83. return False
  84. if refname.endswith(b".lock"):
  85. return False
  86. if b"@{" in refname:
  87. return False
  88. if b"\\" in refname:
  89. return False
  90. return True
  91. class RefsContainer:
  92. """A container for refs."""
  93. def __init__(self, logger=None):
  94. self._logger = logger
  95. def _log(
  96. self,
  97. ref,
  98. old_sha,
  99. new_sha,
  100. committer=None,
  101. timestamp=None,
  102. timezone=None,
  103. message=None,
  104. ):
  105. if self._logger is None:
  106. return
  107. if message is None:
  108. return
  109. self._logger(ref, old_sha, new_sha, committer, timestamp, timezone, message)
  110. def set_symbolic_ref(
  111. self,
  112. name,
  113. other,
  114. committer=None,
  115. timestamp=None,
  116. timezone=None,
  117. message=None,
  118. ):
  119. """Make a ref point at another ref.
  120. Args:
  121. name: Name of the ref to set
  122. other: Name of the ref to point at
  123. message: Optional message
  124. """
  125. raise NotImplementedError(self.set_symbolic_ref)
  126. def get_packed_refs(self):
  127. """Get contents of the packed-refs file.
  128. Returns: Dictionary mapping ref names to SHA1s
  129. Note: Will return an empty dictionary when no packed-refs file is
  130. present.
  131. """
  132. raise NotImplementedError(self.get_packed_refs)
  133. def add_packed_refs(self, new_refs: Dict[Ref, Optional[ObjectID]]):
  134. """Add the given refs as packed refs.
  135. Args:
  136. new_refs: A mapping of ref names to targets; if a target is None that
  137. means remove the ref
  138. """
  139. raise NotImplementedError(self.add_packed_refs)
  140. def get_peeled(self, name):
  141. """Return the cached peeled value of a ref, if available.
  142. Args:
  143. name: Name of the ref to peel
  144. Returns: The peeled value of the ref. If the ref is known not point to
  145. a tag, this will be the SHA the ref refers to. If the ref may point
  146. to a tag, but no cached information is available, None is returned.
  147. """
  148. return None
  149. def import_refs(
  150. self,
  151. base: Ref,
  152. other: Dict[Ref, ObjectID],
  153. committer: Optional[bytes] = None,
  154. timestamp: Optional[bytes] = None,
  155. timezone: Optional[bytes] = None,
  156. message: Optional[bytes] = None,
  157. prune: bool = False,
  158. ):
  159. if prune:
  160. to_delete = set(self.subkeys(base))
  161. else:
  162. to_delete = set()
  163. for name, value in other.items():
  164. if value is None:
  165. to_delete.add(name)
  166. else:
  167. self.set_if_equals(
  168. b"/".join((base, name)), None, value, message=message
  169. )
  170. if to_delete:
  171. try:
  172. to_delete.remove(name)
  173. except KeyError:
  174. pass
  175. for ref in to_delete:
  176. self.remove_if_equals(b"/".join((base, ref)), None, message=message)
  177. def allkeys(self):
  178. """All refs present in this container."""
  179. raise NotImplementedError(self.allkeys)
  180. def __iter__(self):
  181. return iter(self.allkeys())
  182. def keys(self, base=None):
  183. """Refs present in this container.
  184. Args:
  185. base: An optional base to return refs under.
  186. Returns: An unsorted set of valid refs in this container, including
  187. packed refs.
  188. """
  189. if base is not None:
  190. return self.subkeys(base)
  191. else:
  192. return self.allkeys()
  193. def subkeys(self, base):
  194. """Refs present in this container under a base.
  195. Args:
  196. base: The base to return refs under.
  197. Returns: A set of valid refs in this container under the base; the base
  198. prefix is stripped from the ref names returned.
  199. """
  200. keys = set()
  201. base_len = len(base) + 1
  202. for refname in self.allkeys():
  203. if refname.startswith(base):
  204. keys.add(refname[base_len:])
  205. return keys
  206. def as_dict(self, base=None):
  207. """Return the contents of this container as a dictionary."""
  208. ret = {}
  209. keys = self.keys(base)
  210. if base is None:
  211. base = b""
  212. else:
  213. base = base.rstrip(b"/")
  214. for key in keys:
  215. try:
  216. ret[key] = self[(base + b"/" + key).strip(b"/")]
  217. except (SymrefLoop, KeyError):
  218. continue # Unable to resolve
  219. return ret
  220. def _check_refname(self, name):
  221. """Ensure a refname is valid and lives in refs or is HEAD.
  222. HEAD is not a valid refname according to git-check-ref-format, but this
  223. class needs to be able to touch HEAD. Also, check_ref_format expects
  224. refnames without the leading 'refs/', but this class requires that
  225. so it cannot touch anything outside the refs dir (or HEAD).
  226. Args:
  227. name: The name of the reference.
  228. Raises:
  229. KeyError: if a refname is not HEAD or is otherwise not valid.
  230. """
  231. if name in (HEADREF, b"refs/stash"):
  232. return
  233. if not name.startswith(b"refs/") or not check_ref_format(name[5:]):
  234. raise RefFormatError(name)
  235. def read_ref(self, refname):
  236. """Read a reference without following any references.
  237. Args:
  238. refname: The name of the reference
  239. Returns: The contents of the ref file, or None if it does
  240. not exist.
  241. """
  242. contents = self.read_loose_ref(refname)
  243. if not contents:
  244. contents = self.get_packed_refs().get(refname, None)
  245. return contents
  246. def read_loose_ref(self, name):
  247. """Read a loose reference and return its contents.
  248. Args:
  249. name: the refname to read
  250. Returns: The contents of the ref file, or None if it does
  251. not exist.
  252. """
  253. raise NotImplementedError(self.read_loose_ref)
  254. def follow(self, name):
  255. """Follow a reference name.
  256. Returns: a tuple of (refnames, sha), wheres refnames are the names of
  257. references in the chain
  258. """
  259. contents = SYMREF + name
  260. depth = 0
  261. refnames = []
  262. while contents.startswith(SYMREF):
  263. refname = contents[len(SYMREF) :]
  264. refnames.append(refname)
  265. contents = self.read_ref(refname)
  266. if not contents:
  267. break
  268. depth += 1
  269. if depth > 5:
  270. raise SymrefLoop(name, depth)
  271. return refnames, contents
  272. def __contains__(self, refname):
  273. if self.read_ref(refname):
  274. return True
  275. return False
  276. def __getitem__(self, name):
  277. """Get the SHA1 for a reference name.
  278. This method follows all symbolic references.
  279. """
  280. _, sha = self.follow(name)
  281. if sha is None:
  282. raise KeyError(name)
  283. return sha
  284. def set_if_equals(
  285. self,
  286. name,
  287. old_ref,
  288. new_ref,
  289. committer=None,
  290. timestamp=None,
  291. timezone=None,
  292. message=None,
  293. ):
  294. """Set a refname to new_ref only if it currently equals old_ref.
  295. This method follows all symbolic references if applicable for the
  296. subclass, and can be used to perform an atomic compare-and-swap
  297. operation.
  298. Args:
  299. name: The refname to set.
  300. old_ref: The old sha the refname must refer to, or None to set
  301. unconditionally.
  302. new_ref: The new sha the refname will refer to.
  303. message: Message for reflog
  304. Returns: True if the set was successful, False otherwise.
  305. """
  306. raise NotImplementedError(self.set_if_equals)
  307. def add_if_new(self, name, ref, committer=None, timestamp=None,
  308. timezone=None, message=None):
  309. """Add a new reference only if it does not already exist.
  310. Args:
  311. name: Ref name
  312. ref: Ref value
  313. """
  314. raise NotImplementedError(self.add_if_new)
  315. def __setitem__(self, name, ref):
  316. """Set a reference name to point to the given SHA1.
  317. This method follows all symbolic references if applicable for the
  318. subclass.
  319. Note: This method unconditionally overwrites the contents of a
  320. reference. To update atomically only if the reference has not
  321. changed, use set_if_equals().
  322. Args:
  323. name: The refname to set.
  324. ref: The new sha the refname will refer to.
  325. """
  326. self.set_if_equals(name, None, ref)
  327. def remove_if_equals(
  328. self,
  329. name,
  330. old_ref,
  331. committer=None,
  332. timestamp=None,
  333. timezone=None,
  334. message=None,
  335. ):
  336. """Remove a refname only if it currently equals old_ref.
  337. This method does not follow symbolic references, even if applicable for
  338. the subclass. It can be used to perform an atomic compare-and-delete
  339. operation.
  340. Args:
  341. name: The refname to delete.
  342. old_ref: The old sha the refname must refer to, or None to
  343. delete unconditionally.
  344. message: Message for reflog
  345. Returns: True if the delete was successful, False otherwise.
  346. """
  347. raise NotImplementedError(self.remove_if_equals)
  348. def __delitem__(self, name):
  349. """Remove a refname.
  350. This method does not follow symbolic references, even if applicable for
  351. the subclass.
  352. Note: This method unconditionally deletes the contents of a reference.
  353. To delete atomically only if the reference has not changed, use
  354. remove_if_equals().
  355. Args:
  356. name: The refname to delete.
  357. """
  358. self.remove_if_equals(name, None)
  359. def get_symrefs(self):
  360. """Get a dict with all symrefs in this container.
  361. Returns: Dictionary mapping source ref to target ref
  362. """
  363. ret = {}
  364. for src in self.allkeys():
  365. try:
  366. dst = parse_symref_value(self.read_ref(src))
  367. except ValueError:
  368. pass
  369. else:
  370. ret[src] = dst
  371. return ret
  372. class DictRefsContainer(RefsContainer):
  373. """RefsContainer backed by a simple dict.
  374. This container does not support symbolic or packed references and is not
  375. threadsafe.
  376. """
  377. def __init__(self, refs, logger=None):
  378. super().__init__(logger=logger)
  379. self._refs = refs
  380. self._peeled = {}
  381. self._watchers = set()
  382. def allkeys(self):
  383. return self._refs.keys()
  384. def read_loose_ref(self, name):
  385. return self._refs.get(name, None)
  386. def get_packed_refs(self):
  387. return {}
  388. def _notify(self, ref, newsha):
  389. for watcher in self._watchers:
  390. watcher._notify((ref, newsha))
  391. def set_symbolic_ref(
  392. self,
  393. name: Ref,
  394. other: Ref,
  395. committer=None,
  396. timestamp=None,
  397. timezone=None,
  398. message=None,
  399. ):
  400. old = self.follow(name)[-1]
  401. new = SYMREF + other
  402. self._refs[name] = new
  403. self._notify(name, new)
  404. self._log(
  405. name,
  406. old,
  407. new,
  408. committer=committer,
  409. timestamp=timestamp,
  410. timezone=timezone,
  411. message=message,
  412. )
  413. def set_if_equals(
  414. self,
  415. name,
  416. old_ref,
  417. new_ref,
  418. committer=None,
  419. timestamp=None,
  420. timezone=None,
  421. message=None,
  422. ):
  423. if old_ref is not None and self._refs.get(name, ZERO_SHA) != old_ref:
  424. return False
  425. realnames, _ = self.follow(name)
  426. for realname in realnames:
  427. self._check_refname(realname)
  428. old = self._refs.get(realname)
  429. self._refs[realname] = new_ref
  430. self._notify(realname, new_ref)
  431. self._log(
  432. realname,
  433. old,
  434. new_ref,
  435. committer=committer,
  436. timestamp=timestamp,
  437. timezone=timezone,
  438. message=message,
  439. )
  440. return True
  441. def add_if_new(
  442. self,
  443. name: Ref,
  444. ref: ObjectID,
  445. committer=None,
  446. timestamp=None,
  447. timezone=None,
  448. message: Optional[bytes] = None,
  449. ):
  450. if name in self._refs:
  451. return False
  452. self._refs[name] = ref
  453. self._notify(name, ref)
  454. self._log(
  455. name,
  456. None,
  457. ref,
  458. committer=committer,
  459. timestamp=timestamp,
  460. timezone=timezone,
  461. message=message,
  462. )
  463. return True
  464. def remove_if_equals(
  465. self,
  466. name,
  467. old_ref,
  468. committer=None,
  469. timestamp=None,
  470. timezone=None,
  471. message=None,
  472. ):
  473. if old_ref is not None and self._refs.get(name, ZERO_SHA) != old_ref:
  474. return False
  475. try:
  476. old = self._refs.pop(name)
  477. except KeyError:
  478. pass
  479. else:
  480. self._notify(name, None)
  481. self._log(
  482. name,
  483. old,
  484. None,
  485. committer=committer,
  486. timestamp=timestamp,
  487. timezone=timezone,
  488. message=message,
  489. )
  490. return True
  491. def get_peeled(self, name):
  492. return self._peeled.get(name)
  493. def _update(self, refs):
  494. """Update multiple refs; intended only for testing."""
  495. # TODO(dborowitz): replace this with a public function that uses
  496. # set_if_equal.
  497. for ref, sha in refs.items():
  498. self.set_if_equals(ref, None, sha)
  499. def _update_peeled(self, peeled):
  500. """Update cached peeled refs; intended only for testing."""
  501. self._peeled.update(peeled)
  502. class InfoRefsContainer(RefsContainer):
  503. """Refs container that reads refs from a info/refs file."""
  504. def __init__(self, f):
  505. self._refs = {}
  506. self._peeled = {}
  507. for line in f.readlines():
  508. sha, name = line.rstrip(b"\n").split(b"\t")
  509. if name.endswith(ANNOTATED_TAG_SUFFIX):
  510. name = name[:-3]
  511. if not check_ref_format(name):
  512. raise ValueError("invalid ref name %r" % name)
  513. self._peeled[name] = sha
  514. else:
  515. if not check_ref_format(name):
  516. raise ValueError("invalid ref name %r" % name)
  517. self._refs[name] = sha
  518. def allkeys(self):
  519. return self._refs.keys()
  520. def read_loose_ref(self, name):
  521. return self._refs.get(name, None)
  522. def get_packed_refs(self):
  523. return {}
  524. def get_peeled(self, name):
  525. try:
  526. return self._peeled[name]
  527. except KeyError:
  528. return self._refs[name]
  529. class DiskRefsContainer(RefsContainer):
  530. """Refs container that reads refs from disk."""
  531. def __init__(self, path, worktree_path=None, logger=None):
  532. super().__init__(logger=logger)
  533. if getattr(path, "encode", None) is not None:
  534. path = os.fsencode(path)
  535. self.path = path
  536. if worktree_path is None:
  537. worktree_path = path
  538. if getattr(worktree_path, "encode", None) is not None:
  539. worktree_path = os.fsencode(worktree_path)
  540. self.worktree_path = worktree_path
  541. self._packed_refs = None
  542. self._peeled_refs = None
  543. def __repr__(self):
  544. return "{}({!r})".format(self.__class__.__name__, self.path)
  545. def subkeys(self, base):
  546. subkeys = set()
  547. path = self.refpath(base)
  548. for root, unused_dirs, files in os.walk(path):
  549. dir = root[len(path) :]
  550. if os.path.sep != "/":
  551. dir = dir.replace(os.fsencode(os.path.sep), b"/")
  552. dir = dir.strip(b"/")
  553. for filename in files:
  554. refname = b"/".join(([dir] if dir else []) + [filename])
  555. # check_ref_format requires at least one /, so we prepend the
  556. # base before calling it.
  557. if check_ref_format(base + b"/" + refname):
  558. subkeys.add(refname)
  559. for key in self.get_packed_refs():
  560. if key.startswith(base):
  561. subkeys.add(key[len(base) :].strip(b"/"))
  562. return subkeys
  563. def allkeys(self):
  564. allkeys = set()
  565. if os.path.exists(self.refpath(HEADREF)):
  566. allkeys.add(HEADREF)
  567. path = self.refpath(b"")
  568. refspath = self.refpath(b"refs")
  569. for root, unused_dirs, files in os.walk(refspath):
  570. dir = root[len(path) :]
  571. if os.path.sep != "/":
  572. dir = dir.replace(os.fsencode(os.path.sep), b"/")
  573. for filename in files:
  574. refname = b"/".join([dir, filename])
  575. if check_ref_format(refname):
  576. allkeys.add(refname)
  577. allkeys.update(self.get_packed_refs())
  578. return allkeys
  579. def refpath(self, name):
  580. """Return the disk path of a ref."""
  581. if os.path.sep != "/":
  582. name = name.replace(b"/", os.fsencode(os.path.sep))
  583. # TODO: as the 'HEAD' reference is working tree specific, it
  584. # should actually not be a part of RefsContainer
  585. if name == HEADREF:
  586. return os.path.join(self.worktree_path, name)
  587. else:
  588. return os.path.join(self.path, name)
  589. def get_packed_refs(self):
  590. """Get contents of the packed-refs file.
  591. Returns: Dictionary mapping ref names to SHA1s
  592. Note: Will return an empty dictionary when no packed-refs file is
  593. present.
  594. """
  595. # TODO: invalidate the cache on repacking
  596. if self._packed_refs is None:
  597. # set both to empty because we want _peeled_refs to be
  598. # None if and only if _packed_refs is also None.
  599. self._packed_refs = {}
  600. self._peeled_refs = {}
  601. path = os.path.join(self.path, b"packed-refs")
  602. try:
  603. f = GitFile(path, "rb")
  604. except FileNotFoundError:
  605. return {}
  606. with f:
  607. first_line = next(iter(f)).rstrip()
  608. if first_line.startswith(b"# pack-refs") and b" peeled" in first_line:
  609. for sha, name, peeled in read_packed_refs_with_peeled(f):
  610. self._packed_refs[name] = sha
  611. if peeled:
  612. self._peeled_refs[name] = peeled
  613. else:
  614. f.seek(0)
  615. for sha, name in read_packed_refs(f):
  616. self._packed_refs[name] = sha
  617. return self._packed_refs
  618. def add_packed_refs(self, new_refs: Dict[Ref, Optional[ObjectID]]):
  619. """Add the given refs as packed refs.
  620. Args:
  621. new_refs: A mapping of ref names to targets; if a target is None that
  622. means remove the ref
  623. """
  624. if not new_refs:
  625. return
  626. path = os.path.join(self.path, b"packed-refs")
  627. with GitFile(path, "wb") as f:
  628. # reread cached refs from disk, while holding the lock
  629. packed_refs = self.get_packed_refs().copy()
  630. for ref, target in new_refs.items():
  631. # sanity check
  632. if ref == HEADREF:
  633. raise ValueError("cannot pack HEAD")
  634. # remove any loose refs pointing to this one -- please
  635. # note that this bypasses remove_if_equals as we don't
  636. # want to affect packed refs in here
  637. try:
  638. os.remove(self.refpath(ref))
  639. except OSError:
  640. pass
  641. if target is not None:
  642. packed_refs[ref] = target
  643. else:
  644. packed_refs.pop(ref, None)
  645. write_packed_refs(f, packed_refs, self._peeled_refs)
  646. self._packed_refs = packed_refs
  647. def get_peeled(self, name):
  648. """Return the cached peeled value of a ref, if available.
  649. Args:
  650. name: Name of the ref to peel
  651. Returns: The peeled value of the ref. If the ref is known not point to
  652. a tag, this will be the SHA the ref refers to. If the ref may point
  653. to a tag, but no cached information is available, None is returned.
  654. """
  655. self.get_packed_refs()
  656. if self._peeled_refs is None or name not in self._packed_refs:
  657. # No cache: no peeled refs were read, or this ref is loose
  658. return None
  659. if name in self._peeled_refs:
  660. return self._peeled_refs[name]
  661. else:
  662. # Known not peelable
  663. return self[name]
  664. def read_loose_ref(self, name):
  665. """Read a reference file and return its contents.
  666. If the reference file a symbolic reference, only read the first line of
  667. the file. Otherwise, only read the first 40 bytes.
  668. Args:
  669. name: the refname to read, relative to refpath
  670. Returns: The contents of the ref file, or None if the file does not
  671. exist.
  672. Raises:
  673. IOError: if any other error occurs
  674. """
  675. filename = self.refpath(name)
  676. try:
  677. with GitFile(filename, "rb") as f:
  678. header = f.read(len(SYMREF))
  679. if header == SYMREF:
  680. # Read only the first line
  681. return header + next(iter(f)).rstrip(b"\r\n")
  682. else:
  683. # Read only the first 40 bytes
  684. return header + f.read(40 - len(SYMREF))
  685. except (OSError, UnicodeError):
  686. # don't assume anything specific about the error; in
  687. # particular, invalid or forbidden paths can raise weird
  688. # errors depending on the specific operating system
  689. return None
  690. def _remove_packed_ref(self, name):
  691. if self._packed_refs is None:
  692. return
  693. filename = os.path.join(self.path, b"packed-refs")
  694. # reread cached refs from disk, while holding the lock
  695. f = GitFile(filename, "wb")
  696. try:
  697. self._packed_refs = None
  698. self.get_packed_refs()
  699. if name not in self._packed_refs:
  700. return
  701. del self._packed_refs[name]
  702. with suppress(KeyError):
  703. del self._peeled_refs[name]
  704. write_packed_refs(f, self._packed_refs, self._peeled_refs)
  705. f.close()
  706. finally:
  707. f.abort()
  708. def set_symbolic_ref(
  709. self,
  710. name,
  711. other,
  712. committer=None,
  713. timestamp=None,
  714. timezone=None,
  715. message=None,
  716. ):
  717. """Make a ref point at another ref.
  718. Args:
  719. name: Name of the ref to set
  720. other: Name of the ref to point at
  721. message: Optional message to describe the change
  722. """
  723. self._check_refname(name)
  724. self._check_refname(other)
  725. filename = self.refpath(name)
  726. f = GitFile(filename, "wb")
  727. try:
  728. f.write(SYMREF + other + b"\n")
  729. sha = self.follow(name)[-1]
  730. self._log(
  731. name,
  732. sha,
  733. sha,
  734. committer=committer,
  735. timestamp=timestamp,
  736. timezone=timezone,
  737. message=message,
  738. )
  739. except BaseException:
  740. f.abort()
  741. raise
  742. else:
  743. f.close()
  744. def set_if_equals(
  745. self,
  746. name,
  747. old_ref,
  748. new_ref,
  749. committer=None,
  750. timestamp=None,
  751. timezone=None,
  752. message=None,
  753. ):
  754. """Set a refname to new_ref only if it currently equals old_ref.
  755. This method follows all symbolic references, and can be used to perform
  756. an atomic compare-and-swap operation.
  757. Args:
  758. name: The refname to set.
  759. old_ref: The old sha the refname must refer to, or None to set
  760. unconditionally.
  761. new_ref: The new sha the refname will refer to.
  762. message: Set message for reflog
  763. Returns: True if the set was successful, False otherwise.
  764. """
  765. self._check_refname(name)
  766. try:
  767. realnames, _ = self.follow(name)
  768. realname = realnames[-1]
  769. except (KeyError, IndexError, SymrefLoop):
  770. realname = name
  771. filename = self.refpath(realname)
  772. # make sure none of the ancestor folders is in packed refs
  773. probe_ref = os.path.dirname(realname)
  774. packed_refs = self.get_packed_refs()
  775. while probe_ref:
  776. if packed_refs.get(probe_ref, None) is not None:
  777. raise NotADirectoryError(filename)
  778. probe_ref = os.path.dirname(probe_ref)
  779. ensure_dir_exists(os.path.dirname(filename))
  780. with GitFile(filename, "wb") as f:
  781. if old_ref is not None:
  782. try:
  783. # read again while holding the lock
  784. orig_ref = self.read_loose_ref(realname)
  785. if orig_ref is None:
  786. orig_ref = self.get_packed_refs().get(realname, ZERO_SHA)
  787. if orig_ref != old_ref:
  788. f.abort()
  789. return False
  790. except OSError:
  791. f.abort()
  792. raise
  793. try:
  794. f.write(new_ref + b"\n")
  795. except OSError:
  796. f.abort()
  797. raise
  798. self._log(
  799. realname,
  800. old_ref,
  801. new_ref,
  802. committer=committer,
  803. timestamp=timestamp,
  804. timezone=timezone,
  805. message=message,
  806. )
  807. return True
  808. def add_if_new(
  809. self,
  810. name: bytes,
  811. ref: bytes,
  812. committer=None,
  813. timestamp=None,
  814. timezone=None,
  815. message: Optional[bytes] = None,
  816. ):
  817. """Add a new reference only if it does not already exist.
  818. This method follows symrefs, and only ensures that the last ref in the
  819. chain does not exist.
  820. Args:
  821. name: The refname to set.
  822. ref: The new sha the refname will refer to.
  823. message: Optional message for reflog
  824. Returns: True if the add was successful, False otherwise.
  825. """
  826. try:
  827. realnames, contents = self.follow(name)
  828. if contents is not None:
  829. return False
  830. realname = realnames[-1]
  831. except (KeyError, IndexError):
  832. realname = name
  833. self._check_refname(realname)
  834. filename = self.refpath(realname)
  835. ensure_dir_exists(os.path.dirname(filename))
  836. with GitFile(filename, "wb") as f:
  837. if os.path.exists(filename) or name in self.get_packed_refs():
  838. f.abort()
  839. return False
  840. try:
  841. f.write(ref + b"\n")
  842. except OSError:
  843. f.abort()
  844. raise
  845. else:
  846. self._log(
  847. name,
  848. None,
  849. ref,
  850. committer=committer,
  851. timestamp=timestamp,
  852. timezone=timezone,
  853. message=message,
  854. )
  855. return True
  856. def remove_if_equals(
  857. self,
  858. name,
  859. old_ref,
  860. committer=None,
  861. timestamp=None,
  862. timezone=None,
  863. message=None,
  864. ):
  865. """Remove a refname only if it currently equals old_ref.
  866. This method does not follow symbolic references. It can be used to
  867. perform an atomic compare-and-delete operation.
  868. Args:
  869. name: The refname to delete.
  870. old_ref: The old sha the refname must refer to, or None to
  871. delete unconditionally.
  872. message: Optional message
  873. Returns: True if the delete was successful, False otherwise.
  874. """
  875. self._check_refname(name)
  876. filename = self.refpath(name)
  877. ensure_dir_exists(os.path.dirname(filename))
  878. f = GitFile(filename, "wb")
  879. try:
  880. if old_ref is not None:
  881. orig_ref = self.read_loose_ref(name)
  882. if orig_ref is None:
  883. orig_ref = self.get_packed_refs().get(name, ZERO_SHA)
  884. if orig_ref != old_ref:
  885. return False
  886. # remove the reference file itself
  887. try:
  888. found = os.path.lexists(filename)
  889. except OSError:
  890. # may only be packed, or otherwise unstorable
  891. found = False
  892. if found:
  893. os.remove(filename)
  894. self._remove_packed_ref(name)
  895. self._log(
  896. name,
  897. old_ref,
  898. None,
  899. committer=committer,
  900. timestamp=timestamp,
  901. timezone=timezone,
  902. message=message,
  903. )
  904. finally:
  905. # never write, we just wanted the lock
  906. f.abort()
  907. # outside of the lock, clean-up any parent directory that might now
  908. # be empty. this ensures that re-creating a reference of the same
  909. # name of what was previously a directory works as expected
  910. parent = name
  911. while True:
  912. try:
  913. parent, _ = parent.rsplit(b"/", 1)
  914. except ValueError:
  915. break
  916. if parent == b'refs':
  917. break
  918. parent_filename = self.refpath(parent)
  919. try:
  920. os.rmdir(parent_filename)
  921. except OSError:
  922. # this can be caused by the parent directory being
  923. # removed by another process, being not empty, etc.
  924. # in any case, this is non fatal because we already
  925. # removed the reference, just ignore it
  926. break
  927. return True
  928. def _split_ref_line(line):
  929. """Split a single ref line into a tuple of SHA1 and name."""
  930. fields = line.rstrip(b"\n\r").split(b" ")
  931. if len(fields) != 2:
  932. raise PackedRefsException("invalid ref line %r" % line)
  933. sha, name = fields
  934. if not valid_hexsha(sha):
  935. raise PackedRefsException("Invalid hex sha %r" % sha)
  936. if not check_ref_format(name):
  937. raise PackedRefsException("invalid ref name %r" % name)
  938. return (sha, name)
  939. def read_packed_refs(f):
  940. """Read a packed refs file.
  941. Args:
  942. f: file-like object to read from
  943. Returns: Iterator over tuples with SHA1s and ref names.
  944. """
  945. for line in f:
  946. if line.startswith(b"#"):
  947. # Comment
  948. continue
  949. if line.startswith(b"^"):
  950. raise PackedRefsException("found peeled ref in packed-refs without peeled")
  951. yield _split_ref_line(line)
  952. def read_packed_refs_with_peeled(f):
  953. """Read a packed refs file including peeled refs.
  954. Assumes the "# pack-refs with: peeled" line was already read. Yields tuples
  955. with ref names, SHA1s, and peeled SHA1s (or None).
  956. Args:
  957. f: file-like object to read from, seek'ed to the second line
  958. """
  959. last = None
  960. for line in f:
  961. if line[0] == b"#":
  962. continue
  963. line = line.rstrip(b"\r\n")
  964. if line.startswith(b"^"):
  965. if not last:
  966. raise PackedRefsException("unexpected peeled ref line")
  967. if not valid_hexsha(line[1:]):
  968. raise PackedRefsException("Invalid hex sha %r" % line[1:])
  969. sha, name = _split_ref_line(last)
  970. last = None
  971. yield (sha, name, line[1:])
  972. else:
  973. if last:
  974. sha, name = _split_ref_line(last)
  975. yield (sha, name, None)
  976. last = line
  977. if last:
  978. sha, name = _split_ref_line(last)
  979. yield (sha, name, None)
  980. def write_packed_refs(f, packed_refs, peeled_refs=None):
  981. """Write a packed refs file.
  982. Args:
  983. f: empty file-like object to write to
  984. packed_refs: dict of refname to sha of packed refs to write
  985. peeled_refs: dict of refname to peeled value of sha
  986. """
  987. if peeled_refs is None:
  988. peeled_refs = {}
  989. else:
  990. f.write(b"# pack-refs with: peeled\n")
  991. for refname in sorted(packed_refs.keys()):
  992. f.write(git_line(packed_refs[refname], refname))
  993. if refname in peeled_refs:
  994. f.write(b"^" + peeled_refs[refname] + b"\n")
  995. def read_info_refs(f):
  996. ret = {}
  997. for line in f.readlines():
  998. (sha, name) = line.rstrip(b"\r\n").split(b"\t", 1)
  999. ret[name] = sha
  1000. return ret
  1001. def write_info_refs(refs, store: ObjectContainer):
  1002. """Generate info refs."""
  1003. # Avoid recursive import :(
  1004. from dulwich.object_store import peel_sha
  1005. for name, sha in sorted(refs.items()):
  1006. # get_refs() includes HEAD as a special case, but we don't want to
  1007. # advertise it
  1008. if name == HEADREF:
  1009. continue
  1010. try:
  1011. o = store[sha]
  1012. except KeyError:
  1013. continue
  1014. peeled = peel_sha(store, sha)
  1015. yield o.id + b"\t" + name + b"\n"
  1016. if o.id != peeled.id:
  1017. yield peeled.id + b"\t" + name + ANNOTATED_TAG_SUFFIX + b"\n"
  1018. def is_local_branch(x):
  1019. return x.startswith(LOCAL_BRANCH_PREFIX)
  1020. def strip_peeled_refs(refs):
  1021. """Remove all peeled refs"""
  1022. return {
  1023. ref: sha
  1024. for (ref, sha) in refs.items()
  1025. if not ref.endswith(ANNOTATED_TAG_SUFFIX)
  1026. }
  1027. def _set_origin_head(refs, origin, origin_head):
  1028. # set refs/remotes/origin/HEAD
  1029. origin_base = b"refs/remotes/" + origin + b"/"
  1030. if origin_head and origin_head.startswith(LOCAL_BRANCH_PREFIX):
  1031. origin_ref = origin_base + HEADREF
  1032. target_ref = origin_base + origin_head[len(LOCAL_BRANCH_PREFIX) :]
  1033. if target_ref in refs:
  1034. refs.set_symbolic_ref(origin_ref, target_ref)
  1035. def _set_default_branch(
  1036. refs: RefsContainer, origin: bytes, origin_head: bytes, branch: bytes,
  1037. ref_message: Optional[bytes]) -> bytes:
  1038. """Set the default branch.
  1039. """
  1040. origin_base = b"refs/remotes/" + origin + b"/"
  1041. if branch:
  1042. origin_ref = origin_base + branch
  1043. if origin_ref in refs:
  1044. local_ref = LOCAL_BRANCH_PREFIX + branch
  1045. refs.add_if_new(
  1046. local_ref, refs[origin_ref], ref_message
  1047. )
  1048. head_ref = local_ref
  1049. elif LOCAL_TAG_PREFIX + branch in refs:
  1050. head_ref = LOCAL_TAG_PREFIX + branch
  1051. else:
  1052. raise ValueError(
  1053. "%r is not a valid branch or tag" % os.fsencode(branch)
  1054. )
  1055. elif origin_head:
  1056. head_ref = origin_head
  1057. if origin_head.startswith(LOCAL_BRANCH_PREFIX):
  1058. origin_ref = origin_base + origin_head[len(LOCAL_BRANCH_PREFIX) :]
  1059. else:
  1060. origin_ref = origin_head
  1061. try:
  1062. refs.add_if_new(
  1063. head_ref, refs[origin_ref], ref_message
  1064. )
  1065. except KeyError:
  1066. pass
  1067. else:
  1068. raise ValueError('neither origin_head nor branch are provided')
  1069. return head_ref
  1070. def _set_head(refs, head_ref, ref_message):
  1071. if head_ref.startswith(LOCAL_TAG_PREFIX):
  1072. # detach HEAD at specified tag
  1073. head = refs[head_ref]
  1074. if isinstance(head, Tag):
  1075. _cls, obj = head.object
  1076. head = obj.get_object(obj).id
  1077. del refs[HEADREF]
  1078. refs.set_if_equals(
  1079. HEADREF, None, head, message=ref_message
  1080. )
  1081. else:
  1082. # set HEAD to specific branch
  1083. try:
  1084. head = refs[head_ref]
  1085. refs.set_symbolic_ref(HEADREF, head_ref)
  1086. refs.set_if_equals(HEADREF, None, head, message=ref_message)
  1087. except KeyError:
  1088. head = None
  1089. return head
  1090. def _import_remote_refs(
  1091. refs_container: RefsContainer,
  1092. remote_name: str,
  1093. refs: Dict[str, str],
  1094. message: Optional[bytes] = None,
  1095. prune: bool = False,
  1096. prune_tags: bool = False,
  1097. ):
  1098. stripped_refs = strip_peeled_refs(refs)
  1099. branches = {
  1100. n[len(LOCAL_BRANCH_PREFIX) :]: v
  1101. for (n, v) in stripped_refs.items()
  1102. if n.startswith(LOCAL_BRANCH_PREFIX)
  1103. }
  1104. refs_container.import_refs(
  1105. b"refs/remotes/" + remote_name.encode(),
  1106. branches,
  1107. message=message,
  1108. prune=prune,
  1109. )
  1110. tags = {
  1111. n[len(LOCAL_TAG_PREFIX) :]: v
  1112. for (n, v) in stripped_refs.items()
  1113. if n.startswith(LOCAL_TAG_PREFIX) and not n.endswith(ANNOTATED_TAG_SUFFIX)
  1114. }
  1115. refs_container.import_refs(LOCAL_TAG_PREFIX, tags, message=message, prune=prune_tags)