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>
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
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.
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,
22 from commit import Commit
51 def __init__(self, name, ref):
58 ref_locs = ['', 'refs', 'refs/tags', 'refs/heads', 'refs/remotes']
60 def __init__(self, root):
61 if os.path.isdir(os.path.join(root, ".git", "objects")):
63 self._controldir = os.path.join(root, ".git")
64 elif os.path.isdir(os.path.join(root, "objects")):
66 self._controldir = root
68 raise NotGitRepository(root)
70 self.tags = [Tag(name, ref) for name, ref in self.get_tags().items()]
71 self._object_store = None
74 return self._controldir
76 def find_missing_objects(self, determine_wants, graph_walker, progress):
77 """Fetch the missing objects required for a set of revisions.
79 :param determine_wants: Function that takes a dictionary with heads
80 and returns the list of heads to fetch.
81 :param graph_walker: Object that can iterate over the list of revisions
82 to fetch and has an "ack" method that will be called to acknowledge
83 that a revision is present.
84 :param progress: Simple progress function that will be called with
85 updated progress strings.
87 wants = determine_wants(self.heads())
88 commits_to_send = set(wants)
90 ref = graph_walker.next()
93 if ref in self.object_store:
95 ref = graph_walker.next()
96 while commits_to_send:
97 sha = commits_to_send.pop()
102 assert isinstance(c, Commit)
105 commits_to_send.update([p for p in c.parents if not p in sha_done])
107 def parse_tree(tree, sha_done):
108 for mode, name, x in tree.entries():
109 if not x in sha_done:
113 parse_tree(t, sha_done)
118 if treesha not in sha_done:
119 t = self.tree(treesha)
120 sha_done.add(treesha)
121 parse_tree(t, sha_done)
123 progress("counting objects: %d\r" % len(sha_done))
126 def fetch_objects(self, determine_wants, graph_walker, progress):
127 """Fetch the missing objects required for a set of revisions.
129 :param determine_wants: Function that takes a dictionary with heads
130 and returns the list of heads to fetch.
131 :param graph_walker: Object that can iterate over the list of revisions
132 to fetch and has an "ack" method that will be called to acknowledge
133 that a revision is present.
134 :param progress: Simple progress function that will be called with
135 updated progress strings.
137 shas = self.find_missing_objects(determine_wants, graph_walker, progress)
139 yield self.get_object(sha)
141 def object_dir(self):
142 return os.path.join(self.controldir(), OBJECTDIR)
145 def object_store(self):
146 if self._object_store is None:
147 self._object_store = ObjectStore(self.object_dir())
148 return self._object_store
151 return os.path.join(self.object_dir(), PACKDIR)
153 def _get_ref(self, file):
157 if contents.startswith(SYMREF):
158 ref = contents[len(SYMREF):]
162 assert len(contents) == 41, 'Invalid ref in %s' % file
168 for dir in self.ref_locs:
169 file = os.path.join(self.controldir(), dir, name)
170 if os.path.exists(file):
171 return self._get_ref(file)
174 ret = {"HEAD": self.head()}
175 for dir in ["refs/heads", "refs/tags"]:
176 for name in os.listdir(os.path.join(self.controldir(), dir)):
177 path = os.path.join(self.controldir(), dir, name)
178 if os.path.isfile(path):
179 ret["/".join([dir, name])] = self._get_ref(path)
182 def set_ref(self, name, value):
183 file = os.path.join(self.controldir(), name)
184 open(file, 'w').write(value+"\n")
186 def remove_ref(self, name):
187 file = os.path.join(self.controldir(), name)
188 if os.path.exists(file):
194 for root, dirs, files in os.walk(os.path.join(self.controldir(), 'refs', 'tags')):
196 ret[name] = self._get_ref(os.path.join(root, name))
201 for root, dirs, files in os.walk(os.path.join(self.controldir(), 'refs', 'heads')):
203 ret[name] = self._get_ref(os.path.join(root, name))
207 return self.ref('HEAD')
209 def _get_object(self, sha, cls):
210 assert len(sha) in (20, 40)
211 ret = self.get_object(sha)
212 if ret._type != cls._type:
214 raise NotCommitError(ret)
216 raise NotBlobError(ret)
218 raise NotTreeError(ret)
220 raise Exception("Type invalid: %r != %r" % (ret._type, cls._type))
223 def get_object(self, sha):
224 return self.object_store[sha]
226 def get_parents(self, sha):
227 return self.commit(sha).parents
229 def commit(self, sha):
230 return self._get_object(sha, Commit)
233 return self._get_object(sha, Tree)
235 def get_blob(self, sha):
236 return self._get_object(sha, Blob)
238 def revision_history(self, head):
239 """Returns a list of the commits reachable from head.
241 Returns a list of commit objects. the first of which will be the commit
242 of head, then following theat will be the parents.
244 Raises NotCommitError if any no commits are referenced, including if the
245 head parameter isn't the sha of a commit.
247 XXX: work out how to handle merges.
249 # We build the list backwards, as parents are more likely to be older
251 pending_commits = [head]
253 while pending_commits != []:
254 head = pending_commits.pop(0)
256 commit = self.commit(head)
258 raise MissingCommitError(head)
259 if commit in history:
262 for known_commit in history:
263 if known_commit.commit_time > commit.commit_time:
266 history.insert(i, commit)
267 parents = commit.parents
268 pending_commits += parents
273 return "<Repo at %r>" % self.path
276 def init_bare(cls, path, mkdir=True):
277 for d in [["objects"],
286 os.mkdir(os.path.join(path, *d))
287 open(os.path.join(path, 'HEAD'), 'w').write("ref: refs/heads/master\n")
288 open(os.path.join(path, 'description'), 'w').write("Unnamed repository")
289 open(os.path.join(path, 'info', 'excludes'), 'w').write("")
294 class ObjectStore(object):
296 def __init__(self, path):
301 return os.path.join(self.path, PACKDIR)
303 def __contains__(self, sha):
304 # TODO: This can be more efficient
313 """List with pack objects."""
314 if self._packs is None:
315 self._packs = list(load_packs(self.pack_dir()))
318 def _get_shafile(self, sha):
321 # Check from object dir
322 path = os.path.join(self.path, dir, file)
323 if os.path.exists(path):
324 return ShaFile.from_file(path)
327 def get_raw(self, sha):
328 """Obtain the raw text for an object.
330 :param sha: Sha for the object.
331 :return: tuple with object type and object contents.
333 for pack in self.packs:
335 return pack.get_raw(sha, self.get_raw)
336 # FIXME: Are pack deltas ever against on-disk shafiles ?
337 ret = self._get_shafile(sha)
339 return ret.as_raw_string()
342 def __getitem__(self, sha):
343 assert len(sha) == 40, "Incorrect length sha: %s" % str(sha)
344 ret = self._get_shafile(sha)
348 type, uncomp = self.get_raw(sha)
349 return ShaFile.from_raw_string(type, uncomp)
351 def move_in_pack(self, path):
352 """Move a specific file containing a pack into the pack directory.
354 :note: The file should be on the same file system as the
357 :param path: Path to the pack file.
360 entries = p.sorted_entries(self.get_raw)
361 basename = os.path.join(self.pack_dir(),
362 "pack-%s" % iter_sha1(entry[0] for entry in entries))
363 write_pack_index_v2(basename+".idx", entries, p.calculate_checksum())
364 os.rename(path, basename + ".pack")
367 """Add a new pack to this object store.
369 :return: Fileobject to write to and a commit function to
370 call when the pack is finished.
372 fd, path = tempfile.mkstemp(dir=self.pack_dir(), suffix=".pack")
373 f = os.fdopen(fd, 'w')
375 if os.path.getsize(path) > 0:
376 self.move_in_pack(path)