/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: 2008-12-08 23:24:15 UTC
  • mto: (0.215.1 trunk)
  • mto: This revision was merged to the branch mainline in revision 6960.
  • Revision ID: jelmer@samba.org-20081208232415-odeovndmt9vvsfik
remove silly build-inplace target.

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
 
# 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
 
TAG_ID = "tag"
34
 
TREE_ID = "tree"
35
 
COMMIT_ID = "commit"
36
 
PARENT_ID = "parent"
37
 
AUTHOR_ID = "author"
38
 
COMMITTER_ID = "committer"
39
 
 
40
 
def _decompress(string):
41
 
    dcomp = zlib.decompressobj()
42
 
    dcomped = dcomp.decompress(string)
43
 
    dcomped += dcomp.flush()
44
 
    return dcomped
45
 
 
46
 
def sha_to_hex(sha):
47
 
  """Takes a string and returns the hex of the sha within"""
48
 
  hexsha = ''
49
 
  for c in sha:
50
 
    hexsha += "%02x" % ord(c)
51
 
  assert len(hexsha) == 40, "Incorrect length of sha1 string: %d" % \
52
 
         len(hexsha)
53
 
  return hexsha
54
 
 
55
 
 
56
 
class ShaFile(object):
57
 
  """A git SHA file."""
58
 
 
59
 
  @classmethod
60
 
  def _parse_legacy_object(cls, map):
61
 
    """Parse a legacy object, creating it and setting object._text"""
62
 
    text = _decompress(map)
63
 
    object = None
64
 
    for posstype in type_map.keys():
65
 
      if text.startswith(posstype):
66
 
        object = type_map[posstype]()
67
 
        text = text[len(posstype):]
68
 
        break
69
 
    assert object is not None, "%s is not a known object type" % text[:9]
70
 
    assert text[0] == ' ', "%s is not a space" % text[0]
71
 
    text = text[1:]
72
 
    size = 0
73
 
    i = 0
74
 
    while text[0] >= '0' and text[0] <= '9':
75
 
      if i > 0 and size == 0:
76
 
        assert False, "Size is not in canonical format"
77
 
      size = (size * 10) + int(text[0])
78
 
      text = text[1:]
79
 
      i += 1
80
 
    object._size = size
81
 
    assert text[0] == "\0", "Size not followed by null"
82
 
    text = text[1:]
83
 
    object._text = text
84
 
    return object
85
 
 
86
 
  def as_raw_string(self):
87
 
    return self._num_type, self._text
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
 
    return object
106
 
 
107
 
  @classmethod
108
 
  def _parse_file(cls, map):
109
 
    word = (ord(map[0]) << 8) + ord(map[1])
110
 
    if ord(map[0]) == 0x78 and (word % 31) == 0:
111
 
      return cls._parse_legacy_object(map)
112
 
    else:
113
 
      return cls._parse_object(map)
114
 
 
115
 
  def __init__(self):
116
 
    """Don't call this directly"""
117
 
 
118
 
  def _parse_text(self):
119
 
    """For subclasses to do initialisation time parsing"""
120
 
 
121
 
  @classmethod
122
 
  def from_file(cls, filename):
123
 
    """Get the contents of a SHA file on disk"""
124
 
    size = os.path.getsize(filename)
125
 
    f = open(filename, 'rb')
126
 
    try:
127
 
      map = mmap.mmap(f.fileno(), size, access=mmap.ACCESS_READ)
128
 
      shafile = cls._parse_file(map)
129
 
      shafile._parse_text()
130
 
      return shafile
131
 
    finally:
132
 
      f.close()
133
 
 
134
 
  @classmethod
135
 
  def from_raw_string(cls, type, string):
136
 
    """Creates an object of the indicated type from the raw string given.
137
 
 
138
 
    Type is the numeric type of an object. String is the raw uncompressed
139
 
    contents.
140
 
    """
141
 
    real_class = num_type_map[type]
142
 
    obj = real_class()
143
 
    obj._num_type = type
144
 
    obj._text = string
145
 
    obj._parse_text()
146
 
    return obj
147
 
 
148
 
  def _header(self):
149
 
    return "%s %lu\0" % (self._type, len(self._text))
150
 
 
151
 
  def crc32(self):
152
 
    return zlib.crc32(self._text)
