repo.py 37 KB

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