Rename package to dulwich, add setup.py.
[jelmer/dulwich-libgit2.git] / dulwich / objects.py
1 # objects.py -- Acces to base git objects
2 # Copyright (C) 2007 James Westby <jw+debian@jameswestby.net>
3 # The header parsing code is based on that from git itself, which is
4 # Copyright (C) 2005 Linus Torvalds
5 # and licensed under v2 of the GPL.
6
7 # This program is free software; you can redistribute it and/or
8 # modify it under the terms of the GNU General Public License
9 # as published by the Free Software Foundation; version 2
10 # of the License.
11
12 # This program is distributed in the hope that it will be useful,
13 # but WITHOUT ANY WARRANTY; without even the implied warranty of
14 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15 # GNU General Public License for more details.
16
17 # You should have received a copy of the GNU General Public License
18 # along with this program; if not, write to the Free Software
19 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
20 # MA  02110-1301, USA.
21
22 import mmap
23 import os
24 import sha
25 import zlib
26
27 from errors import (NotCommitError,
28                     NotTreeError,
29                     NotBlobError,
30                     )
31
32 blob_id = "blob"
33 tree_id = "tree"
34 commit_id = "commit"
35 parent_id = "parent"
36 author_id = "author"
37 committer_id = "committer"
38
39 def _decompress(string):
40     dcomp = zlib.decompressobj()
41     dcomped = dcomp.decompress(string)
42     dcomped += dcomp.flush()
43     return dcomped
44
45 def sha_to_hex(sha):
46   """Takes a string and returns the hex of the sha within"""
47   hexsha = ''
48   for c in sha:
49     hexsha += "%02x" % ord(c)
50   assert len(hexsha) == 40, "Incorrect length of sha1 string: %d" % \
51          len(hexsha)
52   return hexsha
53
54 class ShaFile(object):
55   """A git SHA file."""
56
57   def _update_contents(self):
58     """Update the _contents from the _text"""
59     self._contents = [ord(c) for c in self._text]
60
61   @classmethod
62   def _parse_legacy_object(cls, map):
63     """Parse a legacy object, creating it and setting object._text"""
64     text = _decompress(map)
65     object = None
66     for posstype in type_map.keys():
67       if text.startswith(posstype):
68         object = type_map[posstype]()
69         text = text[len(posstype):]
70         break
71     assert object is not None, "%s is not a known object type" % text[:9]
72     assert text[0] == ' ', "%s is not a space" % text[0]
73     text = text[1:]
74     size = 0
75     i = 0
76     while text[0] >= '0' and text[0] <= '9':
77       if i > 0 and size == 0:
78         assert False, "Size is not in canonical format"
79       size = (size * 10) + int(text[0])
80       text = text[1:]
81       i += 1
82     object._size = size
83     assert text[0] == "\0", "Size not followed by null"
84     text = text[1:]
85     object._text = text
86     object._update_contents()
87     return object
88
89   @classmethod
90   def _parse_object(cls, map):
91     """Parse a new style object , creating it and setting object._text"""
92     used = 0
93     byte = ord(map[used])
94     used += 1
95     num_type = (byte >> 4) & 7
96     try:
97       object = num_type_map[num_type]()
98     except KeyError:
99       assert False, "Not a known type: %d" % num_type
100     while((byte & 0x80) != 0):
101       byte = ord(map[used])
102       used += 1
103     raw = map[used:]
104     object._text = _decompress(raw)
105     object._update_contents()
106     return object
107
108   @classmethod
109   def _parse_file(cls, map):
110     word = (ord(map[0]) << 8) + ord(map[1])
111     if ord(map[0]) == 0x78 and (word % 31) == 0:
112       return cls._parse_legacy_object(map)
113     else:
114       return cls._parse_object(map)
115
116   def __init__(self):
117     """Don't call this directly"""
118
119   def _parse_text(self):
120     """For subclasses to do initialistion time parsing"""
121
122   @classmethod
123   def from_file(cls, filename):
124     """Get the contents of a SHA file on disk"""
125     size = os.path.getsize(filename)
126     f = open(filename, 'rb')
127     try:
128       map = mmap.mmap(f.fileno(), size, access=mmap.ACCESS_READ)
129       shafile = cls._parse_file(map)
130       shafile._parse_text()
131       return shafile
132     finally:
133       f.close()
134
135   @classmethod
136   def from_raw_string(cls, type, string):
137     """Creates an object of the indicated type from the raw string given.
138
139     Type is the numeric type of an object. String is the raw uncompressed
140     contents.
141     """
142     real_class = num_type_map[type]
143     obj = real_class()
144     obj._text = string
145     obj._update_contents()
146     return obj
147
148   def _header(self):
149     return "%s %lu\0" % (self._type, len(self._contents))
150
151   def contents(self):
152     """The raw bytes of this object"""
153     return self._contents
154
155   def sha(self):
156     """The SHA1 object that is the name of this object."""
157     ressha = sha.new()
158     ressha.update(self._header())
159     ressha.update(self._text)
160     return ressha
161
162   def __eq__(self, other):
163     """Return true id the sha of the two objects match.
164
165     The __le__ etc methods aren't overriden as they make no sense,
166     certainly at this level.
167     """
168     return self.sha().digest() == other.sha().digest()
169
170 class Blob(ShaFile):
171   """A Git Blob object."""
172
173   _type = blob_id
174
175   def text(self):
176     """The text contained within the blob object."""
177     return self._text
178
179   @classmethod
180   def from_file(cls, filename):
181     blob = ShaFile.from_file(filename)
182     if blob._type != cls._type:
183       raise NotBlobError(filename)
184     return blob
185
186   @classmethod
187   def from_string(cls, string):
188     """Create a blob from a string."""
189     shafile = cls()
190     shafile._text = string
191     shafile._update_contents()
192     return shafile
193
194 class Tree(ShaFile):
195   """A Git tree object"""
196
197   _type = tree_id
198
199   @classmethod
200   def from_file(cls, filename):
201     tree = ShaFile.from_file(filename)
202     if tree._type != cls._type:
203       raise NotTreeError(filename)
204     return tree
205
206   def entries(self):
207     """Reutrn a list of tuples describing the tree entries"""
208     return self._entries
209
210   def _parse_text(self):
211     """Grab the entries in the tree"""
212     self._entries = []
213     count = 0
214     while count < len(self._text):
215       mode = 0
216       chr = self._text[count]
217       while chr != ' ':
218         assert chr >= '0' and chr <= '7', "%s is not a valid mode char" % chr
219         mode = (mode << 3) + (ord(chr) - ord('0'))
220         count += 1
221         chr = self._text[count]
222       count += 1
223       chr = self._text[count]
224       name = ''
225       while chr != '\0':
226         name += chr
227         count += 1
228         chr = self._text[count]
229       count += 1
230       chr = self._text[count]
231       sha = self._text[count:count+20]
232       hexsha = sha_to_hex(sha)
233       self._entries.append((mode, name, hexsha))
234       count = count + 20
235
236 class Commit(ShaFile):
237   """A git commit object"""
238
239   _type = commit_id
240
241   @classmethod
242   def from_file(cls, filename):
243     commit = ShaFile.from_file(filename)
244     if commit._type != cls._type:
245       raise NotCommitError(filename)
246     return commit
247
248   def _parse_text(self):
249     text = self._text
250     count = 0
251     assert text.startswith(tree_id), "Invlid commit object, " \
252          "must start with %s" % tree_id
253     count += len(tree_id)
254     assert text[count] == ' ', "Invalid commit object, " \
255          "%s must be followed by space not %s" % (tree_id, text[count])
256     count += 1
257     self._tree = text[count:count+40]
258     count = count + 40
259     assert text[count] == "\n", "Invalid commit object, " \
260          "tree sha must be followed by newline"
261     count += 1
262     self._parents = []
263     while text[count:].startswith(parent_id):
264       count += len(parent_id)
265       assert text[count] == ' ', "Invalid commit object, " \
266            "%s must be followed by space not %s" % (parent_id, text[count])
267       count += 1
268       self._parents.append(text[count:count+40])
269       count += 40
270       assert text[count] == "\n", "Invalid commit object, " \
271            "parent sha must be followed by newline"
272       count += 1
273     self._author = None
274     if text[count:].startswith(author_id):
275       count += len(author_id)
276       assert text[count] == ' ', "Invalid commit object, " \
277            "%s must be followed by space not %s" % (author_id, text[count])
278       count += 1
279       self._author = ''
280       while text[count] != '>':
281         assert text[count] != '\n', "Malformed author information"
282         self._author += text[count]
283         count += 1
284       self._author += text[count]
285       count += 1
286       while text[count] != '\n':
287         count += 1
288       count += 1
289     self._committer = None
290     if text[count:].startswith(committer_id):
291       count += len(committer_id)
292       assert text[count] == ' ', "Invalid commit object, " \
293            "%s must be followed by space not %s" % (committer_id, text[count])
294       count += 1
295       self._committer = ''
296       while text[count] != '>':
297         assert text[count] != '\n', "Malformed committer information"
298         self._committer += text[count]
299         count += 1
300       self._committer += text[count]
301       count += 1
302       assert text[count] == ' ', "Invalid commit object, " \
303            "commiter information must be followed by space not %s" % text[count]
304       count += 1
305       self._commit_time = int(text[count:count+10])
306       while text[count] != '\n':
307         count += 1
308       count += 1
309     assert text[count] == '\n', "There must be a new line after the headers"
310     count += 1
311     # XXX: There can be an encoding field.
312     self._message = text[count:]
313
314   def tree(self):
315     """Returns the tree that is the state of this commit"""
316     return self._tree
317
318   def parents(self):
319     """Return a list of parents of this commit."""
320     return self._parents
321
322   def author(self):
323     """Returns the name of the author of the commit"""
324     return self._author
325
326   def committer(self):
327     """Returns the name of the committer of the commit"""
328     return self._committer
329
330   def message(self):
331     """Returns the commit message"""
332     return self._message
333
334   def commit_time(self):
335     """Returns the timestamp of the commit.
336     
337     Returns it as the number of seconds since the epoch.
338     """
339     return self._commit_time
340
341 type_map = {
342   blob_id : Blob,
343   tree_id : Tree,
344   commit_id : Commit,
345 }
346
347 num_type_map = {
348   1 : Commit,
349   2 : Tree,
350   3 : Blob,
351 }
352