/brz/remove-bazaar

To get this branch, use:
bzr branch http://gegoxaren.bato24.eu/bzr/brz/remove-bazaar

« back to all changes in this revision

Viewing changes to dulwich/objects.py

  • Committer: Jelmer Vernooij
  • Date: 2009-01-14 18:24:38 UTC
  • mto: (0.222.3 dulwich)
  • mto: This revision was merged to the branch mainline in revision 6960.
  • Revision ID: jelmer@samba.org-20090114182438-c0tn5eczyupi4ztn
Fix download url, add version number.

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
# objects.py -- Acces to base git objects
 
2
# Copyright (C) 2007 James Westby <jw+debian@jameswestby.net>
 
3
# Copyright (C) 2008 Jelmer Vernooij <jelmer@samba.org>
 
4
# The header parsing code is based on that from git itself, which is
 
5
# Copyright (C) 2005 Linus Torvalds
 
6
# and licensed under v2 of the GPL.
 
7
 
8
# This program is free software; you can redistribute it and/or
 
9
# modify it under the terms of the GNU General Public License
 
10
# as published by the Free Software Foundation; version 2
 
11
# of the License.
 
12
 
13
# This program is distributed in the hope that it will be useful,
 
14
# but WITHOUT ANY WARRANTY; without even the implied warranty of
 
15
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 
16
# GNU General Public License for more details.
 
17
 
18
# You should have received a copy of the GNU General Public License
 
19
# along with this program; if not, write to the Free Software
 
20
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
 
21
# MA  02110-1301, USA.
 
22
 
 
23
import mmap
 
24
import os
 
25
import sha
 
26
import zlib
 
27
 
 
28
from errors import (NotCommitError,
 
29
                    NotTreeError,
 
30
                    NotBlobError,
 
31
                    )
 
32
 
 
33
BLOB_ID = "blob"
 
34
TAG_ID = "tag"
 
35
TREE_ID = "tree"
 
36
COMMIT_ID = "commit"
 
37
PARENT_ID = "parent"
 
38
AUTHOR_ID = "author"
 
39
COMMITTER_ID = "committer"
 
40
OBJECT_ID = "object"
 
41
TYPE_ID = "type"
 
42
TAGGER_ID = "tagger"
 
43
 
 
44
def _decompress(string):
 
45
    dcomp = zlib.decompressobj()
 
46
    dcomped = dcomp.decompress(string)
 
47
    dcomped += dcomp.flush()
 
48
    return dcomped
 
49
 
 
50
def sha_to_hex(sha):
 
51
  """Takes a string and returns the hex of the sha within"""
 
52
  hexsha = ''
 
53
  for c in sha:
 
54
    hexsha += "%02x" % ord(c)
 
55
  assert len(hexsha) == 40, "Incorrect length of sha1 string: %d" % \
 
56
         len(hexsha)
 
57
  return hexsha
 
58
 
 
59
def hex_to_sha(hex):
 
60
  """Takes a hex sha and returns a binary sha"""
 
61
  sha = ''
 
62
  for i in range(0, len(hex), 2):
 
63
    sha += chr(int(hex[i:i+2], 16))
 
64
  assert len(sha) == 20, "Incorrent length of sha1: %d" % len(sha)
 
65
  return sha
 
66
 
 
67
class ShaFile(object):
 
68
  """A git SHA file."""
 
69
 
 
70
  @classmethod
 
71
  def _parse_legacy_object(cls, map):
 
72
    """Parse a legacy object, creating it and setting object._text"""
 
73
    text = _decompress(map)
 
74
    object = None
 
75
    for posstype in type_map.keys():
 
76
      if text.startswith(posstype):
 
77
        object = type_map[posstype]()
 
78
        text = text[len(posstype):]
 
79
        break
 
80
    assert object is not None, "%s is not a known object type" % text[:9]
 
81
    assert text[0] == ' ', "%s is not a space" % text[0]
 
82
    text = text[1:]
 
83
    size = 0
 
84
    i = 0
 
85
    while text[0] >= '0' and text[0] <= '9':
 
86
      if i > 0 and size == 0:
 
87
        assert False, "Size is not in canonical format"
 
88
      size = (size * 10) + int(text[0])
 
