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, x in tree.entries():
121 if not x in sha_done:
125 parse_tree(t, sha_done)
130 if treesha not in sha_done:
131 t = self.tree(treesha)
132 sha_done.add(treesha)
133 parse_tree(t, sha_done)
135 progress("counting objects: %d\r" % len(sha_done))
138 def fetch_objects(self, determine_wants, graph_walker, progress):
139 """Fetch the missing objects required for a set of revisions.
141 :param determine_wants: Function that takes a dictionary with heads
142 and returns the list of heads to fetch.
143 :param graph_walker: Object that can iterate over the list of revisions
144 to fetch and has an "ack" method that will be called to acknowledge
145 that a revision is present.
146 :param progress: Simple progress function that will be called with
147 updated progress strings.
149 shas = self.find_missing_objects(determine_wants, graph_walker, progress)
151 yield self.get_object(sha)
153 def object_dir(self):
154 return os.path.join(self.controldir(), OBJECTDIR)
157 def object_store(self):
158 if self._object_store is None:
159 self._object_store = ObjectStore(self.object_dir())
160 return self._object_store
163 return os.path.join(self.object_dir(), PACKDIR)
165 def _get_ref(self, file):
169 if contents.startswith(SYMREF):
170 ref = contents[len(SYMREF):]
174 assert len(contents) == 41, 'Invalid ref in %s' % file
180 for dir in self.ref_locs:
181 file = os.path.join(self.controldir(), dir, name)
182 if os.path.exists(file):
183 return self._get_ref(file)
188 ret['HEAD'] = self.head()
189 for dir in ["refs/heads", "refs/tags"]:
190 for name in os.listdir(os.path.join(self.controldir(), dir)):
191 path = os.path.join(self.controldir(), dir, name)
192 if os.path.isfile(path):
193 ret["/".join([dir, name])] = self._get_ref(path)
196 def set_ref(self, name, value):
197 file = os.path.join(self.controldir(), name)
198 open(file, 'w').write(value+"\n")
200 def remove_ref(self, name):
201 file = os.path.join(self.controldir(), name)
202 if os.path.exists(file):
207 return os.path.join(self.controldir(), 'refs', 'tags')
211 for root, dirs, files in os.walk(self.tagdir()):
213 ret[name] = self._get_ref(os.path.join(root, name))
218 for root, dirs, files in os.walk(os.path.join(self.controldir(), 'refs', 'heads')):
220 ret[name] = self._get_ref(os.path.join(root, name))
224 return self.ref('HEAD')
226 def _get_object(self, sha, cls):
227 assert len(sha) in (20, 40)
228 ret = self.get_object(sha)
229 if ret._type != cls._type:
231 raise NotCommitError(ret)
233 raise NotBlobError(ret)
235 raise NotTreeError(ret)
237 raise Exception("Type invalid: %r != %r" % (ret._type, cls._type))
240 def get_object(self, sha):
241 return self.object_store[sha]
243 def get_parents(self, sha):
244 return self.commit(sha).parents
246 def commit(self, sha):
247 return self._get_object(sha, Commit)
250 return self._get_object(sha, Tree)
252 def get_blob(self, sha):
253 return self._get_object(sha, Blob)
255 def revision_history(self, head):
256 """Returns a list of the commits reachable from head.
258 Returns a list of commit objects. the first of which will be the commit
259 of head, then following theat will be the parents.
261 Raises NotCommitError if any no commits are referenced, including if the
262 head parameter isn't the sha of a commit.
264 XXX: work out how to handle merges.
266 # We build the list backwards, as parents are more likely to be older
268 pending_commits = [head]
270 while pending_commits != []:
271 head = pending_commits.pop(0)
273 commit = self.commit(head)
275 raise MissingCommitError(head)
276 if commit in history:
279 for known_commit in history:
280 if known_commit.commit_time > commit.commit_time:
283 history.insert(i, commit)
284 parents = commit.parents
285 pending_commits += parents
290 return "<Repo at %r>" % self.path
293 def init(cls, path, mkdir=True):
294 controldir = os.path.join(path, ".git")
296 cls.init_bare(controldir)
299 def init_bare(cls, path, mkdir=True):
300 for d in [["objects"],
309 os.mkdir(os.path.join(path, *d))
310 open(os.path.join(path, 'HEAD'), 'w').write("ref: refs/heads/master\n")
311 open(os.path.join(path, 'description'), 'w').write("Unnamed repository")
312 open(os.path.join(path, 'info', 'excludes'), 'w').write("")