repo.py 34 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001
  1. # repo.py -- For dealing with git repositories.
  2. # Copyright (C) 2007 James Westby <jw+debian@jameswestby.net>
  3. # Copyright (C) 2008-2013 Jelmer Vernooij <jelmer@samba.org>
  4. #
  5. # This program is free software; you can redistribute it and/or
  6. # modify it under the terms of the GNU General Public License
  7. # as published by the Free Software Foundation; version 2
  8. # of the License or (at your option) any later version of
  9. # the License.
  10. #
  11. # This program is distributed in the hope that it will be useful,
  12. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  13. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  14. # GNU General Public License for more details.
  15. #
  16. # You should have received a copy of the GNU General Public License
  17. # along with this program; if not, write to the Free Software
  18. # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
  19. # MA 02110-1301, USA.
  20. """Repository access.
  21. This module contains the base class for git repositories
  22. (BaseRepo) and an implementation which uses a repository on
  23. local disk (Repo).
  24. """
  25. from io import BytesIO
  26. import errno
  27. import os
  28. import sys
  29. from dulwich.errors import (
  30. NoIndexPresent,
  31. NotBlobError,
  32. NotCommitError,
  33. NotGitRepository,
  34. NotTreeError,
  35. NotTagError,
  36. CommitError,
  37. RefFormatError,
  38. HookError,
  39. )
  40. from dulwich.file import (
  41. GitFile,
  42. )
  43. from dulwich.object_store import (
  44. DiskObjectStore,
  45. MemoryObjectStore,
  46. ObjectStoreGraphWalker,
  47. )
  48. from dulwich.objects import (
  49. check_hexsha,
  50. Blob,
  51. Commit,
  52. ShaFile,
  53. Tag,
  54. Tree,
  55. )
  56. from dulwich.hooks import (
  57. PreCommitShellHook,
  58. PostCommitShellHook,
  59. CommitMsgShellHook,
  60. )
  61. from dulwich.refs import (
  62. check_ref_format,
  63. RefsContainer,
  64. DictRefsContainer,
  65. InfoRefsContainer,
  66. DiskRefsContainer,
  67. read_packed_refs,
  68. read_packed_refs_with_peeled,
  69. write_packed_refs,
  70. SYMREF,
  71. )
  72. import warnings
  73. OBJECTDIR = b'objects'
  74. REFSDIR = b'refs'
  75. REFSDIR_TAGS = b'tags'
  76. REFSDIR_HEADS = b'heads'
  77. INDEX_FILENAME = b'index'
  78. BASE_DIRECTORIES = [
  79. [b'branches'],
  80. [REFSDIR],
  81. [REFSDIR, REFSDIR_TAGS],
  82. [REFSDIR, REFSDIR_HEADS],
  83. [b'hooks'],
  84. [b'info']
  85. ]
  86. def parse_graftpoints(graftpoints):
  87. """Convert a list of graftpoints into a dict
  88. :param graftpoints: Iterator of graftpoint lines
  89. Each line is formatted as:
  90. <commit sha1> <parent sha1> [<parent sha1>]*
  91. Resulting dictionary is:
  92. <commit sha1>: [<parent sha1>*]
  93. https://git.wiki.kernel.org/index.php/GraftPoint
  94. """
  95. grafts = {}
  96. for l in graftpoints:
  97. raw_graft = l.split(None, 1)
  98. commit = raw_graft[0]
  99. if len(raw_graft) == 2:
  100. parents = raw_graft[1].split()
  101. else:
  102. parents = []
  103. for sha in [commit] + parents:
  104. check_hexsha(sha, 'Invalid graftpoint')
  105. grafts[commit] = parents
  106. return grafts
  107. def serialize_graftpoints(graftpoints):
  108. """Convert a dictionary of grafts into string
  109. The graft dictionary is:
  110. <commit sha1>: [<parent sha1>*]
  111. Each line is formatted as:
  112. <commit sha1> <parent sha1> [<parent sha1>]*
  113. https://git.wiki.kernel.org/index.php/GraftPoint
  114. """
  115. graft_lines = []
  116. for commit, parents in graftpoints.items():
  117. if parents:
  118. graft_lines.append(commit + b' ' + b' '.join(parents))
  119. else:
  120. graft_lines.append(commit)
  121. return b'\n'.join(graft_lines)
  122. class BaseRepo(object):
  123. """Base class for a git repository.
  124. :ivar object_store: Dictionary-like object for accessing
  125. the objects
  126. :ivar refs: Dictionary-like object with the refs in this
  127. repository
  128. """
  129. def __init__(self, object_store, refs):
  130. """Open a repository.
  131. This shouldn't be called directly, but rather through one of the
  132. base classes, such as MemoryRepo or Repo.
  133. :param object_store: Object store to use
  134. :param refs: Refs container to use
  135. """
  136. self.object_store = object_store
  137. self.refs = refs
  138. self._graftpoints = {}
  139. self.hooks = {}
  140. def _init_files(self, bare):
  141. """Initialize a default set of named files."""
  142. from dulwich.config import ConfigFile
  143. self._put_named_file(b'description', b"Unnamed repository")
  144. f = BytesIO()
  145. cf = ConfigFile()
  146. cf.set(b"core", b"repositoryformatversion", b"0")
  147. cf.set(b"core", b"filemode", b"true")
  148. cf.set(b"core", b"bare", bare)
  149. cf.set(b"core", b"logallrefupdates", True)
  150. cf.write_to_file(f)
  151. self._put_named_file(b'config', f.getvalue())
  152. self._put_named_file(os.path.join(b'info', b'exclude'), b'')
  153. def get_named_file(self, path):
  154. """Get a file from the control dir with a specific name.
  155. Although the filename should be interpreted as a filename relative to
  156. the control dir in a disk-based Repo, the object returned need not be
  157. pointing to a file in that location.
  158. :param path: The path to the file, relative to the control dir.
  159. :return: An open file object, or None if the file does not exist.
  160. """
  161. raise NotImplementedError(self.get_named_file)
  162. def _put_named_file(self, path, contents):
  163. """Write a file to the control dir with the given name and contents.
  164. :param path: The path to the file, relative to the control dir.
  165. :param contents: A string to write to the file.
  166. """
  167. raise NotImplementedError(self._put_named_file)
  168. def open_index(self):
  169. """Open the index for this repository.
  170. :raise NoIndexPresent: If no index is present
  171. :return: The matching `Index`
  172. """
  173. raise NotImplementedError(self.open_index)
  174. def fetch(self, target, determine_wants=None, progress=None):
  175. """Fetch objects into another repository.
  176. :param target: The target repository
  177. :param determine_wants: Optional function to determine what refs to
  178. fetch.
  179. :param progress: Optional progress function
  180. :return: The local refs
  181. """
  182. if determine_wants is None:
  183. determine_wants = target.object_store.determine_wants_all
  184. target.object_store.add_objects(
  185. self.fetch_objects(determine_wants, target.get_graph_walker(),
  186. progress))
  187. return self.get_refs()
  188. def fetch_objects(self, determine_wants, graph_walker, progress,
  189. get_tagged=None):
  190. """Fetch the missing objects required for a set of revisions.
  191. :param determine_wants: Function that takes a dictionary with heads
  192. and returns the list of heads to fetch.
  193. :param graph_walker: Object that can iterate over the list of revisions
  194. to fetch and has an "ack" method that will be called to acknowledge
  195. that a revision is present.
  196. :param progress: Simple progress function that will be called with
  197. updated progress strings.
  198. :param get_tagged: Function that returns a dict of pointed-to sha -> tag
  199. sha for including tags.
  200. :return: iterator over objects, with __len__ implemented
  201. """
  202. wants = determine_wants(self.get_refs())
  203. if not isinstance(wants, list):
  204. raise TypeError("determine_wants() did not return a list")
  205. shallows = getattr(graph_walker, 'shallow', frozenset())
  206. unshallows = getattr(graph_walker, 'unshallow', frozenset())
  207. if wants == []:
  208. # TODO(dborowitz): find a way to short-circuit that doesn't change
  209. # this interface.
  210. if shallows or unshallows:
  211. # Do not send a pack in shallow short-circuit path
  212. return None
  213. return []
  214. haves = self.object_store.find_common_revisions(graph_walker)
  215. # Deal with shallow requests separately because the haves do
  216. # not reflect what objects are missing
  217. if shallows or unshallows:
  218. haves = [] # TODO: filter the haves commits from iter_shas.
  219. # the specific commits aren't missing.
  220. def get_parents(commit):
  221. if commit.id in shallows:
  222. return []
  223. return self.get_parents(commit.id, commit)
  224. return self.object_store.iter_shas(
  225. self.object_store.find_missing_objects(
  226. haves, wants, progress,
  227. get_tagged,
  228. get_parents=get_parents))
  229. def get_graph_walker(self, heads=None):
  230. """Retrieve a graph walker.
  231. A graph walker is used by a remote repository (or proxy)
  232. to find out which objects are present in this repository.
  233. :param heads: Repository heads to use (optional)
  234. :return: A graph walker object
  235. """
  236. if heads is None:
  237. heads = self.refs.as_dict(b'refs/heads').values()
  238. return ObjectStoreGraphWalker(heads, self.get_parents)
  239. def get_refs(self):
  240. """Get dictionary with all refs.
  241. :return: A ``dict`` mapping ref names to SHA1s
  242. """
  243. return self.refs.as_dict()
  244. def head(self):
  245. """Return the SHA1 pointed at by HEAD."""
  246. return self.refs[b'HEAD']
  247. def _get_object(self, sha, cls):
  248. assert len(sha) in (20, 40)
  249. ret = self.get_object(sha)
  250. if not isinstance(ret, cls):
  251. if cls is Commit:
  252. raise NotCommitError(ret)
  253. elif cls is Blob:
  254. raise NotBlobError(ret)
  255. elif cls is Tree:
  256. raise NotTreeError(ret)
  257. elif cls is Tag:
  258. raise NotTagError(ret)
  259. else:
  260. raise Exception("Type invalid: %r != %r" % (
  261. ret.type_name, cls.type_name))
  262. return ret
  263. def get_object(self, sha):
  264. """Retrieve the object with the specified SHA.
  265. :param sha: SHA to retrieve
  266. :return: A ShaFile object
  267. :raise KeyError: when the object can not be found
  268. """
  269. return self.object_store[sha]
  270. def get_parents(self, sha, commit=None):
  271. """Retrieve the parents of a specific commit.
  272. If the specific commit is a graftpoint, the graft parents
  273. will be returned instead.
  274. :param sha: SHA of the commit for which to retrieve the parents
  275. :param commit: Optional commit matching the sha
  276. :return: List of parents
  277. """
  278. try:
  279. return self._graftpoints[sha]
  280. except KeyError:
  281. if commit is None:
  282. commit = self[sha]
  283. return commit.parents
  284. def get_config(self):
  285. """Retrieve the config object.
  286. :return: `ConfigFile` object for the ``.git/config`` file.
  287. """
  288. raise NotImplementedError(self.get_config)
  289. def get_description(self):
  290. """Retrieve the description for this repository.
  291. :return: String with the description of the repository
  292. as set by the user.
  293. """
  294. raise NotImplementedError(self.get_description)
  295. def set_description(self, description):
  296. """Set the description for this repository.
  297. :param description: Text to set as description for this repository.
  298. """
  299. raise NotImplementedError(self.set_description)
  300. def get_config_stack(self):
  301. """Return a config stack for this repository.
  302. This stack accesses the configuration for both this repository
  303. itself (.git/config) and the global configuration, which usually
  304. lives in ~/.gitconfig.
  305. :return: `Config` instance for this repository
  306. """
  307. from dulwich.config import StackedConfig
  308. backends = [self.get_config()] + StackedConfig.default_backends()
  309. return StackedConfig(backends, writable=backends[0])
  310. def get_peeled(self, ref):
  311. """Get the peeled value of a ref.
  312. :param ref: The refname to peel.
  313. :return: The fully-peeled SHA1 of a tag object, after peeling all
  314. intermediate tags; if the original ref does not point to a tag, this
  315. will equal the original SHA1.
  316. """
  317. cached = self.refs.get_peeled(ref)
  318. if cached is not None:
  319. return cached
  320. return self.object_store.peel_sha(self.refs[ref]).id
  321. def get_walker(self, include=None, *args, **kwargs):
  322. """Obtain a walker for this repository.
  323. :param include: Iterable of SHAs of commits to include along with their
  324. ancestors. Defaults to [HEAD]
  325. :param exclude: Iterable of SHAs of commits to exclude along with their
  326. ancestors, overriding includes.
  327. :param order: ORDER_* constant specifying the order of results. Anything
  328. other than ORDER_DATE may result in O(n) memory usage.
  329. :param reverse: If True, reverse the order of output, requiring O(n)
  330. memory.
  331. :param max_entries: The maximum number of entries to yield, or None for
  332. no limit.
  333. :param paths: Iterable of file or subtree paths to show entries for.
  334. :param rename_detector: diff.RenameDetector object for detecting
  335. renames.
  336. :param follow: If True, follow path across renames/copies. Forces a
  337. default rename_detector.
  338. :param since: Timestamp to list commits after.
  339. :param until: Timestamp to list commits before.
  340. :param queue_cls: A class to use for a queue of commits, supporting the
  341. iterator protocol. The constructor takes a single argument, the
  342. Walker.
  343. :return: A `Walker` object
  344. """
  345. from dulwich.walk import Walker
  346. if include is None:
  347. include = [self.head()]
  348. if isinstance(include, str):
  349. include = [include]
  350. kwargs['get_parents'] = lambda commit: self.get_parents(commit.id, commit)
  351. return Walker(self.object_store, include, *args, **kwargs)
  352. def __getitem__(self, name):
  353. """Retrieve a Git object by SHA1 or ref.
  354. :param name: A Git object SHA1 or a ref name
  355. :return: A `ShaFile` object, such as a Commit or Blob
  356. :raise KeyError: when the specified ref or object does not exist
  357. """
  358. if not isinstance(name, bytes):
  359. raise TypeError("'name' must be bytestring, not %.80s" %
  360. type(name).__name__)
  361. if len(name) in (20, 40):
  362. try:
  363. return self.object_store[name]
  364. except (KeyError, ValueError):
  365. pass
  366. try:
  367. return self.object_store[self.refs[name]]
  368. except RefFormatError:
  369. raise KeyError(name)
  370. def __contains__(self, name):
  371. """Check if a specific Git object or ref is present.
  372. :param name: Git object SHA1 or ref name
  373. """
  374. if len(name) in (20, 40):
  375. return name in self.object_store or name in self.refs
  376. else:
  377. return name in self.refs
  378. def __setitem__(self, name, value):
  379. """Set a ref.
  380. :param name: ref name
  381. :param value: Ref value - either a ShaFile object, or a hex sha
  382. """
  383. if name.startswith(b"refs/") or name == b'HEAD':
  384. if isinstance(value, ShaFile):
  385. self.refs[name] = value.id
  386. elif isinstance(value, bytes):
  387. self.refs[name] = value
  388. else:
  389. raise TypeError(value)
  390. else:
  391. raise ValueError(name)
  392. def __delitem__(self, name):
  393. """Remove a ref.
  394. :param name: Name of the ref to remove
  395. """
  396. if name.startswith(b"refs/") or name == b"HEAD":
  397. del self.refs[name]
  398. else:
  399. raise ValueError(name)
  400. def _get_user_identity(self):
  401. """Determine the identity to use for new commits.
  402. """
  403. config = self.get_config_stack()
  404. return (config.get((b"user", ), b"name") + b" <" +
  405. config.get((b"user", ), b"email") + b">")
  406. def _add_graftpoints(self, updated_graftpoints):
  407. """Add or modify graftpoints
  408. :param updated_graftpoints: Dict of commit shas to list of parent shas
  409. """
  410. # Simple validation
  411. for commit, parents in updated_graftpoints.items():
  412. for sha in [commit] + parents:
  413. check_hexsha(sha, 'Invalid graftpoint')
  414. self._graftpoints.update(updated_graftpoints)
  415. def _remove_graftpoints(self, to_remove=[]):
  416. """Remove graftpoints
  417. :param to_remove: List of commit shas
  418. """
  419. for sha in to_remove:
  420. del self._graftpoints[sha]
  421. def do_commit(self, message=None, committer=None,
  422. author=None, commit_timestamp=None,
  423. commit_timezone=None, author_timestamp=None,
  424. author_timezone=None, tree=None, encoding=None,
  425. ref=b'HEAD', merge_heads=None):
  426. """Create a new commit.
  427. :param message: Commit message
  428. :param committer: Committer fullname
  429. :param author: Author fullname (defaults to committer)
  430. :param commit_timestamp: Commit timestamp (defaults to now)
  431. :param commit_timezone: Commit timestamp timezone (defaults to GMT)
  432. :param author_timestamp: Author timestamp (defaults to commit timestamp)
  433. :param author_timezone: Author timestamp timezone
  434. (defaults to commit timestamp timezone)
  435. :param tree: SHA1 of the tree root to use (if not specified the
  436. current index will be committed).
  437. :param encoding: Encoding
  438. :param ref: Optional ref to commit to (defaults to current branch)
  439. :param merge_heads: Merge heads (defaults to .git/MERGE_HEADS)
  440. :return: New commit SHA1
  441. """
  442. import time
  443. c = Commit()
  444. if tree is None:
  445. index = self.open_index()
  446. c.tree = index.commit(self.object_store)
  447. else:
  448. if len(tree) != 40:
  449. raise ValueError("tree must be a 40-byte hex sha string")
  450. c.tree = tree
  451. try:
  452. self.hooks['pre-commit'].execute()
  453. except HookError as e:
  454. raise CommitError(e)
  455. except KeyError: # no hook defined, silent fallthrough
  456. pass
  457. if merge_heads is None:
  458. # FIXME: Read merge heads from .git/MERGE_HEADS
  459. merge_heads = []
  460. if committer is None:
  461. # FIXME: Support GIT_COMMITTER_NAME/GIT_COMMITTER_EMAIL environment
  462. # variables
  463. committer = self._get_user_identity()
  464. c.committer = committer
  465. if commit_timestamp is None:
  466. # FIXME: Support GIT_COMMITTER_DATE environment variable
  467. commit_timestamp = time.time()
  468. c.commit_time = int(commit_timestamp)
  469. if commit_timezone is None:
  470. # FIXME: Use current user timezone rather than UTC
  471. commit_timezone = 0
  472. c.commit_timezone = commit_timezone
  473. if author is None:
  474. # FIXME: Support GIT_AUTHOR_NAME/GIT_AUTHOR_EMAIL environment
  475. # variables
  476. author = committer
  477. c.author = author
  478. if author_timestamp is None:
  479. # FIXME: Support GIT_AUTHOR_DATE environment variable
  480. author_timestamp = commit_timestamp
  481. c.author_time = int(author_timestamp)
  482. if author_timezone is None:
  483. author_timezone = commit_timezone
  484. c.author_timezone = author_timezone
  485. if encoding is not None:
  486. c.encoding = encoding
  487. if message is None:
  488. # FIXME: Try to read commit message from .git/MERGE_MSG
  489. raise ValueError("No commit message specified")
  490. try:
  491. c.message = self.hooks['commit-msg'].execute(message)
  492. if c.message is None:
  493. c.message = message
  494. except HookError as e:
  495. raise CommitError(e)
  496. except KeyError: # no hook defined, message not modified
  497. c.message = message
  498. if ref is None:
  499. # Create a dangling commit
  500. c.parents = merge_heads
  501. self.object_store.add_object(c)
  502. else:
  503. try:
  504. old_head = self.refs[ref]
  505. c.parents = [old_head] + merge_heads
  506. self.object_store.add_object(c)
  507. ok = self.refs.set_if_equals(ref, old_head, c.id)
  508. except KeyError:
  509. c.parents = merge_heads
  510. self.object_store.add_object(c)
  511. ok = self.refs.add_if_new(ref, c.id)
  512. if not ok:
  513. # Fail if the atomic compare-and-swap failed, leaving the commit and
  514. # all its objects as garbage.
  515. raise CommitError("%s changed during commit" % (ref,))
  516. try:
  517. self.hooks['post-commit'].execute()
  518. except HookError as e: # silent failure
  519. warnings.warn("post-commit hook failed: %s" % e, UserWarning)
  520. except KeyError: # no hook defined, silent fallthrough
  521. pass
  522. return c.id
  523. path_sep_bytes = os.path.sep.encode(sys.getfilesystemencoding())
  524. class Repo(BaseRepo):
  525. """A git repository backed by local disk.
  526. To open an existing repository, call the contructor with
  527. the path of the repository.
  528. To create a new repository, use the Repo.init class method.
  529. """
  530. def __init__(self, path):
  531. self.path = path
  532. if not isinstance(path, bytes):
  533. self._path_bytes = path.encode(sys.getfilesystemencoding())
  534. else:
  535. self._path_bytes = path
  536. if os.path.isdir(os.path.join(self._path_bytes, b'.git', OBJECTDIR)):
  537. self.bare = False
  538. self._controldir = os.path.join(self._path_bytes, b'.git')
  539. elif (os.path.isdir(os.path.join(self._path_bytes, OBJECTDIR)) and
  540. os.path.isdir(os.path.join(self._path_bytes, REFSDIR))):
  541. self.bare = True
  542. self._controldir = self._path_bytes
  543. elif (os.path.isfile(os.path.join(self._path_bytes, b'.git'))):
  544. import re
  545. with open(os.path.join(self._path_bytes, b'.git'), 'rb') as f:
  546. _, gitdir = re.match(b'(gitdir: )(.+$)', f.read()).groups()
  547. self.bare = False
  548. self._controldir = os.path.join(self._path_bytes, gitdir)
  549. else:
  550. raise NotGitRepository(
  551. "No git repository was found at %(path)s" % dict(path=path)
  552. )
  553. object_store = DiskObjectStore(os.path.join(self.controldir(),
  554. OBJECTDIR))
  555. refs = DiskRefsContainer(self.controldir())
  556. BaseRepo.__init__(self, object_store, refs)
  557. self._graftpoints = {}
  558. graft_file = self.get_named_file(os.path.join(b'info', b'grafts'))
  559. if graft_file:
  560. with graft_file:
  561. self._graftpoints.update(parse_graftpoints(graft_file))
  562. graft_file = self.get_named_file(b'shallow')
  563. if graft_file:
  564. with graft_file:
  565. self._graftpoints.update(parse_graftpoints(graft_file))
  566. self.hooks['pre-commit'] = PreCommitShellHook(self.controldir())
  567. self.hooks['commit-msg'] = CommitMsgShellHook(self.controldir())
  568. self.hooks['post-commit'] = PostCommitShellHook(self.controldir())
  569. def controldir(self):
  570. """Return the path of the control directory."""
  571. return self._controldir
  572. def _put_named_file(self, path, contents):
  573. """Write a file to the control dir with the given name and contents.
  574. :param path: The path to the file, relative to the control dir.
  575. :param contents: A string to write to the file.
  576. """
  577. path = path.lstrip(path_sep_bytes)
  578. with GitFile(os.path.join(self.controldir(), path), 'wb') as f:
  579. f.write(contents)
  580. def get_named_file(self, path):
  581. """Get a file from the control dir with a specific name.
  582. Although the filename should be interpreted as a filename relative to
  583. the control dir in a disk-based Repo, the object returned need not be
  584. pointing to a file in that location.
  585. :param path: The path to the file, relative to the control dir.
  586. :return: An open file object, or None if the file does not exist.
  587. """
  588. # TODO(dborowitz): sanitize filenames, since this is used directly by
  589. # the dumb web serving code.
  590. path = path.lstrip(path_sep_bytes)
  591. try:
  592. return open(os.path.join(self.controldir(), path), 'rb')
  593. except (IOError, OSError) as e:
  594. if e.errno == errno.ENOENT:
  595. return None
  596. raise
  597. def index_path(self):
  598. """Return path to the index file."""
  599. return os.path.join(self.controldir(), INDEX_FILENAME)
  600. def open_index(self):
  601. """Open the index for this repository.
  602. :raise NoIndexPresent: If no index is present
  603. :return: The matching `Index`
  604. """
  605. from dulwich.index import Index
  606. if not self.has_index():
  607. raise NoIndexPresent()
  608. return Index(self.index_path())
  609. def has_index(self):
  610. """Check if an index is present."""
  611. # Bare repos must never have index files; non-bare repos may have a
  612. # missing index file, which is treated as empty.
  613. return not self.bare
  614. def stage(self, paths, fsencoding=sys.getfilesystemencoding()):
  615. """Stage a set of paths.
  616. :param paths: List of paths, relative to the repository path
  617. """
  618. if not isinstance(paths, list):
  619. paths = [paths]
  620. from dulwich.index import (
  621. blob_from_path_and_stat,
  622. index_entry_from_stat,
  623. )
  624. index = self.open_index()
  625. for path in paths:
  626. if not isinstance(path, bytes):
  627. disk_path_bytes = path.encode(sys.getfilesystemencoding())
  628. repo_path_bytes = path.encode(fsencoding)
  629. else:
  630. disk_path_bytes, repo_path_bytes = path, path
  631. full_path = os.path.join(self._path_bytes, disk_path_bytes)
  632. try:
  633. st = os.lstat(full_path)
  634. except OSError:
  635. # File no longer exists
  636. try:
  637. del index[repo_path_bytes]
  638. except KeyError:
  639. pass # already removed
  640. else:
  641. blob = blob_from_path_and_stat(full_path, st)
  642. self.object_store.add_object(blob)
  643. index[repo_path_bytes] = index_entry_from_stat(st, blob.id, 0)
  644. index.write()
  645. def clone(self, target_path, mkdir=True, bare=False,
  646. origin=b"origin"):
  647. """Clone this repository.
  648. :param target_path: Target path
  649. :param mkdir: Create the target directory
  650. :param bare: Whether to create a bare repository
  651. :param origin: Base name for refs in target repository
  652. cloned from this repository
  653. :return: Created repository as `Repo`
  654. """
  655. if not bare:
  656. target = self.init(target_path, mkdir=mkdir)
  657. else:
  658. target = self.init_bare(target_path)
  659. self.fetch(target)
  660. target.refs.import_refs(
  661. b'refs/remotes/' + origin, self.refs.as_dict(b'refs/heads'))
  662. target.refs.import_refs(
  663. b'refs/tags', self.refs.as_dict(b'refs/tags'))
  664. try:
  665. target.refs.add_if_new(
  666. b'refs/heads/master',
  667. self.refs[b'refs/heads/master'])
  668. except KeyError:
  669. pass
  670. # Update target head
  671. head, head_sha = self.refs._follow(b'HEAD')
  672. if head is not None and head_sha is not None:
  673. target.refs.set_symbolic_ref(b'HEAD', head)
  674. target[b'HEAD'] = head_sha
  675. if not bare:
  676. # Checkout HEAD to target dir
  677. target.reset_index()
  678. return target
  679. def reset_index(self, tree=None):
  680. """Reset the index back to a specific tree.
  681. :param tree: Tree SHA to reset to, None for current HEAD tree.
  682. """
  683. from dulwich.index import (
  684. build_index_from_tree,
  685. validate_path_element_default,
  686. validate_path_element_ntfs,
  687. )
  688. if tree is None:
  689. tree = self[b'HEAD'].tree
  690. config = self.get_config()
  691. honor_filemode = config.get_boolean('core', 'filemode', os.name != "nt")
  692. if config.get_boolean('core', 'core.protectNTFS', os.name == "nt"):
  693. validate_path_element = validate_path_element_ntfs
  694. else:
  695. validate_path_element = validate_path_element_default
  696. return build_index_from_tree(self._path_bytes, self.index_path(),
  697. self.object_store, tree, honor_filemode=honor_filemode,
  698. validate_path_element=validate_path_element)
  699. def get_config(self):
  700. """Retrieve the config object.
  701. :return: `ConfigFile` object for the ``.git/config`` file.
  702. """
  703. from dulwich.config import ConfigFile
  704. path = os.path.join(self._controldir, b'config')
  705. try:
  706. return ConfigFile.from_path(path)
  707. except (IOError, OSError) as e:
  708. if e.errno != errno.ENOENT:
  709. raise
  710. ret = ConfigFile()
  711. ret.path = path
  712. return ret
  713. def get_description(self):
  714. """Retrieve the description of this repository.
  715. :return: A string describing the repository or None.
  716. """
  717. path = os.path.join(self._controldir, b'description')
  718. try:
  719. with GitFile(path, 'rb') as f:
  720. return f.read()
  721. except (IOError, OSError) as e:
  722. if e.errno != errno.ENOENT:
  723. raise
  724. return None
  725. def __repr__(self):
  726. return "<Repo at %r>" % self.path
  727. def set_description(self, description):
  728. """Set the description for this repository.
  729. :param description: Text to set as description for this repository.
  730. """
  731. self._put_named_file(b'description', description)
  732. @classmethod
  733. def _init_maybe_bare(cls, path, bare):
  734. if not isinstance(path, bytes):
  735. path_bytes = path.encode(sys.getfilesystemencoding())
  736. else:
  737. path_bytes = path
  738. for d in BASE_DIRECTORIES:
  739. os.mkdir(os.path.join(path_bytes, *d))
  740. DiskObjectStore.init(os.path.join(path_bytes, OBJECTDIR))
  741. ret = cls(path)
  742. ret.refs.set_symbolic_ref(b'HEAD', b"refs/heads/master")
  743. ret._init_files(bare)
  744. return ret
  745. @classmethod
  746. def init(cls, path, mkdir=False):
  747. """Create a new repository.
  748. :param path: Path in which to create the repository
  749. :param mkdir: Whether to create the directory
  750. :return: `Repo` instance
  751. """
  752. if not isinstance(path, bytes):
  753. path_bytes = path.encode(sys.getfilesystemencoding())
  754. else:
  755. path_bytes = path
  756. if mkdir:
  757. os.mkdir(path_bytes)
  758. controldir = os.path.join(path_bytes, b'.git')
  759. os.mkdir(controldir)
  760. cls._init_maybe_bare(controldir, False)
  761. return cls(path)
  762. @classmethod
  763. def init_bare(cls, path):
  764. """Create a new bare repository.
  765. ``path`` should already exist and be an emty directory.
  766. :param path: Path to create bare repository in
  767. :return: a `Repo` instance
  768. """
  769. return cls._init_maybe_bare(path, True)
  770. create = init_bare
  771. def close(self):
  772. self.object_store.close()
  773. def __enter__(self):
  774. return self
  775. def __exit__(self, exc_type, exc_val, exc_tb):
  776. self.close()
  777. class MemoryRepo(BaseRepo):
  778. """Repo that stores refs, objects, and named files in memory.
  779. MemoryRepos are always bare: they have no working tree and no index, since
  780. those have a stronger dependency on the filesystem.
  781. """
  782. def __init__(self):
  783. from dulwich.config import ConfigFile
  784. BaseRepo.__init__(self, MemoryObjectStore(), DictRefsContainer({}))
  785. self._named_files = {}
  786. self.bare = True
  787. self._config = ConfigFile()
  788. def _put_named_file(self, path, contents):
  789. """Write a file to the control dir with the given name and contents.
  790. :param path: The path to the file, relative to the control dir.
  791. :param contents: A string to write to the file.
  792. """
  793. self._named_files[path] = contents
  794. def get_named_file(self, path):
  795. """Get a file from the control dir with a specific name.
  796. Although the filename should be interpreted as a filename relative to
  797. the control dir in a disk-baked Repo, the object returned need not be
  798. pointing to a file in that location.
  799. :param path: The path to the file, relative to the control dir.
  800. :return: An open file object, or None if the file does not exist.
  801. """
  802. contents = self._named_files.get(path, None)
  803. if contents is None:
  804. return None
  805. return BytesIO(contents)
  806. def open_index(self):
  807. """Fail to open index for this repo, since it is bare.
  808. :raise NoIndexPresent: Raised when no index is present
  809. """
  810. raise NoIndexPresent()
  811. def get_config(self):
  812. """Retrieve the config object.
  813. :return: `ConfigFile` object.
  814. """
  815. return self._config
  816. def get_description(self):
  817. """Retrieve the repository description.
  818. This defaults to None, for no description.
  819. """
  820. return None
  821. @classmethod
  822. def init_bare(cls, objects, refs):
  823. """Create a new bare repository in memory.
  824. :param objects: Objects for the new repository,
  825. as iterable
  826. :param refs: Refs as dictionary, mapping names
  827. to object SHA1s
  828. """
  829. ret = cls()
  830. for obj in objects:
  831. ret.object_store.add_object(obj)
  832. for refname, sha in refs.items():
  833. ret.refs[refname] = sha
  834. ret._init_files(bare=True)
  835. return ret