89
      text = text[1:]
 
90
      i += 1
 
91
    object._size = size
 
92
    assert text[0] == "\0", "Size not followed by null"
 
93
    text = text[1:]
 
94
    object._text = text
 
95
    return object
 
96
 
 
97
  def as_raw_string(self):
 
98
    return self._num_type, self._text
 
99
 
 
100
  @classmethod
 
101
  def _parse_object(cls, map):
 
102
    """Parse a new style object , creating it and setting object._text"""
 
103
    used = 0
 
104
    byte = ord(map[used])
 
105
    used += 1
 
106
    num_type = (byte >> 4) & 7
 
107
    try:
 
108
      object = num_type_map[num_type]()
 
109
    except KeyError:
 
110
      assert False, "Not a known type: %d" % num_type
 
111
    while((byte & 0x80) != 0):
 
112
      byte = ord(map[used])
 
113
      used += 1
 
114
    raw = map[used:]
 
115
    object._text = _decompress(raw)
 
116
    return object
 
117
 
 
118
  @classmethod
 
119
  def _parse_file(cls, map):
 
120
    word = (ord(map[0]) << 8) + ord(map[1])
 
121
    if ord(map[0]) == 0x78 and (word % 31) == 0:
 
122
      return cls._parse_legacy_object(map)
 
123
    else:
 
124
      return cls._parse_object(map)
 
125
 
 
126
  def __init__(self):
 
127
    """Don't call this directly"""
 
128
 
 
129
  def _parse_text(self):
 
130
    """For subclasses to do initialisation time parsing"""
 
131
 
 
132
  @classmethod
 
133
  def from_file(cls, filename):
 
134
    """Get the contents of a SHA file on disk"""
 
135
    size = os.path.getsize(filename)
 
136
    f = open(filename, 'rb')
 
137
    try:
 
138
      map = mmap.mmap(f.fileno(), size, access=mmap.ACCESS_READ)
 
139
      shafile = cls._parse_file(map)
 
140
      shafile._parse_text()
 
141
      return shafile
 
142
    finally:
 
143
      f.close()
 
144
 
 
145
  @classmethod
 
146
  def from_raw_string(cls, type, string):
 
147
    """Creates an object of the indicated type from the raw string given.
 
148
 
 
149
    Type is the numeric type of an object. String is the raw uncompressed
 
150
    contents.
 
151
    """
 
152
    real_class = num_type_map[type]
 
153
    obj = real_class()
 
154
    obj._num_type = type
 
155
    obj._text = string
 
156
    obj._parse_text()
 
157
    return obj
 
158
 
 
159
  def _header(self):
 
160
    return "%s %lu\0" % (self._type, len(self._text))
 
161
 
 
162
  def crc32(self):
 
163
    return zlib.crc32(self._text)
 
164
 
 
165
  def sha(self):
 
166
    """The SHA1 object that is the name of this object."""
 
167
    ressha = sha.new()
 
168
    ressha.update(self._header())
 
169
    ressha.update(self._text)
 
170
    return ressha
 
171
 
 
172
  @property
 
173
  def id(self):
 
174
      return self.sha().hexdigest()
 
175
 
 
176
  def __repr__(self):
 
177
    return "<%s %s>" % (self.__class__.__name__, self.id)
 
178
 
 
179
  def __eq__(self, other):
 
180
    """Return true id the sha of the two objects match.
 
181
 
 
182
    The __le__ etc methods aren't overriden as they make no sense,
 
183
    certainly at this level.
 
184
    """
 
185
    return self.sha().digest() == other.sha().digest()
 
186
 
 
187
 
 
188
class Blob(ShaFile):
 
189
  """A Git Blob object."""
 
190
 
 
191
  _type = BLOB_ID
 
192
  _num_type = 3
 
193
 
 
194
  @property
 
195
  def data(self):
 
196
    """The text contained within the blob object."""
 
197
    return self._text
 
198
 
 
199
  @classmethod
 
200
  def from_file(cls, filename):
 
201
    blob = ShaFile.from_file(filename)
 
202
    if blob._type != cls._type:
 
203
      raise NotBlobError(filename)
 
204
    return blob
 
