repo.py 8.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303
  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 MissingCommitError, NotBlobError, NotTreeError, NotCommitError
  22. from objects import (ShaFile,
  23. Commit,
  24. Tree,
  25. Blob,
  26. )
  27. from pack import load_packs, iter_sha1, PackData, write_pack_index_v2
  28. import tempfile
  29. OBJECTDIR = 'objects'
  30. PACKDIR = 'pack'
  31. SYMREF = 'ref: '
  32. class Tag(object):
  33. def __init__(self, name, ref):
  34. self.name = name
  35. self.ref = ref
  36. class Repo(object):
  37. ref_locs = ['', 'refs', 'refs/tags', 'refs/heads', 'refs/remotes']
  38. def __init__(self, root):
  39. controldir = os.path.join(root, ".git")
  40. if os.path.exists(os.path.join(controldir, "objects")):
  41. self.bare = False
  42. self._basedir = controldir
  43. else:
  44. self.bare = True
  45. self._basedir = root
  46. self.path = controldir
  47. self.tags = [Tag(name, ref) for name, ref in self.get_tags().items()]
  48. self._object_store = None
  49. def basedir(self):
  50. return self._basedir
  51. def fetch_objects(self, determine_wants, graph_walker, progress):
  52. sha_done = set()
  53. commits_to_send = want[:]
  54. for sha in commits_to_send:
  55. if sha in sha_done:
  56. continue
  57. c = self.commit(sha)
  58. yield c
  59. sha_done.add(sha)
  60. for p in c.parents:
  61. if not p in commits_to_send:
  62. commits_to_send.append(p)
  63. def parse_tree(tree, sha_done):
  64. for mode, name, x in tree.entries():
  65. if not x in sha_done:
  66. try:
  67. t = self.tree(x)
  68. yield t
  69. sha_done.add(x)
  70. parse_tree(t, sha_done)
  71. except:
  72. yield self.get_object(x)
  73. sha_done.append(x)
  74. treesha = c.tree
  75. if treesha not in sha_done:
  76. t = self.tree(treesha)
  77. yield t
  78. sha_done.add(treesha)
  79. parse_tree(t, sha_done)
  80. progress("counting objects: %d\r" % len(sha_done))
  81. def object_dir(self):
  82. return os.path.join(self.basedir(), OBJECTDIR)
  83. @property
  84. def object_store(self):
  85. if self._object_store is None:
  86. self._object_store = ObjectStore(self.object_dir())
  87. return self._object_store
  88. def pack_dir(self):
  89. return os.path.join(self.object_dir(), PACKDIR)
  90. def _get_ref(self, file):
  91. f = open(file, 'rb')
  92. try:
  93. contents = f.read()
  94. if contents.startswith(SYMREF):
  95. ref = contents[len(SYMREF):]
  96. if ref[-1] == '\n':
  97. ref = ref[:-1]
  98. return self.ref(ref)
  99. assert len(contents) == 41, 'Invalid ref'
  100. return contents[:-1]
  101. finally:
  102. f.close()
  103. def ref(self, name):
  104. for dir in self.ref_locs:
  105. file = os.path.join(self.basedir(), dir, name)
  106. if os.path.exists(file):
  107. return self._get_ref(file)
  108. def set_ref(self, name, value):
  109. file = os.path.join(self.basedir(), name)
  110. open(file, 'w').write(value+"\n")
  111. def remove_ref(self, name):
  112. file = os.path.join(self.basedir(), name)
  113. if os.path.exists(file):
  114. os.remove(file)
  115. return
  116. def get_tags(self):
  117. ret = {}
  118. for root, dirs, files in os.walk(os.path.join(self.basedir(), 'refs', 'tags')):
  119. for name in files:
  120. ret[name] = self._get_ref(os.path.join(root, name))
  121. return ret
  122. def heads(self):
  123. ret = {}
  124. for root, dirs, files in os.walk(os.path.join(self.basedir(), 'refs', 'heads')):
  125. for name in files:
  126. ret[name] = self._get_ref(os.path.join(root, name))
  127. return ret
  128. def head(self):
  129. return self.ref('HEAD')
  130. def _get_object(self, sha, cls):
  131. ret = self.get_object(sha)
  132. if ret._type != cls._type:
  133. if cls is Commit:
  134. raise NotCommitError(ret)
  135. elif cls is Blob:
  136. raise NotBlobError(ret)
  137. elif cls is Tree:
  138. raise NotTreeError(ret)
  139. else:
  140. raise Exception("Type invalid: %r != %r" % (ret._type, cls._type))
  141. return ret
  142. def get_object(self, sha):
  143. return self.object_store[sha]
  144. def get_parents(self, sha):
  145. return self.commit(sha).parents
  146. def commit(self, sha):
  147. return self._get_object(sha, Commit)
  148. def tree(self, sha):
  149. return self._get_object(sha, Tree)
  150. def get_blob(self, sha):
  151. return self._get_object(sha, Blob)
  152. def revision_history(self, head):
  153. """Returns a list of the commits reachable from head.
  154. Returns a list of commit objects. the first of which will be the commit
  155. of head, then following theat will be the parents.
  156. Raises NotCommitError if any no commits are referenced, including if the
  157. head parameter isn't the sha of a commit.
  158. XXX: work out how to handle merges.
  159. """
  160. # We build the list backwards, as parents are more likely to be older
  161. # than children
  162. pending_commits = [head]
  163. history = []
  164. while pending_commits != []:
  165. head = pending_commits.pop(0)
  166. try:
  167. commit = self.commit(head)
  168. except KeyError:
  169. raise MissingCommitError(head)
  170. if commit in history:
  171. continue
  172. i = 0
  173. for known_commit in history:
  174. if known_commit.commit_time > commit.commit_time:
  175. break
  176. i += 1
  177. history.insert(i, commit)
  178. parents = commit.parents
  179. pending_commits += parents
  180. history.reverse()
  181. return history
  182. @classmethod
  183. def init_bare(cls, path, mkdir=True):
  184. for d in [["objects"],
  185. ["objects", "info"],
  186. ["objects", "pack"],
  187. ["branches"],
  188. ["refs"],
  189. ["refs", "tags"],
  190. ["refs", "heads"],
  191. ["hooks"],
  192. ["info"]]:
  193. os.mkdir(os.path.join(path, *d))
  194. open(os.path.join(path, 'HEAD'), 'w').write("ref: refs/heads/master\n")
  195. open(os.path.join(path, 'description'), 'w').write("Unnamed repository")
  196. open(os.path.join(path, 'info', 'excludes'), 'w').write("")
  197. create = init_bare
  198. class ObjectStore(object):
  199. def __init__(self, path):
  200. self.path = path
  201. self._packs = None
  202. def pack_dir(self):
  203. return os.path.join(self.path, PACKDIR)
  204. def __contains__(self, sha):
  205. # TODO: This can be more efficient
  206. try:
  207. self[sha]
  208. return True
  209. except KeyError:
  210. return False
  211. @property
  212. def packs(self):
  213. if self._packs is None:
  214. self._packs = list(load_packs(self.pack_dir()))
  215. return self._packs
  216. def _get_shafile(self, sha):
  217. dir = sha[:2]
  218. file = sha[2:]
  219. # Check from object dir
  220. path = os.path.join(self.path, dir, file)
  221. if os.path.exists(path):
  222. return ShaFile.from_file(path)
  223. return None
  224. def get_raw(self, sha):
  225. for pack in self.packs:
  226. if sha in pack:
  227. return pack.get_raw(sha, self.get_raw)
  228. # FIXME: Are pack deltas ever against on-disk shafiles ?
  229. ret = self._get_shafile(sha)
  230. if ret is not None:
  231. return ret.as_raw_string()
  232. raise KeyError(sha)
  233. def __getitem__(self, sha):
  234. assert len(sha) == 40, "Incorrect length sha: %s" % str(sha)
  235. ret = self._get_shafile(sha)
  236. if ret is not None:
  237. return ret
  238. # Check from packs
  239. type, uncomp = self.get_raw(sha)
  240. return ShaFile.from_raw_string(type, uncomp)
  241. def move_in_pack(self, path):
  242. p = PackData(path)
  243. entries = p.sorted_entries(self.get_raw)
  244. basename = os.path.join(self.pack_dir(), "pack-%s" % iter_sha1(entry[0] for entry in entries))
  245. write_pack_index_v2(basename+".idx", entries, p.calculate_checksum())
  246. os.rename(path, basename + ".pack")
  247. def add_pack(self):
  248. fd, path = tempfile.mkstemp(dir=self.pack_dir(), suffix=".pack")
  249. f = os.fdopen(fd, 'w')
  250. def commit():
  251. if os.path.getsize(path) > 0:
  252. self.move_in_pack(path)
  253. return f, commit