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.
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
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.
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,
27
from errors import (NotCommitError,
38
COMMITTER_ID = "committer"
11
committer_id = "committer"
40
13
def _decompress(string):
41
14
dcomp = zlib.decompressobj()
47
20
"""Takes a string and returns the hex of the sha within"""
50
hexsha += "%02x" % ord(c)
24
hexsha += "0%x" % ord(c)
26
hexsha += "%x" % ord(c)
51
27
assert len(hexsha) == 40, "Incorrect length of sha1 string: %d" % \
56
31
class ShaFile(object):
57
32
"""A git SHA file."""
34
def _update_contents(self):
35
"""Update the _contents from the _text"""
36
self._contents = [ord(c) for c in self._text]
60
39
def _parse_legacy_object(cls, map):
61
40
"""Parse a legacy object, creating it and setting object._text"""
81
60
assert text[0] == "\0", "Size not followed by null"
83
62
object._text = text
63
object._update_contents()
86
def as_raw_string(self):
87
return self._num_type, self._text
90
67
def _parse_object(cls, map):
91
68
"""Parse a new style object , creating it and setting object._text"""
116
94
"""Don't call this directly"""
118
96
def _parse_text(self):
119
"""For subclasses to do initialisation time parsing"""
97
"""For subclasses to do initialistion time parsing"""
122
100
def from_file(cls, filename):
123
101
"""Get the contents of a SHA file on disk"""
124
102
size = os.path.getsize(filename)
125
f = open(filename, 'rb')
103
f = open(filename, 'rb+')
127
map = mmap.mmap(f.fileno(), size, access=mmap.ACCESS_READ)
105
map = mmap.mmap(f.fileno(), size)
128
106
shafile = cls._parse_file(map)
129
107
shafile._parse_text()
135
def from_raw_string(cls, type, string):
136
"""Creates an object of the indicated type from the raw string given.
138
Type is the numeric type of an object. String is the raw uncompressed
141
real_class = num_type_map[type]
148
112
def _header(self):
149
return "%s %lu\0" % (self._type, len(self._text))
113
return "%s %lu\0" % (self._type, len(self._contents))
152
return zlib.crc32(self._text)
116
"""The raw bytes of this object"""
117
return self._contents
155
120
"""The SHA1 object that is the name of this object."""
158
123
ressha.update(self._text)
163
return self.sha().hexdigest()
166
return "<%s %s>" % (self.__class__.__name__, self.id)
168
def __eq__(self, other):
169
"""Return true id the sha of the two objects match.
171
The __le__ etc methods aren't overriden as they make no sense,
172
certainly at this level.
174
return self.sha().digest() == other.sha().digest()
177
126
class Blob(ShaFile):
178
127
"""A Git Blob object."""
184
132
"""The text contained within the blob object."""
185
133
return self._text
188
136
def from_file(cls, filename):
189
137
blob = ShaFile.from_file(filename)
190
if blob._type != cls._type:
191
raise NotBlobError(filename)
195
def from_string(cls, string):
196
"""Create a blob from a string."""
198
shafile._text = string
203
"""A Git Tag object."""
208
def from_file(cls, filename):
209
blob = ShaFile.from_file(filename)
210
if blob._type != cls._type:
211
raise NotBlobError(filename)
215
def from_string(cls, string):
216
"""Create a blob from a string."""
218
shafile._text = string
138
assert blob._type == cls._type, "%s is not a blob object" % filename
142
def from_string(cls, string):
143
"""Create a blob from a string."""
145
shafile._text = string
146
shafile._update_contents()
222
149
class Tree(ShaFile):
223
150
"""A Git tree object"""
228
155
def from_file(cls, filename):
229
156
tree = ShaFile.from_file(filename)
230
if tree._type != cls._type:
231
raise NotTreeError(filename)
157
assert tree._type == cls._type, "%s is not a tree object" % filename
234
160
def entries(self):
235
"""Return a list of tuples describing the tree entries"""
161
"""Reutrn a list of tuples describing the tree entries"""
236
162
return self._entries
238
164
def _parse_text(self):
264
190
class Commit(ShaFile):
265
191
"""A git commit object"""
270
196
def from_file(cls, filename):
271
197
commit = ShaFile.from_file(filename)
272
if commit._type != cls._type:
273
raise NotCommitError(filename)
198
assert commit._type == cls._type, "%s is not a commit object" % filename
276
201
def _parse_text(self):
277
202
text = self._text
279
assert text.startswith(TREE_ID), "Invalid commit object, " \
280
"must start with %s" % TREE_ID
281
count += len(TREE_ID)
204
assert text.startswith(tree_id), "Invlid commit object, " \
205
"must start with %s" % tree_id
206
count += len(tree_id)
282
207
assert text[count] == ' ', "Invalid commit object, " \
283
"%s must be followed by space not %s" % (TREE_ID, text[count])
208
"%s must be followed by space not %s" % (tree_id, text[count])
285
210
self._tree = text[count:count+40]
286
211
count = count + 40
288
213
"tree sha must be followed by newline"
290
215
self._parents = []
291
while text[count:].startswith(PARENT_ID):
292
count += len(PARENT_ID)
216
while text[count:].startswith(parent_id):
217
count += len(parent_id)
293
218
assert text[count] == ' ', "Invalid commit object, " \
294
"%s must be followed by space not %s" % (PARENT_ID, text[count])
219
"%s must be followed by space not %s" % (parent_id, text[count])
296
221
self._parents.append(text[count:count+40])
299
224
"parent sha must be followed by newline"
301
226
self._author = None
302
if text[count:].startswith(AUTHOR_ID):
303
count += len(AUTHOR_ID)
227
if text[count:].startswith(author_id):
228
count += len(author_id)
304
229
assert text[count] == ' ', "Invalid commit object, " \
305
"%s must be followed by space not %s" % (AUTHOR_ID, text[count])
230
"%s must be followed by space not %s" % (author_id, text[count])
307
232
self._author = ''
308
while text[count] != '>':
309
assert text[count] != '\n', "Malformed author information"
233
while text[count] != '\n':
310
234
self._author += text[count]
312
self._author += text[count]
314
while text[count] != '\n':
317
237
self._committer = None
318
if text[count:].startswith(COMMITTER_ID):
319
count += len(COMMITTER_ID)
238
if text[count:].startswith(committer_id):
239
count += len(committer_id)
320
240
assert text[count] == ' ', "Invalid commit object, " \
321
"%s must be followed by space not %s" % (COMMITTER_ID, text[count])
241
"%s must be followed by space not %s" % (committer_id, text[count])
323
243
self._committer = ''
324
while text[count] != '>':
325
assert text[count] != '\n', "Malformed committer information"
244
while text[count] != '\n':
326
245
self._committer += text[count]
328
self._committer += text[count]
330
assert text[count] == ' ', "Invalid commit object, " \
331
"commiter information must be followed by space not %s" % text[count]
333
self._commit_time = int(text[count:count+10])
334
while text[count] != '\n':
337
248
assert text[count] == '\n', "There must be a new line after the headers"
339
# XXX: There can be an encoding field.
340
250
self._message = text[count:]
344
253
"""Returns the tree that is the state of this commit"""
345
254
return self._tree
348
256
def parents(self):
349
257
"""Return a list of parents of this commit."""
350
258
return self._parents
353
260
def author(self):
354
261
"""Returns the name of the author of the commit"""
355
262
return self._author
358
264
def committer(self):
359
265
"""Returns the name of the committer of the commit"""
360
266
return self._committer
363
268
def message(self):
364
269
"""Returns the commit message"""
365
270
return self._message
368
def commit_time(self):
369
"""Returns the timestamp of the commit.
371
Returns it as the number of seconds since the epoch.
373
return self._commit_time
388
# 5 Is reserved for further expansion