repo.py 7.7 KB

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