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