205
 
 
206
  @classmethod
 
207
  def from_string(cls, string):
 
208
    """Create a blob from a string."""
 
209
    shafile = cls()
 
210
    shafile._text = string
 
211
    return shafile
 
212
 
 
213
 
 
214
class Tag(ShaFile):
 
215
  """A Git Tag object."""
 
216
 
 
217
  _type = TAG_ID
 
218
 
 
219
  @classmethod
 
220
  def from_file(cls, filename):
 
221
    blob = ShaFile.from_file(filename)
 
222
    if blob._type != cls._type:
 
223
      raise NotBlobError(filename)
 
224
    return blob
 
225
 
 
226
  @classmethod
 
227
  def from_string(cls, string):
 
228
    """Create a blob from a string."""
 
229
    shafile = cls()
 
230
    shafile._text = string
 
231
    return shafile
 
232
 
 
233
  def _parse_text(self):
 
234
    """Grab the metadata attached to the tag"""
 
235
    text = self._text
 
236
    count = 0
 
237
    assert text.startswith(OBJECT_ID), "Invalid tag object, " \
 
238
         "must start with %s" % OBJECT_ID
 
239
    count += len(OBJECT_ID)
 
240
    assert text[count] == ' ', "Invalid tag object, " \
 
241
         "%s must be followed by space not %s" % (OBJECT_ID, text[count])
 
242
    count += 1
 
243
    self._object_sha = text[count:count+40]
 
244
    count += 40
 
245
    assert text[count] == '\n', "Invalid tag object, " \
 
246
         "%s sha must be followed by newline" % OBJECT_ID
 
247
    count += 1
 
248
    assert text[count:].startswith(TYPE_ID), "Invalid tag object, " \
 
249
         "%s sha must be followed by %s" % (OBJECT_ID, TYPE_ID)
 
250
    count += len(TYPE_ID)
 
251
    assert text[count] == ' ', "Invalid tag object, " \
 
252
        "%s must be followed by space not %s" % (TAG_ID, text[count])
 
253
    count += 1
 
254
    self._object_type = ""
 
255
    while text[count] != '\n':
 
256
        self._object_type += text[count]
 
257
        count += 1
 
258
    count += 1
 
259
    assert self._object_type in (COMMIT_ID, BLOB_ID, TREE_ID, TAG_ID), "Invalid tag object, " \
 
260
        "unexpected object type %s" % self._object_type
 
261
    self._object_type = type_map[self._object_type]
 
262
 
 
263
    assert text[count:].startswith(TAG_ID), "Invalid tag object, " \
 
264
        "object type must be followed by %s" % (TAG_ID)
 
265
    count += len(TAG_ID)
 
266
    assert text[count] == ' ', "Invalid tag object, " \
 
267
        "%s must be followed by space not %s" % (TAG_ID, text[count])
 
268
    count += 1
 
269
    self._name = ""
 
270
    while text[count] != '\n':
 
271
        self._name += text[count]
 
272
        count += 1
 
273
    count += 1
 
274
 
 
275
    assert text[count:].startswith(TAGGER_ID), "Invalid tag object, " \
 
276
        "%s must be followed by %s" % (TAG_ID, TAGGER_ID)
 
277
    count += len(TAGGER_ID)
 
278
    assert text[count] == ' ', "Invalid tag object, " \
 
279
        "%s must be followed by space not %s" % (TAGGER_ID, text[count])
 
280
    count += 1
 
281
    self._tagger = ""
 
282
    while text[count] != '>':
 
283
        assert text[count] != '\n', "Malformed tagger information"
 
284
        self._tagger += text[count]
 
285
        count += 1
 
286
    self._tagger += text[count]
 
287
    count += 1
 
288
    assert text[count] == ' ', "Invalid tag object, " \
 
289
        "tagger information must be followed by space not %s" % text[count]
 
290
    count += 1
 
291
    self._tag_time = int(text[count:count+10])
 
292
    while text[count] != '\n':
 
293
        count += 1
 
294
    count += 1
 
295
    assert text[count] == '\n', "There must be a new line after the headers"
 
296
    count += 1
 
297
    self._message = text[count:]
 
298
 
 
299
  @property
 
300
  def object(self):
 
