refs.py 36 KB

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