Add simple log command.
[jelmer/dulwich-libgit2.git] / dulwich / repo.py
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
20 import os
21
22 from commit import Commit
23 from errors import MissingCommitError, NotBlobError, NotTreeError, NotCommitError
24 from objects import (ShaFile,
25                      Commit,
26                      Tree,
27                      Blob,
28                      )
29 from pack import load_packs, iter_sha1, PackData, write_pack_index_v2
30 import tempfile
31
32 OBJECTDIR = 'objects'
33 PACKDIR = 'pack'
34 SYMREF = 'ref: '
35
36
37 class Tag(object):
38
39     def __init__(self, name, ref):
40         self.name = name
41         self.ref = ref
42
43
44 class Repo(object):
45
46   ref_locs = ['', 'refs', 'refs/tags', 'refs/heads', 'refs/remotes']
47
48   def __init__(self, root):
49     controldir = os.path.join(root, ".git")
50     if os.path.exists(os.path.join(controldir, "objects")):
51       self.bare = False
52       self._controldir = controldir
53     else:
54       self.bare = True
55       self._controldir = root
56     self.path = root
57     self.tags = [Tag(name, ref) for name, ref in self.get_tags().items()]
58     self._object_store = None
59
60   def controldir(self):
61     return self._controldir
62
63   def fetch_objects(self, determine_wants, graph_walker, progress):
64     wants = determine_wants(self.heads())
65     commits_to_send = []
66     ref = graph_walker.next()
67     while ref:
68         commits_to_send.append(ref)
69         if ref in self.object_store:
70             graph_walker.ack(ref)
71         ref = graph_walker.next()
72     sha_done = set()
73     for sha in commits_to_send:
74         if sha in sha_done:
75             continue
76
77         c = self.commit(sha)
78         sha_done.add(sha)
79
80         def parse_tree(tree, sha_done):
81             for mode, name, x in tree.entries():
82                 if not x in sha_done:
83                     try:
84                         t = self.tree(x)
85                         sha_done.add(x)
86                         parse_tree(t, sha_done)
87                     except:
88                         sha_done.append(x)
89
90         treesha = c.tree
91         if treesha not in sha_done:
92             t = self.tree(treesha)
93             sha_done.add(treesha)
94             parse_tree(t, sha_done)
95
96         progress("counting objects: %d\r" % len(sha_done))
97
98         for sha in sha_done:
99             yield self.get_object(sha)
100
101   def object_dir(self):
102     return os.path.join(self.controldir(), OBJECTDIR)
103
104   @property
105   def object_store(self):
106     if self._object_store is None:
107         self._object_store = ObjectStore(self.object_dir())
108     return self._object_store
109
110   def pack_dir(self):
111     return os.path.join(self.object_dir(), PACKDIR)
112
113   def _get_ref(self, file):
114     f = open(file, 'rb')
115     try:
116       contents = f.read()
117       if contents.startswith(SYMREF):
118         ref = contents[len(SYMREF):]
119         if ref[-1] == '\n':
120           ref = ref[:-1]
121         return self.ref(ref)
122       assert len(contents) == 41, 'Invalid ref in %s' % file
123       return contents[:-1]
124     finally:
125       f.close()
126
127   def ref(self, name):
128     for dir in self.ref_locs:
129       file = os.path.join(self.controldir(), dir, name)
130       if os.path.exists(file):
131         return self._get_ref(file)
132
133   def get_refs(self):
134     ret = {"HEAD": self.head()}
135     for dir in ["refs/heads", "refs/tags"]:
136         for name in os.listdir(os.path.join(self.controldir(), dir)):
137           path = os.path.join(self.controldir(), dir, name)
138           if os.path.isfile(path):
139             ret["/".join([dir, name])] = self._get_ref(path)
140     return ret
141
142   def set_ref(self, name, value):
143     file = os.path.join(self.controldir(), name)
144     open(file, 'w').write(value+"\n")
145
146   def remove_ref(self, name):
147     file = os.path.join(self.controldir(), name)
148     if os.path.exists(file):
149       os.remove(file)
150       return
151
152   def get_tags(self):
153     ret = {}
154     for root, dirs, files in os.walk(os.path.join(self.controldir(), 'refs', 'tags')):
155       for name in files:
156         ret[name] = self._get_ref(os.path.join(root, name))
157     return ret
158
159   def heads(self):
160     ret = {}
161     for root, dirs, files in os.walk(os.path.join(self.controldir(), 'refs', 'heads')):
162       for name in files:
163         ret[name] = self._get_ref(os.path.join(root, name))
164     return ret
165
166   def head(self):
167     return self.ref('HEAD')
168
169   def _get_object(self, sha, cls):
170     ret = self.get_object(sha)
171     if ret._type != cls._type:
172         if cls is Commit:
173             raise NotCommitError(ret)
174         elif cls is Blob:
175             raise NotBlobError(ret)
176         elif cls is Tree:
177             raise NotTreeError(ret)
178         else:
179             raise Exception("Type invalid: %r != %r" % (ret._type, cls._type))
180     return ret
181
182   def get_object(self, sha):
183     return self.object_store[sha]
184
185   def get_parents(self, sha):
186     return self.commit(sha).parents
187
188   def commit(self, sha):
189     return self._get_object(sha, Commit)
190
191   def tree(self, sha):
192     return self._get_object(sha, Tree)
193
194   def get_blob(self, sha):
195     return self._get_object(sha, Blob)
196
197   def revision_history(self, head):
198     """Returns a list of the commits reachable from head.
199
200     Returns a list of commit objects. the first of which will be the commit
201     of head, then following theat will be the parents.
202
203     Raises NotCommitError if any no commits are referenced, including if the
204     head parameter isn't the sha of a commit.
205
206     XXX: work out how to handle merges.
207     """
208     # We build the list backwards, as parents are more likely to be older
209     # than children
210     pending_commits = [head]
211     history = []
212     while pending_commits != []:
213       head = pending_commits.pop(0)
214       try:
215           commit = self.commit(head)
216       except KeyError:
217         raise MissingCommitError(head)
218       if commit in history:
219         continue
220       i = 0
221       for known_commit in history:
222         if known_commit.commit_time > commit.commit_time:
223           break
224         i += 1
225       history.insert(i, commit)
226       parents = commit.parents
227       pending_commits += parents
228     history.reverse()
229     return history
230
231   def __repr__(self):
232       return "<Repo at %r>" % self.path
233
234   @classmethod
235   def init_bare(cls, path, mkdir=True):
236       for d in [["objects"], 
237                 ["objects", "info"], 
238                 ["objects", "pack"],
239                 ["branches"],
240                 ["refs"],
241                 ["refs", "tags"],
242                 ["refs", "heads"],
243                 ["hooks"],
244                 ["info"]]:
245           os.mkdir(os.path.join(path, *d))
246       open(os.path.join(path, 'HEAD'), 'w').write("ref: refs/heads/master\n")
247       open(os.path.join(path, 'description'), 'w').write("Unnamed repository")
248       open(os.path.join(path, 'info', 'excludes'), 'w').write("")
249
250   create = init_bare
251
252
253 class ObjectStore(object):
254
255     def __init__(self, path):
256         self.path = path
257         self._packs = None
258
259     def pack_dir(self):
260         return os.path.join(self.path, PACKDIR)
261
262     def __contains__(self, sha):
263         # TODO: This can be more efficient
264         try:
265             self[sha]
266             return True
267         except KeyError:
268             return False
269
270     @property
271     def packs(self):
272         """List with pack objects."""
273         if self._packs is None:
274             self._packs = list(load_packs(self.pack_dir()))
275         return self._packs
276
277     def _get_shafile(self, sha):
278         dir = sha[:2]
279         file = sha[2:]
280         # Check from object dir
281         path = os.path.join(self.path, dir, file)
282         if os.path.exists(path):
283           return ShaFile.from_file(path)
284         return None
285
286     def get_raw(self, sha):
287         """Obtain the raw text for an object.
288         
289         :param sha: Sha for the object.
290         :return: tuple with object type and object contents.
291         """
292         for pack in self.packs:
293             if sha in pack:
294                 return pack.get_raw(sha, self.get_raw)
295         # FIXME: Are pack deltas ever against on-disk shafiles ?
296         ret = self._get_shafile(sha)
297         if ret is not None:
298             return ret.as_raw_string()
299         raise KeyError(sha)
300
301     def __getitem__(self, sha):
302         assert len(sha) == 40, "Incorrect length sha: %s" % str(sha)
303         ret = self._get_shafile(sha)
304         if ret is not None:
305             return ret
306         # Check from packs
307         type, uncomp = self.get_raw(sha)
308         return ShaFile.from_raw_string(type, uncomp)
309
310     def move_in_pack(self, path):
311         """Move a specific file containing a pack into the pack directory.
312
313         :note: The file should be on the same file system as the 
314             packs directory.
315
316         :param path: Path to the pack file.
317         """
318         p = PackData(path)
319         entries = p.sorted_entries(self.get_raw)
320         basename = os.path.join(self.pack_dir(), 
321             "pack-%s" % iter_sha1(entry[0] for entry in entries))
322         write_pack_index_v2(basename+".idx", entries, p.calculate_checksum())
323         os.rename(path, basename + ".pack")
324
325     def add_pack(self):
326         """Add a new pack to this object store. 
327
328         :return: Fileobject to write to and a commit function to 
329             call when the pack is finished.
330         """
331         fd, path = tempfile.mkstemp(dir=self.pack_dir(), suffix=".pack")
332         f = os.fdopen(fd, 'w')
333         def commit():
334             if os.path.getsize(path) > 0:
335                 self.move_in_pack(path)
336         return f, commit