Lazily read the contents of ShaFiles from disk.
[jelmer/dulwich-libgit2.git] / dulwich / objects.py
1 # objects.py -- Access to base git objects
2 # Copyright (C) 2007 James Westby <jw+debian@jameswestby.net>
3 # Copyright (C) 2008-2009 Jelmer Vernooij <jelmer@samba.org>
4 #
5 # This program is free software; you can redistribute it and/or
6 # modify it under the terms of the GNU General Public License
7 # as published by the Free Software Foundation; version 2
8 # of the License or (at your option) a later version of the License.
9 #
10 # This program is distributed in the hope that it will be useful,
11 # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13 # GNU General Public License for more details.
14 #
15 # You should have received a copy of the GNU General Public License
16 # along with this program; if not, write to the Free Software
17 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
18 # MA  02110-1301, USA.
19
20
21 """Access to base git objects."""
22
23
24 import binascii
25 from cStringIO import (
26     StringIO,
27     )
28 import mmap
29 import os
30 import stat
31 import zlib
32
33 from dulwich.errors import (
34     NotBlobError,
35     NotCommitError,
36     NotTagError,
37     NotTreeError,
38     ObjectFormatException,
39     )
40 from dulwich.file import GitFile
41 from dulwich.misc import (
42     make_sha,
43     )
44
45
46 # Header fields for commits
47 _TREE_HEADER = "tree"
48 _PARENT_HEADER = "parent"
49 _AUTHOR_HEADER = "author"
50 _COMMITTER_HEADER = "committer"
51 _ENCODING_HEADER = "encoding"
52
53
54 # Header fields for objects
55 _OBJECT_HEADER = "object"
56 _TYPE_HEADER = "type"
57 _TAG_HEADER = "tag"
58 _TAGGER_HEADER = "tagger"
59
60
61 S_IFGITLINK = 0160000
62
63 def S_ISGITLINK(m):
64     return (stat.S_IFMT(m) == S_IFGITLINK)
65
66
67 def _decompress(string):
68     dcomp = zlib.decompressobj()
69     dcomped = dcomp.decompress(string)
70     dcomped += dcomp.flush()
71     return dcomped
72
73
74 def sha_to_hex(sha):
75     """Takes a string and returns the hex of the sha within"""
76     hexsha = binascii.hexlify(sha)
77     assert len(hexsha) == 40, "Incorrect length of sha1 string: %d" % hexsha
78     return hexsha
79
80
81 def hex_to_sha(hex):
82     """Takes a hex sha and returns a binary sha"""
83     assert len(hex) == 40, "Incorrent length of hexsha: %s" % hex
84     return binascii.unhexlify(hex)
85
86
87 def serializable_property(name, docstring=None):
88     def set(obj, value):
89         obj._ensure_parsed()
90         setattr(obj, "_"+name, value)
91         obj._needs_serialization = True
92     def get(obj):
93         obj._ensure_parsed()
94         return getattr(obj, "_"+name)
95     return property(get, set, doc=docstring)
96
97
98 def object_class(type):
99     """Get the object class corresponding to the given type.
100
101     :param type: Either a type name string or a numeric type.
102     :return: The ShaFile subclass corresponding to the given type, or None if
103         type is not a valid type name/number.
104     """
105     return _TYPE_MAP.get(type, None)
106
107
108 def check_hexsha(hex, error_msg):
109     try:
110         hex_to_sha(hex)
111     except (TypeError, AssertionError):
112         raise ObjectFormatException("%s %s" % (error_msg, hex))
113
114
115 def check_identity(identity, error_msg):
116     email_start = identity.find("<")
117     email_end = identity.find(">")
118     if (email_start < 0 or email_end < 0 or email_end <= email_start
119         or identity.find("<", email_start + 1) >= 0
120         or identity.find(">", email_end + 1) >= 0
121         or not identity.endswith(">")):
122         raise ObjectFormatException(error_msg)
123
124
125 class ShaFile(object):
126     """A git SHA file."""
127
128     @staticmethod
129     def _parse_legacy_object_header(magic, f):
130         """Parse a legacy object, creating it but not reading the file."""
131         bufsize = 1024
132         decomp = zlib.decompressobj()
133         header = decomp.decompress(magic)
134         start = 0
135         end = -1
136         while end < 0:
137             header += decomp.decompress(f.read(bufsize))
138             end = header.find("\0", start)
139             start = len(header)
140         header = header[:end]
141         type_name, size = header.split(" ", 1)
142         size = int(size)  # sanity check
143         obj_class = object_class(type_name)
144         if not obj_class:
145             raise ObjectFormatException("Not a known type: %s" % type_name)
146         obj = obj_class()
147         obj._filename = f.name
148         return obj
149
150     def _parse_legacy_object(self, f):
151         """Parse a legacy object, setting the raw string."""
152         size = os.path.getsize(f.name)
153         map = mmap.mmap(f.fileno(), size, access=mmap.ACCESS_READ)
154         try:
155             text = _decompress(map)
156         finally:
157             map.close()
158         header_end = text.find('\0')
159         if header_end < 0:
160             raise ObjectFormatException("Invalid object header")
161         self.set_raw_string(text[header_end+1:])
162
163     def as_legacy_object_chunks(self):
164         compobj = zlib.compressobj()
165         yield compobj.compress(self._header())
166         for chunk in self.as_raw_chunks():
167             yield compobj.compress(chunk)
168         yield compobj.flush()
169
170     def as_legacy_object(self):
171         return "".join(self.as_legacy_object_chunks())
172
173     def as_raw_chunks(self):
174         if self._needs_parsing:
175             self._ensure_parsed()
176         else:
177             self._chunked_text = self._serialize()
178         return self._chunked_text
179
180     def as_raw_string(self):
181         return "".join(self.as_raw_chunks())
182
183     def __str__(self):
184         return self.as_raw_string()
185
186     def __hash__(self):
187         return hash(self.id)
188
189     def as_pretty_string(self):
190         return self.as_raw_string()
191
192     def _ensure_parsed(self):
193         if self._needs_parsing:
194             if not self._chunked_text:
195                 assert self._filename, "ShaFile needs either text or filename"
196                 self._parse_file()
197             self._deserialize(self._chunked_text)
198             self._needs_parsing = False
199
200     def set_raw_string(self, text):
201         if type(text) != str:
202             raise TypeError(text)
203         self.set_raw_chunks([text])
204
205     def set_raw_chunks(self, chunks):
206         self._chunked_text = chunks
207         self._sha = None
208         self._needs_parsing = True
209         self._needs_serialization = False
210
211     @staticmethod
212     def _parse_object_header(magic, f):
213         """Parse a new style object, creating it but not reading the file."""
214         num_type = (ord(magic[0]) >> 4) & 7
215         obj_class = object_class(num_type)
216         if not obj_class:
217             raise ObjectFormatError("Not a known type: %d" % num_type)
218         obj = obj_class()
219         obj._filename = f.name
220         return obj
221
222     def _parse_object(self, f):
223         """Parse a new style object, setting self._text."""
224         size = os.path.getsize(f.name)
225         map = mmap.mmap(f.fileno(), size, access=mmap.ACCESS_READ)
226         try:
227             # skip type and size; type must have already been determined, and we
228             # trust zlib to fail if it's otherwise corrupted
229             byte = ord(map[0])
230             used = 1
231             while (byte & 0x80) != 0:
232                 byte = ord(map[used])
233                 used += 1
234             raw = map[used:]
235             self.set_raw_string(_decompress(raw))
236         finally:
237             map.close()
238
239     @classmethod
240     def _is_legacy_object(cls, magic):
241         b0, b1 = map(ord, magic)
242         word = (b0 << 8) + b1
243         return b0 == 0x78 and (word % 31) == 0
244
245     @classmethod
246     def _parse_file_header(cls, f):
247         magic = f.read(2)
248         if cls._is_legacy_object(magic):
249             return cls._parse_legacy_object_header(magic, f)
250         else:
251             return cls._parse_object_header(magic, f)
252
253     def __init__(self):
254         """Don't call this directly"""
255         self._sha = None
256         self._filename = None
257         self._chunked_text = []
258         self._needs_parsing = False
259         self._needs_serialization = True
260
261     def _deserialize(self, chunks):
262         raise NotImplementedError(self._deserialize)
263
264     def _serialize(self):
265         raise NotImplementedError(self._serialize)
266
267     def _parse_file(self):
268         f = GitFile(self._filename, 'rb')
269         try:
270             magic = f.read(2)
271             if self._is_legacy_object(magic):
272                 self._parse_legacy_object(f)
273             else:
274                 self._parse_object(f)
275         finally:
276             f.close()
277
278     @classmethod
279     def from_file(cls, filename):
280         """Get the contents of a SHA file on disk."""
281         f = GitFile(filename, 'rb')
282         try:
283             try:
284                 obj = cls._parse_file_header(f)
285                 obj._needs_parsing = True
286                 obj._needs_serialization = True
287                 return obj
288             except (IndexError, ValueError), e:
289                 raise ObjectFormatException("invalid object header")
290         finally:
291             f.close()
292
293     @staticmethod
294     def from_raw_string(type_num, string):
295         """Creates an object of the indicated type from the raw string given.
296
297         :param type_num: The numeric type of the object.
298         :param string: The raw uncompressed contents.
299         """
300         obj = object_class(type_num)()
301         obj.set_raw_string(string)
302         return obj
303
304     @staticmethod
305     def from_raw_chunks(type_num, chunks):
306         """Creates an object of the indicated type from the raw chunks given.
307
308         :param type_num: The numeric type of the object.
309         :param chunks: An iterable of the raw uncompressed contents.
310         """
311         obj = object_class(type_num)()
312         obj.set_raw_chunks(chunks)
313         return obj
314
315     @classmethod
316     def from_string(cls, string):
317         """Create a ShaFile from a string."""
318         obj = cls()
319         obj.set_raw_string(string)
320         return obj
321
322     def _check_has_member(self, member, error_msg):
323         """Check that the object has a given member variable.
324
325         :param member: the member variable to check for
326         :param error_msg: the message for an error if the member is missing
327         :raise ObjectFormatException: with the given error_msg if member is
328             missing or is None
329         """
330         if getattr(self, member, None) is None:
331             raise ObjectFormatException(error_msg)
332
333     def check(self):
334         """Check this object for internal consistency.
335
336         :raise ObjectFormatException: if the object is malformed in some way
337         """
338         # TODO: if we find that error-checking during object parsing is a
339         # performance bottleneck, those checks should be moved to the class's
340         # check() method during optimization so we can still check the object
341         # when necessary.
342         try:
343             self._deserialize(self.as_raw_chunks())
344         except Exception, e:
345             raise ObjectFormatException(e)
346
347     def _header(self):
348         return "%s %lu\0" % (self.type_name, self.raw_length())
349
350     def raw_length(self):
351         """Returns the length of the raw string of this object."""
352         ret = 0
353         for chunk in self.as_raw_chunks():
354             ret += len(chunk)
355         return ret
356
357     def _make_sha(self):
358         ret = make_sha()
359         ret.update(self._header())
360         for chunk in self.as_raw_chunks():
361             ret.update(chunk)
362         return ret
363
364     def sha(self):
365         """The SHA1 object that is the name of this object."""
366         if self._needs_serialization or self._sha is None:
367             self._sha = self._make_sha()
368         return self._sha
369
370     @property
371     def id(self):
372         return self.sha().hexdigest()
373
374     def get_type(self):
375         return self.type_num
376
377     def set_type(self, type):
378         self.type_num = type
379
380     # DEPRECATED: use type_num or type_name as needed.
381     type = property(get_type, set_type)
382
383     def __repr__(self):
384         return "<%s %s>" % (self.__class__.__name__, self.id)
385
386     def __ne__(self, other):
387         return self.id != other.id
388
389     def __eq__(self, other):
390         """Return true if the sha of the two objects match.
391
392         The __le__ etc methods aren't overriden as they make no sense,
393         certainly at this level.
394         """
395         return self.id == other.id
396
397
398 class Blob(ShaFile):
399     """A Git Blob object."""
400
401     type_name = 'blob'
402     type_num = 3
403
404     def __init__(self):
405         super(Blob, self).__init__()
406         self._chunked_text = []
407         self._needs_parsing = False
408         self._needs_serialization = False
409
410     def _get_data(self):
411         return self.as_raw_string()
412
413     def _set_data(self, data):
414         self.set_raw_string(data)
415
416     data = property(_get_data, _set_data,
417                     "The text contained within the blob object.")
418
419     def _get_chunked(self):
420         self._ensure_parsed()
421         return self._chunked_text
422
423     def _set_chunked(self, chunks):
424         self._chunked_text = chunks
425
426     def _serialize(self):
427         if not self._chunked_text:
428             self._ensure_parsed()
429         self._needs_serialization = False
430         return self._chunked_text
431
432     def _deserialize(self, chunks):
433         return "".join(chunks)
434
435     chunked = property(_get_chunked, _set_chunked,
436         "The text within the blob object, as chunks (not necessarily lines).")
437
438     @classmethod
439     def from_file(cls, filename):
440         blob = ShaFile.from_file(filename)
441         if not isinstance(blob, cls):
442             raise NotBlobError(filename)
443         return blob
444
445     def check(self):
446         """Check this object for internal consistency.
447
448         :raise ObjectFormatException: if the object is malformed in some way
449         """
450         pass  # it's impossible for raw data to be malformed
451
452
453 def _parse_tag_or_commit(text):
454     """Parse tag or commit text.
455
456     :param text: the raw text of the tag or commit object.
457     :yield: tuples of (field, value), one per header line, in the order read
458         from the text, possibly including duplicates. Includes a field named
459         None for the freeform tag/commit text.
460     """
461     f = StringIO(text)
462     for l in f:
463         l = l.rstrip("\n")
464         if l == "":
465             # Empty line indicates end of headers
466             break
467         yield l.split(" ", 1)
468     yield (None, f.read())
469     f.close()
470
471
472 def parse_tag(text):
473     return _parse_tag_or_commit(text)
474
475
476 class Tag(ShaFile):
477     """A Git Tag object."""
478
479     type_name = 'tag'
480     type_num = 4
481
482     def __init__(self):
483         super(Tag, self).__init__()
484         self._tag_timezone_neg_utc = False
485
486     @classmethod
487     def from_file(cls, filename):
488         tag = ShaFile.from_file(filename)
489         if not isinstance(tag, cls):
490             raise NotTagError(filename)
491         return tag
492
493     def check(self):
494         """Check this object for internal consistency.
495
496         :raise ObjectFormatException: if the object is malformed in some way
497         """
498         super(Tag, self).check()
499         self._check_has_member("_object_sha", "missing object sha")
500         self._check_has_member("_object_class", "missing object type")
501         self._check_has_member("_name", "missing tag name")
502
503         if not self._name:
504             raise ObjectFormatException("empty tag name")
505
506         check_hexsha(self._object_sha, "invalid object sha")
507
508         if getattr(self, "_tagger", None):
509             check_identity(self._tagger, "invalid tagger")
510
511         last = None
512         for field, _ in parse_tag("".join(self._chunked_text)):
513             if field == _OBJECT_HEADER and last is not None:
514                 raise ObjectFormatException("unexpected object")
515             elif field == _TYPE_HEADER and last != _OBJECT_HEADER:
516                 raise ObjectFormatException("unexpected type")
517             elif field == _TAG_HEADER and last != _TYPE_HEADER:
518                 raise ObjectFormatException("unexpected tag name")
519             elif field == _TAGGER_HEADER and last != _TAG_HEADER:
520                 raise ObjectFormatException("unexpected tagger")
521             last = field
522
523     def _serialize(self):
524         chunks = []
525         chunks.append("%s %s\n" % (_OBJECT_HEADER, self._object_sha))
526         chunks.append("%s %s\n" % (_TYPE_HEADER, self._object_class.type_name))
527         chunks.append("%s %s\n" % (_TAG_HEADER, self._name))
528         if self._tagger:
529             if self._tag_time is None:
530                 chunks.append("%s %s\n" % (_TAGGER_HEADER, self._tagger))
531             else:
532                 chunks.append("%s %s %d %s\n" % (
533                   _TAGGER_HEADER, self._tagger, self._tag_time,
534                   format_timezone(self._tag_timezone,
535                     self._tag_timezone_neg_utc)))
536         chunks.append("\n") # To close headers
537         chunks.append(self._message)
538         return chunks
539
540     def _deserialize(self, chunks):
541         """Grab the metadata attached to the tag"""
542         self._tagger = None
543         for field, value in parse_tag("".join(chunks)):
544             if field == _OBJECT_HEADER:
545                 self._object_sha = value
546             elif field == _TYPE_HEADER:
547                 self._object_class = object_class(value)
548             elif field == _TAG_HEADER:
549                 self._name = value
550             elif field == _TAGGER_HEADER:
551                 try:
552                     sep = value.index("> ")
553                 except ValueError:
554                     self._tagger = value
555                     self._tag_time = None
556                     self._tag_timezone = None
557                     self._tag_timezone_neg_utc = False
558                 else:
559                     self._tagger = value[0:sep+1]
560                     (timetext, timezonetext) = value[sep+2:].rsplit(" ", 1)
561                     self._tag_time = int(timetext)
562                     self._tag_timezone, self._tag_timezone_neg_utc = \
563                             parse_timezone(timezonetext)
564             elif field is None:
565                 self._message = value
566             else:
567                 raise AssertionError("Unknown field %s" % field)
568
569     def _get_object(self):
570         """Get the object pointed to by this tag.
571
572         :return: tuple of (object class, sha).
573         """
574         self._ensure_parsed()
575         return (self._object_class, self._object_sha)
576
577     def _set_object(self, value):
578         self._ensure_parsed()
579         (self._object_class, self._object_sha) = value
580         self._needs_serialization = True
581
582     object = property(_get_object, _set_object)
583
584     name = serializable_property("name", "The name of this tag")
585     tagger = serializable_property("tagger",
586         "Returns the name of the person who created this tag")
587     tag_time = serializable_property("tag_time",
588         "The creation timestamp of the tag.  As the number of seconds since the epoch")
589     tag_timezone = serializable_property("tag_timezone",
590         "The timezone that tag_time is in.")
591     message = serializable_property("message", "The message attached to this tag")
592
593
594 def parse_tree(text):
595     """Parse a tree text.
596
597     :param text: Serialized text to parse
598     :yields: tuples of (name, mode, sha)
599     """
600     count = 0
601     l = len(text)
602     while count < l:
603         mode_end = text.index(' ', count)
604         mode = int(text[count:mode_end], 8)
605         name_end = text.index('\0', mode_end)
606         name = text[mode_end+1:name_end]
607         count = name_end+21
608         sha = text[name_end+1:count]
609         yield (name, mode, sha_to_hex(sha))
610
611
612 def serialize_tree(items):
613     """Serialize the items in a tree to a text.
614
615     :param items: Sorted iterable over (name, mode, sha) tuples
616     :return: Serialized tree text as chunks
617     """
618     for name, mode, hexsha in items:
619         yield "%04o %s\0%s" % (mode, name, hex_to_sha(hexsha))
620
621
622 def sorted_tree_items(entries):
623     """Iterate over a tree entries dictionary in the order in which 
624     the items would be serialized.
625
626     :param entries: Dictionary mapping names to (mode, sha) tuples
627     :return: Iterator over (name, mode, sha)
628     """
629     for name, entry in sorted(entries.iteritems(), cmp=cmp_entry):
630         yield name, entry[0], entry[1]
631
632
633 def cmp_entry((name1, value1), (name2, value2)):
634     """Compare two tree entries."""
635     if stat.S_ISDIR(value1[0]):
636         name1 += "/"
637     if stat.S_ISDIR(value2[0]):
638         name2 += "/"
639     return cmp(name1, name2)
640
641
642 class Tree(ShaFile):
643     """A Git tree object"""
644
645     type_name = 'tree'
646     type_num = 2
647
648     def __init__(self):
649         super(Tree, self).__init__()
650         self._entries = {}
651
652     @classmethod
653     def from_file(cls, filename):
654         tree = ShaFile.from_file(filename)
655         if not isinstance(tree, cls):
656             raise NotTreeError(filename)
657         return tree
658
659     def __contains__(self, name):
660         self._ensure_parsed()
661         return name in self._entries
662
663     def __getitem__(self, name):
664         self._ensure_parsed()
665         return self._entries[name]
666
667     def __setitem__(self, name, value):
668         assert isinstance(value, tuple)
669         assert len(value) == 2
670         self._ensure_parsed()
671         self._entries[name] = value
672         self._needs_serialization = True
673
674     def __delitem__(self, name):
675         self._ensure_parsed()
676         del self._entries[name]
677         self._needs_serialization = True
678
679     def __len__(self):
680         self._ensure_parsed()
681         return len(self._entries)
682
683     def __iter__(self):
684         self._ensure_parsed()
685         return iter(self._entries)
686
687     def add(self, mode, name, hexsha):
688         assert type(mode) == int
689         assert type(name) == str
690         assert type(hexsha) == str
691         self._ensure_parsed()
692         self._entries[name] = mode, hexsha
693         self._needs_serialization = True
694
695     def entries(self):
696         """Return a list of tuples describing the tree entries"""
697         self._ensure_parsed()
698         # The order of this is different from iteritems() for historical
699         # reasons
700         return [
701             (mode, name, hexsha) for (name, mode, hexsha) in self.iteritems()]
702
703     def iteritems(self):
704         """Iterate over entries in the order in which they would be serialized.
705
706         :return: Iterator over (name, mode, sha) tuples
707         """
708         self._ensure_parsed()
709         return sorted_tree_items(self._entries)
710
711     def _deserialize(self, chunks):
712         """Grab the entries in the tree"""
713         parsed_entries = parse_tree("".join(chunks))
714         # TODO: list comprehension is for efficiency in the common (small) case;
715         # if memory efficiency in the large case is a concern, use a genexp.
716         self._entries = dict([(n, (m, s)) for n, m, s in parsed_entries])
717
718     def check(self):
719         """Check this object for internal consistency.
720
721         :raise ObjectFormatException: if the object is malformed in some way
722         """
723         super(Tree, self).check()
724         last = None
725         allowed_modes = (stat.S_IFREG | 0755, stat.S_IFREG | 0644,
726                          stat.S_IFLNK, stat.S_IFDIR, S_IFGITLINK,
727                          # TODO: optionally exclude as in git fsck --strict
728                          stat.S_IFREG | 0664)
729         for name, mode, sha in parse_tree("".join(self._chunked_text)):
730             check_hexsha(sha, 'invalid sha %s' % sha)
731             if '/' in name or name in ('', '.', '..'):
732                 raise ObjectFormatException('invalid name %s' % name)
733
734             if mode not in allowed_modes:
735                 raise ObjectFormatException('invalid mode %06o' % mode)
736
737             entry = (name, (mode, sha))
738             if last:
739                 if cmp_entry(last, entry) > 0:
740                     raise ObjectFormatException('entries not sorted')
741                 if name == last[0]:
742                     raise ObjectFormatException('duplicate entry %s' % name)
743             last = entry
744
745     def _serialize(self):
746         return list(serialize_tree(self.iteritems()))
747
748     def as_pretty_string(self):
749         text = []
750         for name, mode, hexsha in self.iteritems():
751             if mode & stat.S_IFDIR:
752                 kind = "tree"
753             else:
754                 kind = "blob"
755             text.append("%04o %s %s\t%s\n" % (mode, kind, hexsha, name))
756         return "".join(text)
757
758
759 def parse_timezone(text):
760     offset = int(text)
761     negative_utc = (offset == 0 and text[0] == '-')
762     signum = (offset < 0) and -1 or 1
763     offset = abs(offset)
764     hours = int(offset / 100)
765     minutes = (offset % 100)
766     return signum * (hours * 3600 + minutes * 60), negative_utc
767
768
769 def format_timezone(offset, negative_utc=False):
770     if offset % 60 != 0:
771         raise ValueError("Unable to handle non-minute offset.")
772     if offset < 0 or (offset == 0 and negative_utc):
773         sign = '-'
774     else:
775         sign = '+'
776     offset = abs(offset)
777     return '%c%02d%02d' % (sign, offset / 3600, (offset / 60) % 60)
778
779
780 def parse_commit(text):
781     return _parse_tag_or_commit(text)
782
783
784 class Commit(ShaFile):
785     """A git commit object"""
786
787     type_name = 'commit'
788     type_num = 1
789
790     def __init__(self):
791         super(Commit, self).__init__()
792         self._parents = []
793         self._encoding = None
794         self._extra = {}
795         self._author_timezone_neg_utc = False
796         self._commit_timezone_neg_utc = False
797
798     @classmethod
799     def from_file(cls, filename):
800         commit = ShaFile.from_file(filename)
801         if not isinstance(commit, cls):
802             raise NotCommitError(filename)
803         return commit
804
805     def _deserialize(self, chunks):
806         self._parents = []
807         self._extra = []
808         self._author = None
809         for field, value in parse_commit("".join(self._chunked_text)):
810             if field == _TREE_HEADER:
811                 self._tree = value
812             elif field == _PARENT_HEADER:
813                 self._parents.append(value)
814             elif field == _AUTHOR_HEADER:
815                 self._author, timetext, timezonetext = value.rsplit(" ", 2)
816                 self._author_time = int(timetext)
817                 self._author_timezone, self._author_timezone_neg_utc =\
818                     parse_timezone(timezonetext)
819             elif field == _COMMITTER_HEADER:
820                 self._committer, timetext, timezonetext = value.rsplit(" ", 2)
821                 self._commit_time = int(timetext)
822                 self._commit_timezone, self._commit_timezone_neg_utc =\
823                     parse_timezone(timezonetext)
824             elif field == _ENCODING_HEADER:
825                 self._encoding = value
826             elif field is None:
827                 self._message = value
828             else:
829                 self._extra.append((field, value))
830
831     def check(self):
832         """Check this object for internal consistency.
833
834         :raise ObjectFormatException: if the object is malformed in some way
835         """
836         super(Commit, self).check()
837         self._check_has_member("_tree", "missing tree")
838         self._check_has_member("_author", "missing author")
839         self._check_has_member("_committer", "missing committer")
840         # times are currently checked when set
841
842         for parent in self._parents:
843             check_hexsha(parent, "invalid parent sha")
844         check_hexsha(self._tree, "invalid tree sha")
845
846         check_identity(self._author, "invalid author")
847         check_identity(self._committer, "invalid committer")
848
849         last = None
850         for field, _ in parse_commit("".join(self._chunked_text)):
851             if field == _TREE_HEADER and last is not None:
852                 raise ObjectFormatException("unexpected tree")
853             elif field == _PARENT_HEADER and last not in (_PARENT_HEADER,
854                                                           _TREE_HEADER):
855                 raise ObjectFormatException("unexpected parent")
856             elif field == _AUTHOR_HEADER and last not in (_TREE_HEADER,
857                                                           _PARENT_HEADER):
858                 raise ObjectFormatException("unexpected author")
859             elif field == _COMMITTER_HEADER and last != _AUTHOR_HEADER:
860                 raise ObjectFormatException("unexpected committer")
861             elif field == _ENCODING_HEADER and last != _COMMITTER_HEADER:
862                 raise ObjectFormatException("unexpected encoding")
863             last = field
864
865         # TODO: optionally check for duplicate parents
866
867     def _serialize(self):
868         chunks = []
869         chunks.append("%s %s\n" % (_TREE_HEADER, self._tree))
870         for p in self._parents:
871             chunks.append("%s %s\n" % (_PARENT_HEADER, p))
872         chunks.append("%s %s %s %s\n" % (
873           _AUTHOR_HEADER, self._author, str(self._author_time),
874           format_timezone(self._author_timezone,
875                           self._author_timezone_neg_utc)))
876         chunks.append("%s %s %s %s\n" % (
877           _COMMITTER_HEADER, self._committer, str(self._commit_time),
878           format_timezone(self._commit_timezone,
879                           self._commit_timezone_neg_utc)))
880         if self.encoding:
881             chunks.append("%s %s\n" % (_ENCODING_HEADER, self.encoding))
882         for k, v in self.extra:
883             if "\n" in k or "\n" in v:
884                 raise AssertionError("newline in extra data: %r -> %r" % (k, v))
885             chunks.append("%s %s\n" % (k, v))
886         chunks.append("\n") # There must be a new line after the headers
887         chunks.append(self._message)
888         return chunks
889
890     tree = serializable_property("tree", "Tree that is the state of this commit")
891
892     def _get_parents(self):
893         """Return a list of parents of this commit."""
894         self._ensure_parsed()
895         return self._parents
896
897     def _set_parents(self, value):
898         """Set a list of parents of this commit."""
899         self._ensure_parsed()
900         self._needs_serialization = True
901         self._parents = value
902
903     parents = property(_get_parents, _set_parents)
904
905     def _get_extra(self):
906         """Return extra settings of this commit."""
907         self._ensure_parsed()
908         return self._extra
909
910     extra = property(_get_extra)
911
912     author = serializable_property("author",
913         "The name of the author of the commit")
914
915     committer = serializable_property("committer",
916         "The name of the committer of the commit")
917
918     message = serializable_property("message",
919         "The commit message")
920
921     commit_time = serializable_property("commit_time",
922         "The timestamp of the commit. As the number of seconds since the epoch.")
923
924     commit_timezone = serializable_property("commit_timezone",
925         "The zone the commit time is in")
926
927     author_time = serializable_property("author_time",
928         "The timestamp the commit was written. as the number of seconds since the epoch.")
929
930     author_timezone = serializable_property("author_timezone",
931         "Returns the zone the author time is in.")
932
933     encoding = serializable_property("encoding",
934         "Encoding of the commit message.")
935
936
937 OBJECT_CLASSES = (
938     Commit,
939     Tree,
940     Blob,
941     Tag,
942     )
943
944 _TYPE_MAP = {}
945
946 for cls in OBJECT_CLASSES:
947     _TYPE_MAP[cls.type_name] = cls
948     _TYPE_MAP[cls.type_num] = cls
949
950
951
952 # Hold on to the pure-python implementations for testing
953 _parse_tree_py = parse_tree
954 _sorted_tree_items_py = sorted_tree_items
955 try:
956     # Try to import C versions
957     from dulwich._objects import parse_tree, sorted_tree_items
958 except ImportError:
959     pass