153
 
 
154
 
  def sha(self):
155
 
    """The SHA1 object that is the name of this object."""
156
 
    ressha = sha.new()
157
 
    ressha.update(self._header())
158
 
    ressha.update(self._text)
159
 
    return ressha
160
 
 
161
 
  @property
162
 
  def id(self):
163
 
      return self.sha().hexdigest()
164
 
 
165
 
  def __repr__(self):
166
 
    return "<%s %s>" % (self.__class__.__name__, self.id)
167
 
 
168
 
  def __eq__(self, other):
169
 
    """Return true id the sha of the two objects match.
170
 
 
171
 
    The __le__ etc methods aren't overriden as they make no sense,
172
 
    certainly at this level.
173
 
    """
174
 
    return self.sha().digest() == other.sha().digest()
175
 
 
176
 
 
177
 
class Blob(ShaFile):
178
 
  """A Git Blob object."""
179
 
 
180
 
  _type = BLOB_ID
181
 
 
182
 
  @property
183
 
  def data(self):
184
 
    """The text contained within the blob object."""
185
 
    return self._text
186
 
 
187
 
  @classmethod
188
 
  def from_file(cls, filename):
189
 
    blob = ShaFile.from_file(filename)
190
 
    if blob._type != cls._type:
191
 
      raise NotBlobError(filename)
192
 
    return blob
193
 
 
194
 
  @classmethod
195
 
  def from_string(cls, string):
196
 
    """Create a blob from a string."""
197
 
    shafile = cls()
198
 
    shafile._text = string
199
 
    return shafile
200
 
 
201
 
 
202
 
class Tag(ShaFile):
203
 
  """A Git Tag object."""
204
 
 
205
 
  _type = TAG_ID
206
 
 
207
 
  @classmethod
208
 
  def from_file(cls, filename):
209
 
    blob = ShaFile.from_file(filename)
210
 
    if blob._type != cls._type:
211
 
      raise NotBlobError(filename)
212
 
    return blob
213
 
 
214
 
  @classmethod
215
 
  def from_string(cls, string):
216
 
    """Create a blob from a string."""
217
 
    shafile = cls()
218
 
    shafile._text = string
219
 
    return shafile
220
 
 
221
 
 
222
 
class Tree(ShaFile):
223
 
  """A Git tree object"""
224
 
 
225
 
  _type = TREE_ID
226
 
 
227
 
  @classmethod
228
 
  def from_file(cls, filename):
229
 
    tree = ShaFile.from_file(filename)
230
 
    if tree._type != cls._type:
231
 
      raise NotTreeError(filename)
232
 
    return tree
233
 
 
234
 
  def entries(self):
235
 
    """Return a list of tuples describing the tree entries"""
236
 
    return self._entries
237
 
 
238
 
  def _parse_text(self):
239
 
    """Grab the entries in the tree"""
240
 
    self._entries = []
241
 
    count = 0
242
 
    while count < len(self._text):
243
 
      mode = 0
244
 
      chr = self._text[count]
245
 
      while chr != ' ':
246
 
        assert chr >= '0' and chr <= '7', "%s is not a valid mode char" % chr
247
 
        mode = (mode << 3) + (ord(chr) - ord('0'))
248
 
        count += 1
249
 
        chr = self._text[count]
250
 
      count += 1
251
 
      chr = self._text[count]
252
 
      name = ''
253
 
      while chr != '\0':
254
 
        name += chr
255
 
        count += 1
256
 
        chr = self._text[count]
257
 
      count += 1
258
 
      chr = self._text[count]
259
 
      sha = self._text[count:count+20]
260
 
      hexsha = sha_to_hex(sha)
261
 
      self._entries.append((mode, name, hexsha))
262
 
      count = count + 20
263
 
 
264
 
class Commit(ShaFile):
265
 
  """A git commit object"""
266
 
 
267
 
  _type = COMMIT_ID
268
 
 
269
 
  @classmethod
270
 
  def from_file(cls, filename):
271
 
    commit = ShaFile.from_file(filename)
272
 
    if commit._type != cls._type:
273
 
      raise NotCommitError(filename)
274
 
    return commit
275
 
 
276
 
  def _parse_text(self):
277
 
    text = self._text
278
 
    count = 0
