repo.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398
  1. # repo.py -- For dealing wih git repositories.
  2. # Copyright (C) 2007 James Westby <jw+debian@jameswestby.net>
  3. # Copyright (C) 2008-2009 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. import os
  22. import stat
  23. from dulwich.errors import (
  24. MissingCommitError,
  25. NotBlobError,
  26. NotCommitError,
  27. NotGitRepository,
  28. NotTreeError,
  29. )
  30. from dulwich.object_store import (
  31. DiskObjectStore,
  32. )
  33. from dulwich.objects import (
  34. Blob,
  35. Commit,
  36. ShaFile,
  37. Tag,
  38. Tree,
  39. )
  40. OBJECTDIR = 'objects'
  41. SYMREF = 'ref: '
  42. REFSDIR = 'refs'
  43. REFSDIR_TAGS = 'tags'
  44. REFSDIR_HEADS = 'heads'
  45. INDEX_FILENAME = "index"
  46. def follow_ref(container, name):
  47. contents = container[name]
  48. if contents.startswith(SYMREF):
  49. ref = contents[len(SYMREF):]
  50. if ref[-1] == '\n':
  51. ref = ref[:-1]
  52. return follow_ref(container, ref)
  53. assert len(contents) == 40, 'Invalid ref in %s' % name
  54. return contents
  55. class RefsContainer(object):
  56. def as_dict(self, base):
  57. raise NotImplementedError(self.as_dict)
  58. def follow(self, name):
  59. return follow_ref(self, name)
  60. def set_ref(self, name, other):
  61. self[name] = "ref: %s\n" % other
  62. def import_refs(self, base, other):
  63. for name, value in other.iteritems():
  64. self["%s/%s" % (base, name)] = value
  65. class DiskRefsContainer(RefsContainer):
  66. def __init__(self, path):
  67. self.path = path
  68. def __repr__(self):
  69. return "%s(%r)" % (self.__class__.__name__, self.path)
  70. def keys(self, base=None):
  71. return list(self.iterkeys(base))
  72. def iterkeys(self, base=None):
  73. if base is not None:
  74. return self.itersubkeys(base)
  75. else:
  76. return self.iterallkeys()
  77. def itersubkeys(self, base):
  78. path = self.refpath(base)
  79. for root, dirs, files in os.walk(path):
  80. dir = root[len(path):].strip("/")
  81. for filename in files:
  82. yield ("%s/%s" % (dir, filename)).strip("/")
  83. def iterallkeys(self):
  84. if os.path.exists(self.refpath("HEAD")):
  85. yield "HEAD"
  86. path = self.refpath("")
  87. for root, dirs, files in os.walk(self.refpath("refs")):
  88. dir = root[len(path):].strip("/")
  89. for filename in files:
  90. yield ("%s/%s" % (dir, filename)).strip("/")
  91. def as_dict(self, base=None, follow=True):
  92. ret = {}
  93. if base is None:
  94. keys = self.iterkeys()
  95. base = ""
  96. else:
  97. keys = self.itersubkeys(base)
  98. for key in keys:
  99. if follow:
  100. try:
  101. ret[key] = self.follow(("%s/%s" % (base, key)).strip("/"))
  102. except KeyError:
  103. continue # Unable to resolve
  104. else:
  105. ret[key] = self[("%s/%s" % (base, key)).strip("/")]
  106. return ret
  107. def refpath(self, name):
  108. if os.path.sep != "/":
  109. name = name.replace("/", os.path.sep)
  110. return os.path.join(self.path, name)
  111. def __getitem__(self, name):
  112. file = self.refpath(name)
  113. if not os.path.exists(file):
  114. raise KeyError(name)
  115. f = open(file, 'rb')
  116. try:
  117. return f.read().strip("\n")
  118. finally:
  119. f.close()
  120. def __setitem__(self, name, ref):
  121. file = self.refpath(name)
  122. dirpath = os.path.dirname(file)
  123. if not os.path.exists(dirpath):
  124. os.makedirs(dirpath)
  125. f = open(file, 'w')
  126. try:
  127. f.write(ref+"\n")
  128. finally:
  129. f.close()
  130. def __delitem__(self, name):
  131. file = self.refpath(name)
  132. if os.path.exists(file):
  133. os.remove(file)
  134. def read_packed_refs(f):
  135. """Read a packed refs file.
  136. Yields tuples with ref names and SHA1s.
  137. :param f: file-like object to read from
  138. """
  139. l = f.readline()
  140. for l in f.readlines():
  141. if l[0] == "#":
  142. # Comment
  143. continue
  144. if l[0] == "^":
  145. # FIXME: Return somehow
  146. continue
  147. yield tuple(l.rstrip("\n").split(" ", 2))
  148. class Repo(object):
  149. """A local git repository.
  150. :ivar refs: Dictionary with the refs in this repository
  151. :ivar object_store: Dictionary-like object for accessing
  152. the objects
  153. """
  154. def __init__(self, root):
  155. if os.path.isdir(os.path.join(root, ".git", OBJECTDIR)):
  156. self.bare = False
  157. self._controldir = os.path.join(root, ".git")
  158. elif os.path.isdir(os.path.join(root, OBJECTDIR)):
  159. self.bare = True
  160. self._controldir = root
  161. else:
  162. raise NotGitRepository(root)
  163. self.path = root
  164. self.refs = DiskRefsContainer(self.controldir())
  165. self.object_store = DiskObjectStore(
  166. os.path.join(self.controldir(), OBJECTDIR))
  167. def controldir(self):
  168. """Return the path of the control directory."""
  169. return self._controldir
  170. def index_path(self):
  171. """Return path to the index file."""
  172. return os.path.join(self.controldir(), INDEX_FILENAME)
  173. def open_index(self):
  174. """Open the index for this repository."""
  175. from dulwich.index import Index
  176. return Index(self.index_path())
  177. def has_index(self):
  178. """Check if an index is present."""
  179. return os.path.exists(self.index_path())
  180. def fetch_objects(self, determine_wants, graph_walker, progress):
  181. """Fetch the missing objects required for a set of revisions.
  182. :param determine_wants: Function that takes a dictionary with heads
  183. and returns the list of heads to fetch.
  184. :param graph_walker: Object that can iterate over the list of revisions
  185. to fetch and has an "ack" method that will be called to acknowledge
  186. that a revision is present.
  187. :param progress: Simple progress function that will be called with
  188. updated progress strings.
  189. :return: tuple with number of objects, iterator over objects
  190. """
  191. wants = determine_wants(self.get_refs())
  192. haves = self.object_store.find_common_revisions(graph_walker)
  193. return self.object_store.iter_shas(
  194. self.object_store.find_missing_objects(haves, wants, progress))
  195. def get_graph_walker(self, heads=None):
  196. if heads is None:
  197. heads = self.refs.as_dict('refs/heads').values()
  198. return self.object_store.get_graph_walker(heads)
  199. def ref(self, name):
  200. """Return the SHA1 a ref is pointing to."""
  201. try:
  202. return self.refs.follow(name)
  203. except KeyError:
  204. return self.get_packed_refs()[name]
  205. def get_refs(self):
  206. """Get dictionary with all refs."""
  207. ret = {}
  208. try:
  209. if self.head():
  210. ret['HEAD'] = self.head()
  211. except KeyError:
  212. pass
  213. ret.update(self.refs.as_dict())
  214. ret.update(self.get_packed_refs())
  215. return ret
  216. def get_packed_refs(self):
  217. """Get contents of the packed-refs file.
  218. :return: Dictionary mapping ref names to SHA1s
  219. :note: Will return an empty dictionary when no packed-refs file is
  220. present.
  221. """
  222. path = os.path.join(self.controldir(), 'packed-refs')
  223. if not os.path.exists(path):
  224. return {}
  225. ret = {}
  226. f = open(path, 'rb')
  227. try:
  228. for entry in read_packed_refs(f):
  229. ret[entry[1]] = entry[0]
  230. return ret
  231. finally:
  232. f.close()
  233. def head(self):
  234. """Return the SHA1 pointed at by HEAD."""
  235. return self.refs.follow('HEAD')
  236. def _get_object(self, sha, cls):
  237. assert len(sha) in (20, 40)
  238. ret = self.get_object(sha)
  239. if ret._type != cls._type:
  240. if cls is Commit:
  241. raise NotCommitError(ret)
  242. elif cls is Blob:
  243. raise NotBlobError(ret)
  244. elif cls is Tree:
  245. raise NotTreeError(ret)
  246. else:
  247. raise Exception("Type invalid: %r != %r" % (ret._type, cls._type))
  248. return ret
  249. def get_object(self, sha):
  250. return self.object_store[sha]
  251. def get_parents(self, sha):
  252. return self.commit(sha).parents
  253. def commit(self, sha):
  254. return self._get_object(sha, Commit)
  255. def tree(self, sha):
  256. return self._get_object(sha, Tree)
  257. def tag(self, sha):
  258. return self._get_object(sha, Tag)
  259. def get_blob(self, sha):
  260. return self._get_object(sha, Blob)
  261. def revision_history(self, head):
  262. """Returns a list of the commits reachable from head.
  263. Returns a list of commit objects. the first of which will be the commit
  264. of head, then following theat will be the parents.
  265. Raises NotCommitError if any no commits are referenced, including if the
  266. head parameter isn't the sha of a commit.
  267. XXX: work out how to handle merges.
  268. """
  269. # We build the list backwards, as parents are more likely to be older
  270. # than children
  271. pending_commits = [head]
  272. history = []
  273. while pending_commits != []:
  274. head = pending_commits.pop(0)
  275. try:
  276. commit = self.commit(head)
  277. except KeyError:
  278. raise MissingCommitError(head)
  279. if commit in history:
  280. continue
  281. i = 0
  282. for known_commit in history:
  283. if known_commit.commit_time > commit.commit_time:
  284. break
  285. i += 1
  286. history.insert(i, commit)
  287. parents = commit.parents
  288. pending_commits += parents
  289. history.reverse()
  290. return history
  291. def __repr__(self):
  292. return "<Repo at %r>" % self.path
  293. def __getitem__(self, name):
  294. if len(name) in (20, 40):
  295. return self.object_store[name]
  296. return self.object_store[self.refs[name]]
  297. def __setitem__(self, name, value):
  298. if name.startswith("refs/") or name == "HEAD":
  299. if isinstance(value, ShaFile):
  300. self.refs[name] = value.id
  301. elif isinstance(value, str):
  302. self.refs[name] = value
  303. else:
  304. raise TypeError(value)
  305. raise ValueError(name)
  306. def __delitem__(self, name):
  307. if name.startswith("refs") or name == "HEAD":
  308. del self.refs[name]
  309. raise ValueError(name)
  310. @classmethod
  311. def init(cls, path, mkdir=True):
  312. controldir = os.path.join(path, ".git")
  313. os.mkdir(controldir)
  314. cls.init_bare(controldir)
  315. return cls(path)
  316. @classmethod
  317. def init_bare(cls, path, mkdir=True):
  318. for d in [[OBJECTDIR],
  319. [OBJECTDIR, "info"],
  320. [OBJECTDIR, "pack"],
  321. ["branches"],
  322. [REFSDIR],
  323. [REFSDIR, REFSDIR_TAGS],
  324. [REFSDIR, REFSDIR_HEADS],
  325. ["hooks"],
  326. ["info"]]:
  327. os.mkdir(os.path.join(path, *d))
  328. ret = cls(path)
  329. ret.refs.set_ref("HEAD", "refs/heads/master")
  330. open(os.path.join(path, 'description'), 'w').write("Unnamed repository")
  331. open(os.path.join(path, 'info', 'excludes'), 'w').write("")
  332. return ret
  333. create = init_bare