repo.py 39 KB

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