279
 
    assert text.startswith(TREE_ID), "Invalid commit object, " \
280
 
         "must start with %s" % TREE_ID
281
 
    count += len(TREE_ID)
282
 
    assert text[count] == ' ', "Invalid commit object, " \
283
 
         "%s must be followed by space not %s" % (TREE_ID, text[count])
284
 
    count += 1
285
 
    self._tree = text[count:count+40]
286
 
    count = count + 40
287
 
    assert text[count] == "\n", "Invalid commit object, " \
288
 
         "tree sha must be followed by newline"
289
 
    count += 1
290
 
    self._parents = []
291
 
    while text[count:].startswith(PARENT_ID):
292
 
      count += len(PARENT_ID)
293
 
      assert text[count] == ' ', "Invalid commit object, " \
294
 
           "%s must be followed by space not %s" % (PARENT_ID, text[count])
295
 
      count += 1
296
 
      self._parents.append(text[count:count+40])
297
 
      count += 40
298
 
      assert text[count] == "\n", "Invalid commit object, " \
299
 
           "parent sha must be followed by newline"
300
 
      count += 1
301
 
    self._author = None
302
 
    if text[count:].startswith(AUTHOR_ID):
303
 
      count += len(AUTHOR_ID)
304
 
      assert text[count] == ' ', "Invalid commit object, " \
305
 
           "%s must be followed by space not %s" % (AUTHOR_ID, text[count])
306
 
      count += 1
307
 
      self._author = ''
308
 
      while text[count] != '>':
309
 
        assert text[count] != '\n', "Malformed author information"
310
 
        self._author += text[count]
311
 
        count += 1
312
 
      self._author += text[count]
313
 
      count += 1
314
 
      while text[count] != '\n':
315
 
        count += 1
316
 
      count += 1
317
 
    self._committer = None
318
 
    if text[count:].startswith(COMMITTER_ID):
319
 
      count += len(COMMITTER_ID)
320
 
      assert text[count] == ' ', "Invalid commit object, " \
321
 
           "%s must be followed by space not %s" % (COMMITTER_ID, text[count])
322
 
      count += 1
323
 
      self._committer = ''
324
 
      while text[count] != '>':
325
 
        assert text[count] != '\n', "Malformed committer information"
326
 
        self._committer += text[count]
327
 
        count += 1
328
 
      self._committer += text[count]
329
 
      count += 1
330
 
      assert text[count] == ' ', "Invalid commit object, " \
331
 
           "commiter information must be followed by space not %s" % text[count]
332
 
      count += 1
333
 
      self._commit_time = int(text[count:count+10])
334
 
      while text[count] != '\n':
335
 
        count += 1
336
 
      count += 1
337
 
    assert text[count] == '\n', "There must be a new line after the headers"
338
 
    count += 1
339
 
    # XXX: There can be an encoding field.
340
 
    self._message = text[count:]
341
 
 
342
 
  @property
343
 
  def tree(self):
344
 
    """Returns the tree that is the state of this commit"""
345
 
    return self._tree
346
 
 
347
 
  @property
348
 
  def parents(self):
349
 
    """Return a list of parents of this commit."""
350
 
    return self._parents
351
 
 
352
 
  @property
353
 
  def author(self):
354
 
    """Returns the name of the author of the commit"""
355
 
    return self._author
356
 
 
357
 
  @property
358
 
  def committer(self):
359
 
    """Returns the name of the committer of the commit"""
360
 
    return self._committer
361
 
 
362
 
  @property
363
 
  def message(self):
364
 
    """Returns the commit message"""
365
 
    return self._message
366
 
 
367
 
  @property
368
 
  def commit_time(self):
369
 
    """Returns the timestamp of the commit.
370
 
    
371
 
    Returns it as the number of seconds since the epoch.
372
 
    """
373
 
    return self._commit_time
374
 
 
375
 
type_map = {
376
 
  BLOB_ID : Blob,
377
 
  TREE_ID : Tree,
378
 
  COMMIT_ID : Commit,
379
 
  TAG_ID: Tag,
380
 
}
381
 
 
382
 
num_type_map = {
383
 
  0: None,
384
 
  1: Commit,
385
 
  2: Tree,
386
 
  3: Blob,
387
 
  4: Tag,
388
 
  # 5 Is reserved for further expansion
389
 
}
390