repo.py 8.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290
  1. # repo.py -- For dealing wih git repositories.
  2. # Copyright (C) 2007 James Westby <jw+debian@jameswestby.net>
  3. # Copyright (C) 2008 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.
  9. #
  10. # This program is distributed in the hope that it will be useful,
  11. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  12. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  13. # GNU General Public License for more details.
  14. #
  15. # You should have received a copy of the GNU General Public License
  16. # along with this program; if not, write to the Free Software
  17. # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
  18. # MA 02110-1301, USA.
  19. import os
  20. from commit import Commit
  21. from errors import (
  22. MissingCommitError,
  23. NotBlobError,
  24. NotCommitError,
  25. NotGitRepository,
  26. NotTreeError,
  27. )
  28. from object_store import ObjectStore
  29. from objects import (
  30. ShaFile,
  31. Commit,
  32. Tree,
  33. Blob,
  34. )
  35. import tempfile
  36. OBJECTDIR = 'objects'
  37. SYMREF = 'ref: '
  38. class Tag(object):
  39. def __init__(self, name, ref):
  40. self.name = name
  41. self.ref = ref
  42. class Repo(object):
  43. ref_locs = ['', 'refs', 'refs/tags', 'refs/heads', 'refs/remotes']
  44. def __init__(self, root):
  45. if os.path.isdir(os.path.join(root, ".git", "objects")):
  46. self.bare = False
  47. self._controldir = os.path.join(root, ".git")
  48. elif os.path.isdir(os.path.join(root, "objects")):
  49. self.bare = True
  50. self._controldir = root
  51. else:
  52. raise NotGitRepository(root)
  53. self.path = root
  54. self.tags = [Tag(name, ref) for name, ref in self.get_tags().items()]
  55. self._object_store = None
  56. def controldir(self):
  57. return self._controldir
  58. def find_missing_objects(self, determine_wants, graph_walker, progress):
  59. """Fetch the missing objects required for a set of revisions.
  60. :param determine_wants: Function that takes a dictionary with heads
  61. and returns the list of heads to fetch.
  62. :param graph_walker: Object that can iterate over the list of revisions
  63. to fetch and has an "ack" method that will be called to acknowledge
  64. that a revision is present.
  65. :param progress: Simple progress function that will be called with
  66. updated progress strings.
  67. """
  68. wants = determine_wants(self.get_refs())
  69. commits_to_send = set(wants)
  70. sha_done = set()
  71. ref = graph_walker.next()
  72. while ref:
  73. sha_done.add(ref)
  74. if ref in self.object_store:
  75. graph_walker.ack(ref)
  76. ref = graph_walker.next()
  77. while commits_to_send:
  78. sha = commits_to_send.pop()
  79. if sha in sha_done:
  80. continue
  81. c = self.commit(sha)
  82. assert isinstance(c, Commit)
  83. sha_done.add(sha)
  84. commits_to_send.update([p for p in c.parents if not p in sha_done])
  85. def parse_tree(tree, sha_done):
  86. for mode, name, x in tree.entries():
  87. if not x in sha_done:
  88. try:
  89. t = self.tree(x)
  90. sha_done.add(x)
  91. parse_tree(t, sha_done)
  92. except:
  93. sha_done.add(x)
  94. treesha = c.tree
  95. if treesha not in sha_done:
  96. t = self.tree(treesha)
  97. sha_done.add(treesha)
  98. parse_tree(t, sha_done)
  99. progress("counting objects: %d\r" % len(sha_done))
  100. return sha_done
  101. def fetch_objects(self, determine_wants, graph_walker, progress):
  102. """Fetch the missing objects required for a set of revisions.
  103. :param determine_wants: Function that takes a dictionary with heads
  104. and returns the list of heads to fetch.
  105. :param graph_walker: Object that can iterate over the list of revisions
  106. to fetch and has an "ack" method that will be called to acknowledge
  107. that a revision is present.
  108. :param progress: Simple progress function that will be called with
  109. updated progress strings.
  110. """
  111. shas = self.find_missing_objects(determine_wants, graph_walker, progress)
  112. for sha in shas:
  113. yield self.get_object(sha)
  114. def object_dir(self):
  115. return os.path.join(self.controldir(), OBJECTDIR)
  116. @property
  117. def object_store(self):
  118. if self._object_store is None:
  119. self._object_store = ObjectStore(self.object_dir())
  120. return self._object_store
  121. def pack_dir(self):
  122. return os.path.join(self.object_dir(), PACKDIR)
  123. def _get_ref(self, file):
  124. f = open(file, 'rb')
  125. try:
  126. contents = f.read()
  127. if contents.startswith(SYMREF):
  128. ref = contents[len(SYMREF):]
  129. if ref[-1] == '\n':
  130. ref = ref[:-1]
  131. return self.ref(ref)
  132. assert len(contents) == 41, 'Invalid ref in %s' % file
  133. return contents[:-1]
  134. finally:
  135. f.close()
  136. def ref(self, name):
  137. for dir in self.ref_locs:
  138. file = os.path.join(self.controldir(), dir, name)
  139. if os.path.exists(file):
  140. return self._get_ref(file)
  141. def get_refs(self):
  142. ret = {}
  143. if self.head():
  144. ret['HEAD'] = self.head()
  145. for dir in ["refs/heads", "refs/tags"]:
  146. for name in os.listdir(os.path.join(self.controldir(), dir)):
  147. path = os.path.join(self.controldir(), dir, name)
  148. if os.path.isfile(path):
  149. ret["/".join([dir, name])] = self._get_ref(path)
  150. return ret
  151. def set_ref(self, name, value):
  152. file = os.path.join(self.controldir(), name)
  153. open(file, 'w').write(value+"\n")
  154. def remove_ref(self, name):
  155. file = os.path.join(self.controldir(), name)
  156. if os.path.exists(file):
  157. os.remove(file)
  158. return
  159. def get_tags(self):
  160. ret = {}
  161. for root, dirs, files in os.walk(os.path.join(self.controldir(), 'refs', 'tags')):
  162. for name in files:
  163. ret[name] = self._get_ref(os.path.join(root, name))
  164. return ret
  165. def heads(self):
  166. ret = {}
  167. for root, dirs, files in os.walk(os.path.join(self.controldir(), 'refs', 'heads')):
  168. for name in files:
  169. ret[name] = self._get_ref(os.path.join(root, name))
  170. return ret
  171. def head(self):
  172. return self.ref('HEAD')
  173. def _get_object(self, sha, cls):
  174. assert len(sha) in (20, 40)
  175. ret = self.get_object(sha)
  176. if ret._type != cls._type:
  177. if cls is Commit:
  178. raise NotCommitError(ret)
  179. elif cls is Blob:
  180. raise NotBlobError(ret)
  181. elif cls is Tree:
  182. raise NotTreeError(ret)
  183. else:
  184. raise Exception("Type invalid: %r != %r" % (ret._type, cls._type))
  185. return ret
  186. def get_object(self, sha):
  187. return self.object_store[sha]
  188. def get_parents(self, sha):
  189. return self.commit(sha).parents
  190. def commit(self, sha):
  191. return self._get_object(sha, Commit)
  192. def tree(self, sha):
  193. return self._get_object(sha, Tree)
  194. def get_blob(self, sha):
  195. return self._get_object(sha, Blob)
  196. def revision_history(self, head):
  197. """Returns a list of the commits reachable from head.
  198. Returns a list of commit objects. the first of which will be the commit
  199. of head, then following theat will be the parents.
  200. Raises NotCommitError if any no commits are referenced, including if the
  201. head parameter isn't the sha of a commit.
  202. XXX: work out how to handle merges.
  203. """
  204. # We build the list backwards, as parents are more likely to be older
  205. # than children
  206. pending_commits = [head]
  207. history = []
  208. while pending_commits != []:
  209. head = pending_commits.pop(0)
  210. try:
  211. commit = self.commit(head)
  212. except KeyError:
  213. raise MissingCommitError(head)
  214. if commit in history:
  215. continue
  216. i = 0
  217. for known_commit in history:
  218. if known_commit.commit_time > commit.commit_time:
  219. break
  220. i += 1
  221. history.insert(i, commit)
  222. parents = commit.parents
  223. pending_commits += parents
  224. history.reverse()
  225. return history
  226. def __repr__(self):
  227. return "<Repo at %r>" % self.path
  228. @classmethod
  229. def init_bare(cls, path, mkdir=True):
  230. for d in [["objects"],
  231. ["objects", "info"],
  232. ["objects", "pack"],
  233. ["branches"],
  234. ["refs"],
  235. ["refs", "tags"],
  236. ["refs", "heads"],
  237. ["hooks"],
  238. ["info"]]:
  239. os.mkdir(os.path.join(path, *d))
  240. open(os.path.join(path, 'HEAD'), 'w').write("ref: refs/heads/master\n")
  241. open(os.path.join(path, 'description'), 'w').write("Unnamed repository")
  242. open(os.path.join(path, 'info', 'excludes'), 'w').write("")
  243. create = init_bare