/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 git/objects.py

  • Committer: James Westby
  • Date: 2007-03-25 11:45:49 UTC
  • mto: (0.215.1 trunk)
  • mto: This revision was merged to the branch mainline in revision 6960.
  • Revision ID: jw+debian@jameswestby.net-20070325114549-jl2t51a668wssrhb
Start the python-git project.

Aims to give an interface to git repos that doesn't call out to git directly.
Probably going to be pure python.

Currently can read blobs, trees and commits from the files. It reads both
legacy and new headers. However it is untested for anything but the simple
case.

Can also understand a little about the repository format.

The testsuite uses the nosetests program from Turbogears, as I got annoyed
trying to set up unittest.

Open up a repo by passing it the path to the .git dir. You can then ask for
HEAD with repo.head() or a ref with repo.ref(name). Both return the SHA id
they currently point to. You can then grab this object with
repo.get_object(sha).

For the actual objects the ShaFile.from_file(filename) will return the object
stored in the file whatever it is. To ensure you get the correct type then
call {Blob,Tree,Commit}.from_file(filename). I will add repo methods to do
this for you with file lookup soon.

There is also support for creating blobs. Blob.from_string(string) will create
a blob object from the string. You can then call blob.sha() to get the sha
object for this blob, and hexdigest() on that will get its ID. There is
currently no method that allows you to write it out though.

Everything is currently done with assertions, where much of it should probably
be exceptions. This was merely done for expediency. If you hit an assertion,
it either means you have done something wrong, there is corruption, or
you are trying an unsupported operation.

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
1
import mmap
23
2
import os
24
3
import sha
25
4
import zlib
26
5
 
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"
 
6
blob_id = "blob"
 
7
tree_id = "tree"
 
8
commit_id = "commit"
 
9
parent_id = "parent"
 
10
author_id = "author"
 
11
committer_id = "committer"
39
12
 
40
13
def _decompress(string):
41
14
    dcomp = zlib.decompressobj()
47
20
  """Takes a string and returns the hex of the sha within"""
48
21
  hexsha = ''
49
22
  for c in sha:
50
 
    hexsha += "%02x" % ord(c)
 
23
    if ord(c) < 16:
 
24
      hexsha += "0%x" % ord(c)
 
25
    else:
 
26
      hexsha += "%x" % ord(c)
51
27
  assert len(hexsha) == 40, "Incorrect length of sha1 string: %d" % \
52
28
         len(hexsha)
53
29
  return hexsha
54
30
 
55
 
 
56
31
class ShaFile(object):
57
32
  """A git SHA file."""
58
33
 
 
34
  def _update_contents(self):
 
35
    """Update the _contents from the _text"""
 
36
    self._contents = [ord(c) for c in self._text]
 
37
 
59
38
  @classmethod
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"
82
61
    text = text[1:]
83
62
    object._text = text
 
63
    object._update_contents()
84
64
    return object
85
65
 
86
 
  def as_raw_string(self):
87
 
    return self._num_type, self._text
88
 
 
89
66
  @classmethod
90
67
  def _parse_object(cls, map):
91
68
    """Parse a new style object , creating it and setting object._text"""
102
79
      used += 1
103
80
    raw = map[used:]
104
81
    object._text = _decompress(raw)
 
82
    object._update_contents()
105
83
    return object
106
84
 
107
85
  @classmethod
116
94
    """Don't call this directly"""
117
95
 
118
96
  def _parse_text(self):
119
 
    """For subclasses to do initialisation time parsing"""
 
97
    """For subclasses to do initialistion time parsing"""
120
98
 
121
99
  @classmethod
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+')
126
104
    try:
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()
130
108
      return shafile
131
109
    finally:
132
110
      f.close()
133
111
 
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
112
  def _header(self):
149
 
    return "%s %lu\0" % (self._type, len(self._text))
 
113
    return "%s %lu\0" % (self._type, len(self._contents))