301
    """Returns the object pointed by this tag, represented as a tuple(type, sha)"""
 
302
    return (self._object_type, self._object_sha)
 
303
 
 
304
  @property
 
305
  def name(self):
 
306
    """Returns the name of this tag"""
 
307
    return self._name
 
308
 
 
309
  @property
 
310
  def tagger(self):
 
311
    """Returns the name of the person who created this tag"""
 
312
    return self._tagger
 
313
 
 
314
  @property
 
315
  def tag_time(self):
 
316
    """Returns the creation timestamp of the tag.
 
317
 
 
318
    Returns it as the number of seconds since the epoch"""
 
319
    return self._tag_time
 
320
 
 
321
  @property
 
322
  def message(self):
 
323
    """Returns the message attached to this tag"""
 
324
    return self._message
 
325
 
 
326
 
 
327
class Tree(ShaFile):
 
328
  """A Git tree object"""
 
329
 
 
330
  _type = TREE_ID
 
331
  _num_type = 2
 
332
 
 
333
  def __init__(self):
 
334
    self._entries = []
 
335
 
 
336
  @classmethod
 
337
  def from_file(cls, filename):
 
338
    tree = ShaFile.from_file(filename)
 
339
    if tree._type != cls._type:
 
340
      raise NotTreeError(filename)
 
341
    return tree
 
342
 
 
343
  def add(self, mode, name, hexsha):
 
344
    self._entries.append((mode, name, hexsha))
 
345
 
 
346
  def entries(self):
 
347
    """Return a list of tuples describing the tree entries"""
 
348
    return self._entries
 
349
 
 
350
  def _parse_text(self):
 
351
    """Grab the entries in the tree"""
 
352
    count = 0
 
353
    while count < len(self._text):
 
354
      mode = 0
 
355
      chr = self._text[count]
 
356
      while chr != ' ':
 
357
        assert chr >= '0' and chr <= '7', "%s is not a valid mode char" % chr
 
358
        mode = (mode << 3) + (ord(chr) - ord('0'))
 
359
        count += 1
 
360
        chr = self._text[count]
 
361
      count += 1
 
362
      chr = self._text[count]
 
363
      name = ''
 
364
      while chr != '\0':
 
365
        name += chr
 
366
        count += 1
 
367
        chr = self._text[count]
 
368
      count += 1
 
369
      chr = self._text[count]
 
370
      sha = self._text[count:count+20]
 
371
      hexsha = sha_to_hex(sha)
 
372
      self.add(mode, name, hexsha)
 
373
      count = count + 20
 
374
 
 
375
  def serialize(self):
 
376
    self._text = ""
 
377
    for mode, name, hexsha in self._entries:
 
378
        self._text += "%04o %s\0%s" % (mode, name, hex_to_sha(hexsha))
 
379
 
 
380
 
 
381
class Commit(ShaFile):
 
382
  """A git commit object"""
 
383
 
 
384
  _type = COMMIT_ID
 
385
  _num_type = 1
 
386
 
 
387
  def __init__(self):
 
388
    self._parents = []
 
389
 
 
390
  @classmethod
 
391
  def from_file(cls, filename):
 
392
    commit = ShaFile.from_file(filename)
 
393
    if commit._type != cls._type:
 
394
      raise NotCommitError(filename)
 
395
    return commit
 
396
 
 
397
  def _parse_text(self):
 
398
    text = self._text
 
399
    count = 0
 
400
    assert text.startswith(TREE_ID), "Invalid commit object, " \
 
401
         "must start with %s" % TREE_ID
 
402
    count += len(TREE_ID)
 
403
    assert text[count] == ' ', "Invalid commit object, " \
 
404
         "%s must be followed by space not %s" % (TREE_ID, text[count])
 
405
    count += 1
 
406
    self._tree = text[count:count+40]
 
407
    count = count + 40
 
408
    assert text[count] == "\n", "Invalid commit object, " \
 
409
         "tree sha must be followed by newline"
 
410
    count += 1
 
411
    self._parents = []
 
412
    while text[count:].startswith(PARENT_ID):
 
413
      count += len(PARENT_ID)
 
414
      assert text[count] == ' ', "Invalid commit object, " \
 
