repo.py 31 KB

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