/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-11 10:52:43 UTC
  • mto: (0.215.1 trunk)
  • mto: This revision was merged to the branch mainline in revision 6960.
  • Revision ID: jelmer@samba.org-20081211105243-dokx6i1dofwnlrrm
Add simple pack dump utility.

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
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
 
 
171
class Blob(ShaFile):
 
172
  """A Git Blob object."""
 
173
 
 
174
  _type = blob_id
 
175
 
 
176
  def text(self):
 
177
    """The text contained within the blob object."""
 
178
    return self._text
 
179
 
 
180
  @classmethod
 
181
  def from_file(cls, filename):
 
182
    blob = ShaFile.from_file(filename)
 
183
    if blob._type != cls._type:
 
184
      raise NotBlobError(filename)
 
185
    return blob
 
186
 
 
187
  @classmethod
 
188
  def from_string(cls, string):
 
189
    """Create a blob from a string."""
 
190
    shafile = cls()
 
191
    shafile._text = string
 
192
    shafile._update_contents()
 
193
    return shafile
 
194
 
 
195
 
 
196
class Tree(ShaFile):
 
197
  """A Git tree object"""
 
198
 
 
199
  _type = tree_id
 
200
 
 
201
  @classmethod
 
202
  def from_file(cls, filename):
 
203
    tree = ShaFile.from_file(filename)
 
204
    if tree._type != cls._type:
 
205
      raise NotTreeError(filename)
 
206
    return tree
 
207
 
 
208
  def entries(self):
 
209
    """Reutrn a list of tuples describing the tree entries"""
 
210
    return self._entries
 
211
 
 
212
  def _parse_text(self):
 
213
    """Grab the entries in the tree"""
 
214
    self._entries = []
 
215
    count = 0
 
216
    while count < len(self._text):
 
217
      mode = 0
 
218
      chr = self._text[count]
 
219
      while chr != ' ':
 
220
        assert chr >= '0' and chr <= '7', "%s is not a valid mode char" % chr
 
221
        mode = (mode << 3) + (ord(chr) - ord('0'))
 
222
        count += 1
 
223
        chr = self._text[count]
 
224
      count += 1
 
225
      chr = self._text[count]
 
226
      name = ''
 
227
      while chr != '\0':
 
228
        name += chr
 
229
        count += 1
 
230
        chr = self._text[count]
 
231
      count += 1
 
232
      chr = self._text[count]
 
233
      sha = self._text[count:count+20]
 
234
      hexsha = sha_to_hex(sha)
 
235
      self._entries.append((mode, name, hexsha))
 
236
      count = count + 20
 
237
 
 
238
class Commit(ShaFile):
 
239
  """A git commit object"""
 
240
 
 
241
  _type = commit_id
 
242
 
 
243
  @classmethod
 
244
  def from_file(cls, filename):
 
245
    commit = ShaFile.from_file(filename)
 
246
    if commit._type != cls._type:
 
247
      raise NotCommitError(filename)
 
248
    return commit
 
249
 
 
250
  def _parse_text(self):
 
251
    text = self._text
 
252
    count = 0
 
253
    assert text.startswith(tree_id), "Invlid commit object, " \
 
254
         "must start with %s" % tree_id
 
255
    count += len(tree_id)
 
256
    assert text[count] == ' ', "Invalid commit object, " \
 
257
         "%s must be followed by space not %s" % (tree_id, text[count])
 
258
    count += 1
 
259
    self._tree = text[count:count+40]
 
260
    count = count + 40
 
261
    assert text[count] == "\n", "Invalid commit object, " \
 
262
         "tree sha must be followed by newline"
 
263
    count += 1
 
264
    self._parents = []
 
265
    while text[count:].startswith(parent_id):
 
266
      count += len(parent_id)
 
267
      assert text[count] == ' ', "Invalid commit object, " \
 
268
           "%s must be followed by space not %s" % (parent_id, text[count])
 
269
      count += 1
 
270
      self._parents.append(text[count:count+40])
 
271
      count += 40
 
272
      assert text[count] == "\n", "Invalid commit object, " \
 
273
           "parent sha must be followed by newline"
 
274
      count += 1
 
275
    self._author = None
 
276
    if text[count:].startswith(author_id):
 
277
      count += len(author_id)
 
278
      assert text[count] == ' ', "Invalid commit object, " \
 
279
           "%s must be followed by space not %s" % (author_id, text[count])
 
280
      count += 1
 
281
      self._author = ''
 
282
      while text[count] != '>':
 
283
        assert text[count] != '\n', "Malformed author information"
 
284
        self._author += text[count]
 
285
        count += 1
 
286
      self._author += text[count]
 
287
      count += 1
 
288
      while text[count] != '\n':
 
289
        count += 1
 
290
      count += 1
 
291
    self._committer = None
 
292
    if text[count:].startswith(committer_id):
 
293
      count += len(committer_id)
 
294
      assert text[count] == ' ', "Invalid commit object, " \
 
295
           "%s must be followed by space not %s" % (committer_id, text[count])
 
296
      count += 1
 
297
      self._committer = ''
 
298
      while text[count] != '>':
 
299
        assert text[count] != '\n', "Malformed committer information"
 
300
        self._committer += text[count]
 
301
        count += 1
 
302
      self._committer += text[count]
 
303
      count += 1
 
304
      assert text[count] == ' ', "Invalid commit object, " \
 
305
           "commiter information must be followed by space not %s" % text[count]
 
306
      count += 1
 
307
      self._commit_time = int(text[count:count+10])
 
308
      while text[count] != '\n':
 
309
        count += 1
 
310
      count += 1
 
311
    assert text[count] == '\n', "There must be a new line after the headers"
 
312
    count += 1
 
313
    # XXX: There can be an encoding field.
 
314
    self._message = text[count:]
 
315
 
 
316
  def tree(self):
 
317
    """Returns the tree that is the state of this commit"""
 
318
    return self._tree
 
319
 
 
320
  def parents(self):
 
321
    """Return a list of parents of this commit."""
 
322
    return self._parents
 
323
 
 
324
  def author(self):
 
325
    """Returns the name of the author of the commit"""
 
326
    return self._author
 
327
 
 
328
  def committer(self):
 
329
    """Returns the name of the committer of the commit"""
 
330
    return self._committer
 
331
 
 
332
  def message(self):
 
333
    """Returns the commit message"""
 
334
    return self._message
 
335
 
 
336
  def commit_time(self):
 
337
    """Returns the timestamp of the commit.
 
338
    
 
339
    Returns it as the number of seconds since the epoch.
 
340
    """
 
341
    return self._commit_time
 
342
 
 
343
type_map = {
 
344
  blob_id : Blob,
 
345
  tree_id : Tree,
 
346
  commit_id : Commit,
 
347
}
 
348
 
 
349
num_type_map = {
 
350
  1 : Commit,
 
351
  2 : Tree,
 
352
  3 : Blob,
 
353
}
 
354