150
114
 
151
 
  def crc32(self):
152
 
    return zlib.crc32(self._text)
 
115
  def contents(self):
 
116
    """The raw bytes of this object"""
 
117
    return self._contents
153
118
 
154
119
  def sha(self):
155
120
    """The SHA1 object that is the name of this object."""
158
123
    ressha.update(self._text)
159
124
    return ressha
160
125
 
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
126
class Blob(ShaFile):
178
127
  """A Git Blob object."""
179
128
 
180
 
  _type = BLOB_ID
 
129
  _type = blob_id
181
130
 
182
 
  @property
183
 
  def data(self):
 
131
  def text(self):
184
132
    """The text contained within the blob object."""
185
133
    return self._text
186
134
 
187
135
  @classmethod
188
136
  def from_file(cls, filename):
189
137
    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
 
 
 
138
    assert blob._type == cls._type, "%s is not a blob object" % filename
 
139
    return blob
 
140
 
 
141
  @classmethod
 
142
  def from_string(cls, string):
 
143
    """Create a blob from a string."""
 
144
    shafile = cls()
 
145
    shafile._text = string
 
146
    shafile._update_contents()
 
147
    return shafile
221
148
 
222
149
class Tree(ShaFile):
223
150
  """A Git tree object"""
224
151
 
225
 
  _type = TREE_ID
 
152
  _type = tree_id
226
153
 
227
154
  @classmethod
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
232
158
    return tree
233
159
 
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
237
163
 
238
164
  def _parse_text(self):
264
190
class Commit(ShaFile):
265
191
  """A git commit object"""
266
192
 
267
 
  _type = COMMIT_ID
 
193
  _type = commit_id
268
194
 
269
195
  @classmethod
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
274
199
    return commit
275
200
 
276
201
  def _parse_text(self):
277
202
    text = self._text
278
203
    count = 0
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])
284
209
    count += 1
285
210
    self._tree = text[count:count+40]
286
211
    count = count + 40
288
213
         "tree sha must be followed by newline"
289
214
    count += 1
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])
295
220
      count += 1
296
221
      self._parents.append(text[count:count+40])
297
222
      count += 40
299
224
           "parent sha must be followed by newline"
300
225
      count += 1
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])
306
231
      count += 1
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]
311
235
        count += 1
312
 
      self._author += text[count]
313
 
      count += 1
314
 
      while text[count] != '\n':
315
 
        count += 1
316
236
      count += 1
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])
322
242
      count += 1
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]
327
246
        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
247
      count += 1
337
248
    assert text[count] == '\n', "There must be a new line after the headers"
338
249
    count += 1
339
 
    # XXX: There can be an encoding field.
340
250
    self._message = text[count:]
341
251
 
342
 
  @property
343
252
  def tree(self):
344
253
    """Returns the tree that is the state of this commit"""
345
254
    return self._tree
346
255
 
347
 
  @property
348
256
  def parents(self):
349
257
    """Return a list of parents of this commit."""
350
258
    return self._parents
351
259
 
352
 
  @property
353
260
  def author(self):
354
261
    """Returns the name of the author of the commit"""
355
262
    return self._author
356
263
 
357
 
  @property
358
264
  def committer(self):
359
265
    """Returns the name of the committer of the commit"""
360
266
    return self._committer
361
267
 
362
 
  @property
363
268
  def message(self):
364
269
    """Returns the commit message"""
365
270
    return self._message
366
271
 
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
272
type_map = {
376
 
  BLOB_ID : Blob,
377
 
  TREE_ID : Tree,
378
 
  COMMIT_ID : Commit,
379
 
  TAG_ID: Tag,
 
273
  blob_id : Blob,
 
274
  tree_id : Tree,
 
275
  commit_id : Commit,
380
276
}
381
277
 
382
278
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
 
279
  1 : Commit,
 
280
  2 : Tree,
 
281
  3 : Blob,
389
282
}
390
283