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
8 # of the License or (at your option) any later version of
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.
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,
23 from commit import Commit
31 from object_store import ObjectStore
45 def __init__(self, tagdir, tags):
49 def __getitem__(self, name):
50 return self.tags[name]
52 def __setitem__(self, name, ref):
54 f = open(os.path.join(self.tagdir, name), 'wb')
70 ref_locs = ['', 'refs', 'refs/tags', 'refs/heads', 'refs/remotes']
72 def __init__(self, root):
73 if os.path.isdir(os.path.join(root, ".git", "objects")):
75 self._controldir = os.path.join(root, ".git")
76 elif os.path.isdir(os.path.join(root, "objects")):
78 self._controldir = root
80 raise NotGitRepository(root)
82 self.tags = Tags(self.tagdir(), self.get_tags())
83 self._object_store = None
86 return self._controldir
88 def find_missing_objects(self, determine_wants, graph_walker, progress):
89 """Fetch the missing objects required for a set of revisions.
91 :param determine_wants: Function that takes a dictionary with heads
92 and returns the list of heads to fetch.
93 :param graph_walker: Object that can iterate over the list of revisions
94 to fetch and has an "ack" method that will be called to acknowledge
95 that a revision is present.
96 :param progress: Simple progress function that will be called with
97 updated progress strings.
99 wants = determine_wants(self.get_refs())
100 commits_to_send = set(wants)
102 ref = graph_walker.next()
105 if ref in self.object_store:
106 graph_walker.ack(ref)
107 ref = graph_walker.next()
108 while commits_to_send:
109 sha = commits_to_send.pop()
114 assert isinstance(c, Commit)
117 commits_to_send.update([p for p in c.parents if not p in sha_done])
119 def parse_tree(tree, sha_done):
120 for mode, name, sha in tree.entries():
123 if mode & stat.S_IFDIR:
124 parse_tree(self.tree(sha), sha_done)
128 if c.tree not in sha_done:
129 parse_tree(self.tree(c.tree), sha_done)
132 progress("counting objects: %d\r" % len(sha_done))
135 def fetch_objects(self, determine_wants, graph_walker, progress):
136 """Fetch the missing objects required for a set of revisions.
138 :param determine_wants: Function that takes a dictionary with heads
139 and returns the list of heads to fetch.
140 :param graph_walker: Object that can iterate over the list of revisions
141 to fetch and has an "ack" method that will be called to acknowledge
142 that a revision is present.
143 :param progress: Simple progress function that will be called with
144 updated progress strings.
146 shas = self.find_missing_objects(determine_wants, graph_walker, progress)
148 yield self.get_object(sha)
150 def object_dir(self):
151 return os.path.join(self.controldir(), OBJECTDIR)
154 def object_store(self):
155 if self._object_store is None:
156 self._object_store = ObjectStore(self.object_dir())
157 return self._object_store
160 return os.path.join(self.object_dir(), PACKDIR)
162 def _get_ref(self, file):
166 if contents.startswith(SYMREF):
167 ref = contents[len(SYMREF):]
171 assert len(contents) == 41, 'Invalid ref in %s' % file
177 for dir in self.ref_locs:
178 file = os.path.join(self.controldir(), dir, name)
179 if os.path.exists(file):
180 return self._get_ref(file)
185 ret['HEAD'] = self.head()
186 for dir in ["refs/heads", "refs/tags"]:
187 for name in os.listdir(os.path.join(self.controldir(), dir)):
188 path = os.path.join(self.controldir(), dir, name)
189 if os.path.isfile(path):
190 ret["/".join([dir, name])] = self._get_ref(path)
193 def set_ref(self, name, value):
194 file = os.path.join(self.controldir(), name)
195 open(file, 'w').write(value+"\n")
197 def remove_ref(self, name):
198 file = os.path.join(self.controldir(), name)
199 if os.path.exists(file):
204 return os.path.join(self.controldir(), 'refs', 'tags')
208 for root, dirs, files in os.walk(self.tagdir()):
210 ret[name] = self._get_ref(os.path.join(root, name))
215 for root, dirs, files in os.walk(os.path.join(self.controldir(), 'refs', 'heads')):
217 ret[name] = self._get_ref(os.path.join(root, name))
221 return self.ref('HEAD')
223 def _get_object(self, sha, cls):
224 assert len(sha) in (20, 40)
225 ret = self.get_object(sha)
226 if ret._type != cls._type:
228 raise NotCommitError(ret)
230 raise NotBlobError(ret)
232 raise NotTreeError(ret)
234 raise Exception("Type invalid: %r != %r" % (ret._type, cls._type))
237 def get_object(self, sha):
238 return self.object_store[sha]
240 def get_parents(self, sha):
241 return self.commit(sha).parents
243 def commit(self, sha):
244 return self._get_object(sha, Commit)
247 return self._get_object(sha, Tree)
249 def get_blob(self, sha):
250 return self._get_object(sha, Blob)
252 def revision_history(self, head):
253 """Returns a list of the commits reachable from head.
255 Returns a list of commit objects. the first of which will be the commit
256 of head, then following theat will be the parents.
258 Raises NotCommitError if any no commits are referenced, including if the
259 head parameter isn't the sha of a commit.
261 XXX: work out how to handle merges.
263 # We build the list backwards, as parents are more likely to be older
265 pending_commits = [head]
267 while pending_commits != []:
268 head = pending_commits.pop(0)
270 commit = self.commit(head)
272 raise MissingCommitError(head)
273 if commit in history:
276 for known_commit in history:
277 if known_commit.commit_time > commit.commit_time:
280 history.insert(i, commit)
281 parents = commit.parents
282 pending_commits += parents
287 return "<Repo at %r>" % self.path
290 def init(cls, path, mkdir=True):
291 controldir = os.path.join(path, ".git")
293 cls.init_bare(controldir)
296 def init_bare(cls, path, mkdir=True):
297 for d in [["objects"],
306 os.mkdir(os.path.join(path, *d))
307 open(os.path.join(path, 'HEAD'), 'w').write("ref: refs/heads/master\n")
308 open(os.path.join(path, 'description'), 'w').write("Unnamed repository")
309 open(os.path.join(path, 'info', 'excludes'), 'w').write("")