415
           "%s must be followed by space not %s" % (PARENT_ID, text[count])
 
416
      count += 1
 
417
      self._parents.append(text[count:count+40])
 
418
      count += 40
 
419
      assert text[count] == "\n", "Invalid commit object, " \
 
420
           "parent sha must be followed by newline"
 
421
      count += 1
 
422
    self._author = None
 
423
    if text[count:].startswith(AUTHOR_ID):
 
424
      count += len(AUTHOR_ID)
 
425
      assert text[count] == ' ', "Invalid commit object, " \
 
426
           "%s must be followed by space not %s" % (AUTHOR_ID, text[count])
 
427
      count += 1
 
428
      self._author = ''
 
429
      while text[count] != '>':
 
430
        assert text[count] != '\n', "Malformed author information"
 
431
        self._author += text[count]
 
432
        count += 1
 
433
      self._author += text[count]
 
434
      count += 1
 
435
      while text[count] != '\n':
 
436
        count += 1
 
437
      count += 1
 
438
    self._committer = None
 
439
    if text[count:].startswith(COMMITTER_ID):
 
440
      count += len(COMMITTER_ID)
 
441
      assert text[count] == ' ', "Invalid commit object, " \
 
442
           "%s must be followed by space not %s" % (COMMITTER_ID, text[count])
 
443
      count += 1
 
444
      self._committer = ''
 
445
      while text[count] != '>':
 
446
        assert text[count] != '\n', "Malformed committer information"
 
447
        self._committer += text[count]
 
448
        count += 1
 
449
      self._committer += text[count]
 
450
      count += 1
 
451
      assert text[count] == ' ', "Invalid commit object, " \
 
452
           "commiter information must be followed by space not %s" % text[count]
 
453
      count += 1
 
454
      self._commit_time = int(text[count:count+10])
 
455
      while text[count] != '\n':
 
456
        count += 1
 
457
      count += 1
 
458
    assert text[count] == '\n', "There must be a new line after the headers"
 
459
    count += 1
 
460
    # XXX: There can be an encoding field.
 
461
    self._message = text[count:]
 
462
 
 
463
  def serialize(self):
 
464
    self._text = ""
 
465
    self._text += "%s %s\n" % (TREE_ID, self._tree)
 
466
    for p in self._parents:
 
467
      self._text += "%s %s\n" % (PARENT_ID, p)
 
468
    self._text += "%s %s %s +0000\n" % (AUTHOR_ID, self._author, str(self._commit_time))
 
469
    self._text += "%s %s %s +0000\n" % (COMMITTER_ID, self._committer, str(self._commit_time))
 
470
    self._text += "\n" # There must be a new line after the headers
 
471
    self._text += self._message
 
472
 
 
473
  @property
 
474
  def tree(self):
 
475
    """Returns the tree that is the state of this commit"""
 
476
    return self._tree
 
477
 
 
478
  @property
 
479
  def parents(self):
 
480
    """Return a list of parents of this commit."""
 
481
    return self._parents
 
482
 
 
483
  @property
 
484
  def author(self):
 
485
    """Returns the name of the author of the commit"""
 
486
    return self._author
 
487
 
 
488
  @property
 
489
  def committer(self):
 
490
    """Returns the name of the committer of the commit"""
 
491
    return self._committer
 
492
 
 
493
  @property
 
494
  def message(self):
 
495
    """Returns the commit message"""
 
496
    return self._message
 
497
 
 
498
  @property
 
499
  def commit_time(self):
 
500
    """Returns the timestamp of the commit.
 
501
    
 
502
    Returns it as the number of seconds since the epoch.
 
503
    """
 
504
    return self._commit_time
 
505
 
 
506
 
 
507
type_map = {
 
508
  BLOB_ID : Blob,
 
509
  TREE_ID : Tree,
 
510
  COMMIT_ID : Commit,
 
511
  TAG_ID: Tag,
 
512
}
 
513
 
 
514
num_type_map = {
 
515
  0: None,
 
516
  1: Commit,
 
517
  2: Tree,
 
518
  3: Blob,
 
519
  4: Tag,
 
520
  # 5 Is reserved for further expansion
 
521
}
 
522