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