repo.py 32 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975
  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. from dulwich.errors import (
  29. NoIndexPresent,
  30. NotBlobError,
  31. NotCommitError,
  32. NotGitRepository,
  33. NotTreeError,
  34. NotTagError,
  35. CommitError,
  36. RefFormatError,
  37. HookError,
  38. )
  39. from dulwich.file import (
  40. GitFile,
  41. )
  42. from dulwich.object_store import (
  43. DiskObjectStore,
  44. MemoryObjectStore,
  45. ObjectStoreGraphWalker,
  46. )
  47. from dulwich.objects import (
  48. check_hexsha,
  49. Blob,
  50. Commit,
  51. ShaFile,
  52. Tag,
  53. Tree,
  54. )
  55. from dulwich.hooks import (
  56. PreCommitShellHook,
  57. PostCommitShellHook,
  58. CommitMsgShellHook,
  59. )
  60. from dulwich.refs import (
  61. check_ref_format,
  62. RefsContainer,
  63. DictRefsContainer,
  64. InfoRefsContainer,
  65. DiskRefsContainer,
  66. read_packed_refs,
  67. read_packed_refs_with_peeled,
  68. write_packed_refs,
  69. SYMREF,
  70. )
  71. import warnings
  72. OBJECTDIR = 'objects'
  73. REFSDIR = 'refs'
  74. REFSDIR_TAGS = 'tags'
  75. REFSDIR_HEADS = 'heads'
  76. INDEX_FILENAME = "index"
  77. BASE_DIRECTORIES = [
  78. ["branches"],
  79. [REFSDIR],
  80. [REFSDIR, REFSDIR_TAGS],
  81. [REFSDIR, REFSDIR_HEADS],
  82. ["hooks"],
  83. ["info"]
  84. ]
  85. def parse_graftpoints(graftpoints):
  86. """Convert a list of graftpoints into a dict
  87. :param graftpoints: Iterator of graftpoint lines
  88. Each line is formatted as:
  89. <commit sha1> <parent sha1> [<parent sha1>]*
  90. Resulting dictionary is:
  91. <commit sha1>: [<parent sha1>*]
  92. https://git.wiki.kernel.org/index.php/GraftPoint
  93. """
  94. grafts = {}
  95. for l in graftpoints:
  96. raw_graft = l.split(None, 1)
  97. commit = raw_graft[0]
  98. if len(raw_graft) == 2:
  99. parents = raw_graft[1].split()
  100. else:
  101. parents = []
  102. for sha in [commit] + parents:
  103. check_hexsha(sha, 'Invalid graftpoint')
  104. grafts[commit] = parents
  105. return grafts
  106. def serialize_graftpoints(graftpoints):
  107. """Convert a dictionary of grafts into string
  108. The graft dictionary is:
  109. <commit sha1>: [<parent sha1>*]
  110. Each line is formatted as:
  111. <commit sha1> <parent sha1> [<parent sha1>]*
  112. https://git.wiki.kernel.org/index.php/GraftPoint
  113. """
  114. graft_lines = []
  115. for commit, parents in graftpoints.iteritems():
  116. if parents:
  117. graft_lines.append('%s %s' % (commit, ' '.join(parents)))
  118. else:
  119. graft_lines.append(commit)
  120. return '\n'.join(graft_lines)
  121. class BaseRepo(object):
  122. """Base class for a git repository.
  123. :ivar object_store: Dictionary-like object for accessing
  124. the objects
  125. :ivar refs: Dictionary-like object with the refs in this
  126. repository
  127. """
  128. def __init__(self, object_store, refs):
  129. """Open a repository.
  130. This shouldn't be called directly, but rather through one of the
  131. base classes, such as MemoryRepo or Repo.
  132. :param object_store: Object store to use
  133. :param refs: Refs container to use
  134. """
  135. self.object_store = object_store
  136. self.refs = refs
  137. self._graftpoints = {}
  138. self.hooks = {}
  139. def _init_files(self, bare):
  140. """Initialize a default set of named files."""
  141. from dulwich.config import ConfigFile
  142. self._put_named_file('description', "Unnamed repository")
  143. f = BytesIO()
  144. cf = ConfigFile()
  145. cf.set("core", "repositoryformatversion", "0")
  146. cf.set("core", "filemode", "true")
  147. cf.set("core", "bare", str(bare).lower())
  148. cf.set("core", "logallrefupdates", "true")
  149. cf.write_to_file(f)
  150. self._put_named_file('config', f.getvalue())
  151. self._put_named_file(os.path.join('info', 'exclude'), '')
  152. def get_named_file(self, path):
  153. """Get a file from the control dir with a specific name.
  154. Although the filename should be interpreted as a filename relative to
  155. the control dir in a disk-based Repo, the object returned need not be
  156. pointing to a file in that location.
  157. :param path: The path to the file, relative to the control dir.
  158. :return: An open file object, or None if the file does not exist.
  159. """
  160. raise NotImplementedError(self.get_named_file)
  161. def _put_named_file(self, path, contents):
  162. """Write a file to the control dir with the given name and contents.
  163. :param path: The path to the file, relative to the control dir.
  164. :param contents: A string to write to the file.
  165. """
  166. raise NotImplementedError(self._put_named_file)
  167. def open_index(self):
  168. """Open the index for this repository.
  169. :raise NoIndexPresent: If no index is present
  170. :return: The matching `Index`
  171. """
  172. raise NotImplementedError(self.open_index)
  173. def fetch(self, target, determine_wants=None, progress=None):
  174. """Fetch objects into another repository.
  175. :param target: The target repository
  176. :param determine_wants: Optional function to determine what refs to
  177. fetch.
  178. :param progress: Optional progress function
  179. :return: The local refs
  180. """
  181. if determine_wants is None:
  182. determine_wants = target.object_store.determine_wants_all
  183. target.object_store.add_objects(
  184. self.fetch_objects(determine_wants, target.get_graph_walker(),
  185. progress))
  186. return self.get_refs()
  187. def fetch_objects(self, determine_wants, graph_walker, progress,
  188. get_tagged=None):
  189. """Fetch the missing objects required for a set of revisions.
  190. :param determine_wants: Function that takes a dictionary with heads
  191. and returns the list of heads to fetch.
  192. :param graph_walker: Object that can iterate over the list of revisions
  193. to fetch and has an "ack" method that will be called to acknowledge
  194. that a revision is present.
  195. :param progress: Simple progress function that will be called with
  196. updated progress strings.
  197. :param get_tagged: Function that returns a dict of pointed-to sha -> tag
  198. sha for including tags.
  199. :return: iterator over objects, with __len__ implemented
  200. """
  201. wants = determine_wants(self.get_refs())
  202. if not isinstance(wants, list):
  203. raise TypeError("determine_wants() did not return a list")
  204. shallows = getattr(graph_walker, 'shallow', frozenset())
  205. unshallows = getattr(graph_walker, 'unshallow', frozenset())
  206. if wants == []:
  207. # TODO(dborowitz): find a way to short-circuit that doesn't change
  208. # this interface.
  209. if shallows or unshallows:
  210. # Do not send a pack in shallow short-circuit path
  211. return None
  212. return []
  213. haves = self.object_store.find_common_revisions(graph_walker)
  214. # Deal with shallow requests separately because the haves do
  215. # not reflect what objects are missing
  216. if shallows or unshallows:
  217. haves = [] # TODO: filter the haves commits from iter_shas.
  218. # the specific commits aren't missing.
  219. def get_parents(commit):
  220. if commit.id in shallows:
  221. return []
  222. return self.get_parents(commit.id, commit)
  223. return self.object_store.iter_shas(
  224. self.object_store.find_missing_objects(
  225. haves, wants, progress,
  226. get_tagged,
  227. get_parents=get_parents))
  228. def get_graph_walker(self, heads=None):
  229. """Retrieve a graph walker.
  230. A graph walker is used by a remote repository (or proxy)
  231. to find out which objects are present in this repository.
  232. :param heads: Repository heads to use (optional)
  233. :return: A graph walker object
  234. """
  235. if heads is None:
  236. heads = self.refs.as_dict('refs/heads').values()
  237. return ObjectStoreGraphWalker(heads, self.get_parents)
  238. def get_refs(self):
  239. """Get dictionary with all refs.
  240. :return: A ``dict`` mapping ref names to SHA1s
  241. """
  242. return self.refs.as_dict()
  243. def head(self):
  244. """Return the SHA1 pointed at by HEAD."""
  245. return self.refs['HEAD']
  246. def _get_object(self, sha, cls):
  247. assert len(sha) in (20, 40)
  248. ret = self.get_object(sha)
  249. if not isinstance(ret, cls):
  250. if cls is Commit:
  251. raise NotCommitError(ret)
  252. elif cls is Blob:
  253. raise NotBlobError(ret)
  254. elif cls is Tree:
  255. raise NotTreeError(ret)
  256. elif cls is Tag:
  257. raise NotTagError(ret)
  258. else:
  259. raise Exception("Type invalid: %r != %r" % (
  260. ret.type_name, cls.type_name))
  261. return ret
  262. def get_object(self, sha):
  263. """Retrieve the object with the specified SHA.
  264. :param sha: SHA to retrieve
  265. :return: A ShaFile object
  266. :raise KeyError: when the object can not be found
  267. """
  268. return self.object_store[sha]
  269. def get_parents(self, sha, commit=None):
  270. """Retrieve the parents of a specific commit.
  271. If the specific commit is a graftpoint, the graft parents
  272. will be returned instead.
  273. :param sha: SHA of the commit for which to retrieve the parents
  274. :param commit: Optional commit matching the sha
  275. :return: List of parents
  276. """
  277. try:
  278. return self._graftpoints[sha]
  279. except KeyError:
  280. if commit is None:
  281. commit = self[sha]
  282. return commit.parents
  283. def get_config(self):
  284. """Retrieve the config object.
  285. :return: `ConfigFile` object for the ``.git/config`` file.
  286. """
  287. raise NotImplementedError(self.get_config)
  288. def get_description(self):
  289. """Retrieve the description for this repository.
  290. :return: String with the description of the repository
  291. as set by the user.
  292. """
  293. raise NotImplementedError(self.get_description)
  294. def set_description(self, description):
  295. """Set the description for this repository.
  296. :param description: Text to set as description for this repository.
  297. """
  298. raise NotImplementedError(self.set_description)
  299. def get_config_stack(self):
  300. """Return a config stack for this repository.
  301. This stack accesses the configuration for both this repository
  302. itself (.git/config) and the global configuration, which usually
  303. lives in ~/.gitconfig.
  304. :return: `Config` instance for this repository
  305. """
  306. from dulwich.config import StackedConfig
  307. backends = [self.get_config()] + StackedConfig.default_backends()
  308. return StackedConfig(backends, writable=backends[0])
  309. def get_peeled(self, ref):
  310. """Get the peeled value of a ref.
  311. :param ref: The refname to peel.
  312. :return: The fully-peeled SHA1 of a tag object, after peeling all
  313. intermediate tags; if the original ref does not point to a tag, this
  314. will equal the original SHA1.
  315. """
  316. cached = self.refs.get_peeled(ref)
  317. if cached is not None:
  318. return cached
  319. return self.object_store.peel_sha(self.refs[ref]).id
  320. def get_walker(self, include=None, *args, **kwargs):
  321. """Obtain a walker for this repository.
  322. :param include: Iterable of SHAs of commits to include along with their
  323. ancestors. Defaults to [HEAD]
  324. :param exclude: Iterable of SHAs of commits to exclude along with their
  325. ancestors, overriding includes.
  326. :param order: ORDER_* constant specifying the order of results. Anything
  327. other than ORDER_DATE may result in O(n) memory usage.
  328. :param reverse: If True, reverse the order of output, requiring O(n)
  329. memory.
  330. :param max_entries: The maximum number of entries to yield, or None for
  331. no limit.
  332. :param paths: Iterable of file or subtree paths to show entries for.
  333. :param rename_detector: diff.RenameDetector object for detecting
  334. renames.
  335. :param follow: If True, follow path across renames/copies. Forces a
  336. default rename_detector.
  337. :param since: Timestamp to list commits after.
  338. :param until: Timestamp to list commits before.
  339. :param queue_cls: A class to use for a queue of commits, supporting the
  340. iterator protocol. The constructor takes a single argument, the
  341. Walker.
  342. :return: A `Walker` object
  343. """
  344. from dulwich.walk import Walker
  345. if include is None:
  346. include = [self.head()]
  347. if isinstance(include, str):
  348. include = [include]
  349. kwargs['get_parents'] = lambda commit: self.get_parents(commit.id, commit)
  350. return Walker(self.object_store, include, *args, **kwargs)
  351. def __getitem__(self, name):
  352. """Retrieve a Git object by SHA1 or ref.
  353. :param name: A Git object SHA1 or a ref name
  354. :return: A `ShaFile` object, such as a Commit or Blob
  355. :raise KeyError: when the specified ref or object does not exist
  356. """
  357. if not isinstance(name, str):
  358. raise TypeError("'name' must be bytestring, not %.80s" %
  359. type(name).__name__)
  360. if len(name) in (20, 40):
  361. try:
  362. return self.object_store[name]
  363. except (KeyError, ValueError):
  364. pass
  365. try:
  366. return self.object_store[self.refs[name]]
  367. except RefFormatError:
  368. raise KeyError(name)
  369. def __contains__(self, name):
  370. """Check if a specific Git object or ref is present.
  371. :param name: Git object SHA1 or ref name
  372. """
  373. if len(name) in (20, 40):
  374. return name in self.object_store or name in self.refs
  375. else:
  376. return name in self.refs
  377. def __setitem__(self, name, value):
  378. """Set a ref.
  379. :param name: ref name
  380. :param value: Ref value - either a ShaFile object, or a hex sha
  381. """
  382. if name.startswith("refs/") or name == "HEAD":
  383. if isinstance(value, ShaFile):
  384. self.refs[name] = value.id
  385. elif isinstance(value, str):
  386. self.refs[name] = value
  387. else:
  388. raise TypeError(value)
  389. else:
  390. raise ValueError(name)
  391. def __delitem__(self, name):
  392. """Remove a ref.
  393. :param name: Name of the ref to remove
  394. """
  395. if name.startswith("refs/") or name == "HEAD":
  396. del self.refs[name]
  397. else:
  398. raise ValueError(name)
  399. def _get_user_identity(self):
  400. """Determine the identity to use for new commits.
  401. """
  402. config = self.get_config_stack()
  403. return "%s <%s>" % (
  404. config.get(("user", ), "name"),
  405. config.get(("user", ), "email"))
  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.iteritems():
  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='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. class Repo(BaseRepo):
  524. """A git repository backed by local disk.
  525. To open an existing repository, call the contructor with
  526. the path of the repository.
  527. To create a new repository, use the Repo.init class method.
  528. """
  529. def __init__(self, root):
  530. if os.path.isdir(os.path.join(root, ".git", OBJECTDIR)):
  531. self.bare = False
  532. self._controldir = os.path.join(root, ".git")
  533. elif (os.path.isdir(os.path.join(root, OBJECTDIR)) and
  534. os.path.isdir(os.path.join(root, REFSDIR))):
  535. self.bare = True
  536. self._controldir = root
  537. elif (os.path.isfile(os.path.join(root, ".git"))):
  538. import re
  539. f = open(os.path.join(root, ".git"), 'r')
  540. try:
  541. _, path = re.match('(gitdir: )(.+$)', f.read()).groups()
  542. finally:
  543. f.close()
  544. self.bare = False
  545. self._controldir = os.path.join(root, path)
  546. else:
  547. raise NotGitRepository(
  548. "No git repository was found at %(path)s" % dict(path=root)
  549. )
  550. self.path = root
  551. object_store = DiskObjectStore(os.path.join(self.controldir(),
  552. OBJECTDIR))
  553. refs = DiskRefsContainer(self.controldir())
  554. BaseRepo.__init__(self, object_store, refs)
  555. self._graftpoints = {}
  556. graft_file = self.get_named_file(os.path.join("info", "grafts"))
  557. if graft_file:
  558. with graft_file:
  559. self._graftpoints.update(parse_graftpoints(graft_file))
  560. graft_file = self.get_named_file("shallow")
  561. if graft_file:
  562. with graft_file:
  563. self._graftpoints.update(parse_graftpoints(graft_file))
  564. self.hooks['pre-commit'] = PreCommitShellHook(self.controldir())
  565. self.hooks['commit-msg'] = CommitMsgShellHook(self.controldir())
  566. self.hooks['post-commit'] = PostCommitShellHook(self.controldir())
  567. def controldir(self):
  568. """Return the path of the control directory."""
  569. return self._controldir
  570. def _put_named_file(self, path, contents):
  571. """Write a file to the control dir with the given name and contents.
  572. :param path: The path to the file, relative to the control dir.
  573. :param contents: A string to write to the file.
  574. """
  575. path = path.lstrip(os.path.sep)
  576. f = GitFile(os.path.join(self.controldir(), path), 'wb')
  577. try:
  578. f.write(contents)
  579. finally:
  580. f.close()
  581. def get_named_file(self, path):
  582. """Get a file from the control dir with a specific name.
  583. Although the filename should be interpreted as a filename relative to
  584. the control dir in a disk-based Repo, the object returned need not be
  585. pointing to a file in that location.
  586. :param path: The path to the file, relative to the control dir.
  587. :return: An open file object, or None if the file does not exist.
  588. """
  589. # TODO(dborowitz): sanitize filenames, since this is used directly by
  590. # the dumb web serving code.
  591. path = path.lstrip(os.path.sep)
  592. try:
  593. return open(os.path.join(self.controldir(), path), 'rb')
  594. except (IOError, OSError) as e:
  595. if e.errno == errno.ENOENT:
  596. return None
  597. raise
  598. def index_path(self):
  599. """Return path to the index file."""
  600. return os.path.join(self.controldir(), INDEX_FILENAME)
  601. def open_index(self):
  602. """Open the index for this repository.
  603. :raise NoIndexPresent: If no index is present
  604. :return: The matching `Index`
  605. """
  606. from dulwich.index import Index
  607. if not self.has_index():
  608. raise NoIndexPresent()
  609. return Index(self.index_path())
  610. def has_index(self):
  611. """Check if an index is present."""
  612. # Bare repos must never have index files; non-bare repos may have a
  613. # missing index file, which is treated as empty.
  614. return not self.bare
  615. def stage(self, paths):
  616. """Stage a set of paths.
  617. :param paths: List of paths, relative to the repository path
  618. """
  619. if isinstance(paths, basestring):
  620. paths = [paths]
  621. from dulwich.index import (
  622. blob_from_path_and_stat,
  623. index_entry_from_stat,
  624. )
  625. index = self.open_index()
  626. for path in paths:
  627. full_path = os.path.join(self.path, path)
  628. try:
  629. st = os.lstat(full_path)
  630. except OSError:
  631. # File no longer exists
  632. try:
  633. del index[path]
  634. except KeyError:
  635. pass # already removed
  636. else:
  637. blob = blob_from_path_and_stat(full_path, st)
  638. self.object_store.add_object(blob)
  639. index[path] = index_entry_from_stat(st, blob.id, 0)
  640. index.write()
  641. def clone(self, target_path, mkdir=True, bare=False,
  642. origin="origin"):
  643. """Clone this repository.
  644. :param target_path: Target path
  645. :param mkdir: Create the target directory
  646. :param bare: Whether to create a bare repository
  647. :param origin: Base name for refs in target repository
  648. cloned from this repository
  649. :return: Created repository as `Repo`
  650. """
  651. if not bare:
  652. target = self.init(target_path, mkdir=mkdir)
  653. else:
  654. target = self.init_bare(target_path)
  655. self.fetch(target)
  656. target.refs.import_refs(
  657. 'refs/remotes/' + origin, self.refs.as_dict('refs/heads'))
  658. target.refs.import_refs(
  659. 'refs/tags', self.refs.as_dict('refs/tags'))
  660. try:
  661. target.refs.add_if_new(
  662. 'refs/heads/master',
  663. self.refs['refs/heads/master'])
  664. except KeyError:
  665. pass
  666. # Update target head
  667. head, head_sha = self.refs._follow('HEAD')
  668. if head is not None and head_sha is not None:
  669. target.refs.set_symbolic_ref('HEAD', head)
  670. target['HEAD'] = head_sha
  671. if not bare:
  672. # Checkout HEAD to target dir
  673. target._build_tree()
  674. return target
  675. def _build_tree(self):
  676. from dulwich.index import build_index_from_tree
  677. config = self.get_config()
  678. honor_filemode = config.get_boolean('core', 'filemode', os.name != "nt")
  679. return build_index_from_tree(self.path, self.index_path(),
  680. self.object_store, self['HEAD'].tree,
  681. honor_filemode=honor_filemode)
  682. def get_config(self):
  683. """Retrieve the config object.
  684. :return: `ConfigFile` object for the ``.git/config`` file.
  685. """
  686. from dulwich.config import ConfigFile
  687. path = os.path.join(self._controldir, 'config')
  688. try:
  689. return ConfigFile.from_path(path)
  690. except (IOError, OSError) as e:
  691. if e.errno != errno.ENOENT:
  692. raise
  693. ret = ConfigFile()
  694. ret.path = path
  695. return ret
  696. def get_description(self):
  697. """Retrieve the description of this repository.
  698. :return: A string describing the repository or None.
  699. """
  700. path = os.path.join(self._controldir, 'description')
  701. try:
  702. f = GitFile(path, 'rb')
  703. try:
  704. return f.read()
  705. finally:
  706. f.close()
  707. except (IOError, OSError) as e:
  708. if e.errno != errno.ENOENT:
  709. raise
  710. return None
  711. def __repr__(self):
  712. return "<Repo at %r>" % self.path
  713. def set_description(self, description):
  714. """Set the description for this repository.
  715. :param description: Text to set as description for this repository.
  716. """
  717. path = os.path.join(self._controldir, 'description')
  718. f = open(path, 'w')
  719. try:
  720. f.write(description)
  721. finally:
  722. f.close()
  723. @classmethod
  724. def _init_maybe_bare(cls, path, bare):
  725. for d in BASE_DIRECTORIES:
  726. os.mkdir(os.path.join(path, *d))
  727. DiskObjectStore.init(os.path.join(path, OBJECTDIR))
  728. ret = cls(path)
  729. ret.refs.set_symbolic_ref("HEAD", "refs/heads/master")
  730. ret._init_files(bare)
  731. return ret
  732. @classmethod
  733. def init(cls, path, mkdir=False):
  734. """Create a new repository.
  735. :param path: Path in which to create the repository
  736. :param mkdir: Whether to create the directory
  737. :return: `Repo` instance
  738. """
  739. if mkdir:
  740. os.mkdir(path)
  741. controldir = os.path.join(path, ".git")
  742. os.mkdir(controldir)
  743. cls._init_maybe_bare(controldir, False)
  744. return cls(path)
  745. @classmethod
  746. def init_bare(cls, path):
  747. """Create a new bare repository.
  748. ``path`` should already exist and be an emty directory.
  749. :param path: Path to create bare repository in
  750. :return: a `Repo` instance
  751. """
  752. return cls._init_maybe_bare(path, True)
  753. create = init_bare
  754. class MemoryRepo(BaseRepo):
  755. """Repo that stores refs, objects, and named files in memory.
  756. MemoryRepos are always bare: they have no working tree and no index, since
  757. those have a stronger dependency on the filesystem.
  758. """
  759. def __init__(self):
  760. from dulwich.config import ConfigFile
  761. BaseRepo.__init__(self, MemoryObjectStore(), DictRefsContainer({}))
  762. self._named_files = {}
  763. self.bare = True
  764. self._config = ConfigFile()
  765. def _put_named_file(self, path, contents):
  766. """Write a file to the control dir with the given name and contents.
  767. :param path: The path to the file, relative to the control dir.
  768. :param contents: A string to write to the file.
  769. """
  770. self._named_files[path] = contents
  771. def get_named_file(self, path):
  772. """Get a file from the control dir with a specific name.
  773. Although the filename should be interpreted as a filename relative to
  774. the control dir in a disk-baked Repo, the object returned need not be
  775. pointing to a file in that location.
  776. :param path: The path to the file, relative to the control dir.
  777. :return: An open file object, or None if the file does not exist.
  778. """
  779. contents = self._named_files.get(path, None)
  780. if contents is None:
  781. return None
  782. return BytesIO(contents)
  783. def open_index(self):
  784. """Fail to open index for this repo, since it is bare.
  785. :raise NoIndexPresent: Raised when no index is present
  786. """
  787. raise NoIndexPresent()
  788. def get_config(self):
  789. """Retrieve the config object.
  790. :return: `ConfigFile` object.
  791. """
  792. return self._config
  793. def get_description(self):
  794. """Retrieve the repository description.
  795. This defaults to None, for no description.
  796. """
  797. return None
  798. @classmethod
  799. def init_bare(cls, objects, refs):
  800. """Create a new bare repository in memory.
  801. :param objects: Objects for the new repository,
  802. as iterable
  803. :param refs: Refs as dictionary, mapping names
  804. to object SHA1s
  805. """
  806. ret = cls()
  807. for obj in objects:
  808. ret.object_store.add_object(obj)
  809. for refname, sha in refs.iteritems():
  810. ret.refs[refname] = sha
  811. ret._init_files(bare=True)
  812. return ret