123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631 |
- # objects.py -- Access to base git objects
- # Copyright (C) 2007 James Westby <jw+debian@jameswestby.net>
- # Copyright (C) 2008-2009 Jelmer Vernooij <jelmer@samba.org>
- #
- # This program is free software; you can redistribute it and/or
- # modify it under the terms of the GNU General Public License
- # as published by the Free Software Foundation; version 2
- # of the License or (at your option) a later version of the License.
- #
- # This program is distributed in the hope that it will be useful,
- # but WITHOUT ANY WARRANTY; without even the implied warranty of
- # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- # GNU General Public License for more details.
- #
- # You should have received a copy of the GNU General Public License
- # along with this program; if not, write to the Free Software
- # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
- # MA 02110-1301, USA.
- """Access to base git objects."""
- import mmap
- import os
- import sha
- import stat
- import zlib
- from dulwich.errors import (
- NotBlobError,
- NotCommitError,
- NotTreeError,
- )
- from dulwich.misc import (
- make_sha,
- )
- BLOB_ID = "blob"
- TAG_ID = "tag"
- TREE_ID = "tree"
- COMMIT_ID = "commit"
- PARENT_ID = "parent"
- AUTHOR_ID = "author"
- COMMITTER_ID = "committer"
- OBJECT_ID = "object"
- TYPE_ID = "type"
- TAGGER_ID = "tagger"
- def _decompress(string):
- dcomp = zlib.decompressobj()
- dcomped = dcomp.decompress(string)
- dcomped += dcomp.flush()
- return dcomped
- def sha_to_hex(sha):
- """Takes a string and returns the hex of the sha within"""
- hexsha = "".join(["%02x" % ord(c) for c in sha])
- assert len(hexsha) == 40, "Incorrect length of sha1 string: %d" % hexsha
- return hexsha
- def hex_to_sha(hex):
- """Takes a hex sha and returns a binary sha"""
- assert len(hex) == 40, "Incorrent length of hexsha: %s" % hex
- return ''.join([chr(int(hex[i:i+2], 16)) for i in xrange(0, len(hex), 2)])
- def serializable_property(name, docstring=None):
- def set(obj, value):
- obj._ensure_parsed()
- setattr(obj, "_"+name, value)
- obj._needs_serialization = True
- def get(obj):
- obj._ensure_parsed()
- return getattr(obj, "_"+name)
- return property(get, set, doc=docstring)
- class ShaFile(object):
- """A git SHA file."""
-
- @classmethod
- def _parse_legacy_object(cls, map):
- """Parse a legacy object, creating it and setting object._text"""
- text = _decompress(map)
- object = None
- for posstype in type_map.keys():
- if text.startswith(posstype):
- object = type_map[posstype]()
- text = text[len(posstype):]
- break
- assert object is not None, "%s is not a known object type" % text[:9]
- assert text[0] == ' ', "%s is not a space" % text[0]
- text = text[1:]
- size = 0
- i = 0
- while text[0] >= '0' and text[0] <= '9':
- if i > 0 and size == 0:
- assert False, "Size is not in canonical format"
- size = (size * 10) + int(text[0])
- text = text[1:]
- i += 1
- object._size = size
- assert text[0] == "\0", "Size not followed by null"
- text = text[1:]
- object.set_raw_string(text)
- return object
- def as_legacy_object(self):
- text = self.as_raw_string()
- return zlib.compress("%s %d\0%s" % (self._type, len(text), text))
-
- def as_raw_string(self):
- if self._needs_serialization:
- self.serialize()
- return self._text
- def as_pretty_string(self):
- return self.as_raw_string()
- def _ensure_parsed(self):
- if self._needs_parsing:
- self._parse_text()
- def set_raw_string(self, text):
- self._text = text
- self._needs_parsing = True
- self._needs_serialization = False
-
- @classmethod
- def _parse_object(cls, map):
- """Parse a new style object , creating it and setting object._text"""
- used = 0
- byte = ord(map[used])
- used += 1
- num_type = (byte >> 4) & 7
- try:
- object = num_type_map[num_type]()
- except KeyError:
- raise AssertionError("Not a known type: %d" % num_type)
- while (byte & 0x80) != 0:
- byte = ord(map[used])
- used += 1
- raw = map[used:]
- object.set_raw_string(_decompress(raw))
- return object
-
- @classmethod
- def _parse_file(cls, map):
- word = (ord(map[0]) << 8) + ord(map[1])
- if ord(map[0]) == 0x78 and (word % 31) == 0:
- return cls._parse_legacy_object(map)
- else:
- return cls._parse_object(map)
-
- def __init__(self):
- """Don't call this directly"""
-
- def _parse_text(self):
- """For subclasses to do initialisation time parsing"""
-
- @classmethod
- def from_file(cls, filename):
- """Get the contents of a SHA file on disk"""
- size = os.path.getsize(filename)
- f = open(filename, 'rb')
- try:
- map = mmap.mmap(f.fileno(), size, access=mmap.ACCESS_READ)
- shafile = cls._parse_file(map)
- return shafile
- finally:
- f.close()
-
- @classmethod
- def from_raw_string(cls, type, string):
- """Creates an object of the indicated type from the raw string given.
-
- Type is the numeric type of an object. String is the raw uncompressed
- contents.
- """
- real_class = num_type_map[type]
- obj = real_class()
- obj.type = type
- obj.set_raw_string(string)
- return obj
-
- def _header(self):
- return "%s %lu\0" % (self._type, len(self.as_raw_string()))
-
- def sha(self):
- """The SHA1 object that is the name of this object."""
- ressha = make_sha()
- ressha.update(self._header())
- ressha.update(self.as_raw_string())
- return ressha
-
- @property
- def id(self):
- return self.sha().hexdigest()
-
- def get_type(self):
- return self._num_type
- def set_type(self, type):
- self._num_type = type
- type = property(get_type, set_type)
-
- def __repr__(self):
- return "<%s %s>" % (self.__class__.__name__, self.id)
-
- def __eq__(self, other):
- """Return true id the sha of the two objects match.
-
- The __le__ etc methods aren't overriden as they make no sense,
- certainly at this level.
- """
- return self.sha().digest() == other.sha().digest()
- class Blob(ShaFile):
- """A Git Blob object."""
- _type = BLOB_ID
- _num_type = 3
- _needs_serialization = False
- _needs_parsing = False
- @property
- def data(self):
- """The text contained within the blob object."""
- return self._text
- @classmethod
- def from_file(cls, filename):
- blob = ShaFile.from_file(filename)
- if blob._type != cls._type:
- raise NotBlobError(filename)
- return blob
- @classmethod
- def from_string(cls, string):
- """Create a blob from a string."""
- shafile = cls()
- shafile.set_raw_string(string)
- return shafile
- class Tag(ShaFile):
- """A Git Tag object."""
- _type = TAG_ID
- _num_type = 4
- @classmethod
- def from_file(cls, filename):
- blob = ShaFile.from_file(filename)
- if blob._type != cls._type:
- raise NotBlobError(filename)
- return blob
- @classmethod
- def from_string(cls, string):
- """Create a blob from a string."""
- shafile = cls()
- shafile.set_raw_string(string)
- return shafile
- def _parse_text(self):
- """Grab the metadata attached to the tag"""
- text = self._text
- count = 0
- assert text.startswith(OBJECT_ID), "Invalid tag object, " \
- "must start with %s" % OBJECT_ID
- count += len(OBJECT_ID)
- assert text[count] == ' ', "Invalid tag object, " \
- "%s must be followed by space not %s" % (OBJECT_ID, text[count])
- count += 1
- self._object_sha = text[count:count+40]
- count += 40
- assert text[count] == '\n', "Invalid tag object, " \
- "%s sha must be followed by newline" % OBJECT_ID
- count += 1
- assert text[count:].startswith(TYPE_ID), "Invalid tag object, " \
- "%s sha must be followed by %s" % (OBJECT_ID, TYPE_ID)
- count += len(TYPE_ID)
- assert text[count] == ' ', "Invalid tag object, " \
- "%s must be followed by space not %s" % (TAG_ID, text[count])
- count += 1
- self._object_type = ""
- while text[count] != '\n':
- self._object_type += text[count]
- count += 1
- count += 1
- assert self._object_type in (COMMIT_ID, BLOB_ID, TREE_ID, TAG_ID), "Invalid tag object, " \
- "unexpected object type %s" % self._object_type
- self._object_type = type_map[self._object_type]
- assert text[count:].startswith(TAG_ID), "Invalid tag object, " \
- "object type must be followed by %s" % (TAG_ID)
- count += len(TAG_ID)
- assert text[count] == ' ', "Invalid tag object, " \
- "%s must be followed by space not %s" % (TAG_ID, text[count])
- count += 1
- self._name = ""
- while text[count] != '\n':
- self._name += text[count]
- count += 1
- count += 1
- assert text[count:].startswith(TAGGER_ID), "Invalid tag object, " \
- "%s must be followed by %s" % (TAG_ID, TAGGER_ID)
- count += len(TAGGER_ID)
- assert text[count] == ' ', "Invalid tag object, " \
- "%s must be followed by space not %s" % (TAGGER_ID, text[count])
- count += 1
- self._tagger = ""
- while text[count] != '>':
- assert text[count] != '\n', "Malformed tagger information"
- self._tagger += text[count]
- count += 1
- self._tagger += text[count]
- count += 1
- assert text[count] == ' ', "Invalid tag object, " \
- "tagger information must be followed by space not %s" % text[count]
- count += 1
- self._tag_time = int(text[count:count+10])
- while text[count] != '\n':
- count += 1
- count += 1
- assert text[count] == '\n', "There must be a new line after the headers"
- count += 1
- self._message = text[count:]
- self._needs_parsing = False
- def get_object(self):
- """Returns the object pointed by this tag, represented as a tuple(type, sha)"""
- self._ensure_parsed()
- return (self._object_type, self._object_sha)
- object = property(get_object)
- name = serializable_property("name", "The name of this tag")
- tagger = serializable_property("tagger",
- "Returns the name of the person who created this tag")
- tag_time = serializable_property("tag_time",
- "The creation timestamp of the tag. As the number of seconds since the epoch")
- message = serializable_property("message", "The message attached to this tag")
- def parse_tree(text):
- ret = {}
- count = 0
- while count < len(text):
- mode = 0
- chr = text[count]
- while chr != ' ':
- assert chr >= '0' and chr <= '7', "%s is not a valid mode char" % chr
- mode = (mode << 3) + (ord(chr) - ord('0'))
- count += 1
- chr = text[count]
- count += 1
- chr = text[count]
- name = ''
- while chr != '\0':
- name += chr
- count += 1
- chr = text[count]
- count += 1
- chr = text[count]
- sha = text[count:count+20]
- hexsha = sha_to_hex(sha)
- ret[name] = (mode, hexsha)
- count = count + 20
- return ret
- class Tree(ShaFile):
- """A Git tree object"""
- _type = TREE_ID
- _num_type = 2
- def __init__(self):
- self._entries = {}
- self._needs_parsing = False
- self._needs_serialization = True
- @classmethod
- def from_file(cls, filename):
- tree = ShaFile.from_file(filename)
- if tree._type != cls._type:
- raise NotTreeError(filename)
- return tree
- def __contains__(self, name):
- self._ensure_parsed()
- return name in self._entries
- def __getitem__(self, name):
- self._ensure_parsed()
- return self._entries[name]
- def __setitem__(self, name, value):
- assert isinstance(value, tuple)
- assert len(value) == 2
- self._ensure_parsed()
- self._entries[name] = value
- self._needs_serialization = True
- def __delitem__(self, name):
- self._ensure_parsed()
- del self._entries[name]
- self._needs_serialization = True
- def add(self, mode, name, hexsha):
- self._ensure_parsed()
- self._entries[name] = mode, hexsha
- self._needs_serialization = True
- def entries(self):
- """Return a list of tuples describing the tree entries"""
- self._ensure_parsed()
- # The order of this is different from iteritems() for historical reasons
- return [(mode, name, hexsha) for (name, mode, hexsha) in self.iteritems()]
- def iteritems(self):
- self._ensure_parsed()
- for name in sorted(self._entries.keys()):
- yield name, self._entries[name][0], self._entries[name][1]
- def _parse_text(self):
- """Grab the entries in the tree"""
- self._entries = parse_tree(self._text)
- self._needs_parsing = False
- def serialize(self):
- self._text = ""
- for name, mode, hexsha in self.iteritems():
- self._text += "%04o %s\0%s" % (mode, name, hex_to_sha(hexsha))
- self._needs_serialization = False
- def as_pretty_string(self):
- text = ""
- for name, mode, hexsha in self.iteritems():
- if mode & stat.S_IFDIR:
- kind = "tree"
- else:
- kind = "blob"
- text += "%04o %s %s\t%s\n" % (mode, kind, hexsha, name)
- return text
- def parse_timezone(text):
- offset = int(text)
- hours = int(offset / 100)
- minutes = (offset % 100)
- return (hours * 3600) + (minutes * 60)
- def format_timezone(offset):
- if offset % 60 != 0:
- raise ValueError("Unable to handle non-minute offset.")
- return ' %+03d%02d' % (offset / 3600, (offset / 60) % 60)
- class Commit(ShaFile):
- """A git commit object"""
- _type = COMMIT_ID
- _num_type = 1
- def __init__(self):
- self._parents = []
- self._needs_parsing = False
- self._needs_serialization = True
- @classmethod
- def from_file(cls, filename):
- commit = ShaFile.from_file(filename)
- if commit._type != cls._type:
- raise NotCommitError(filename)
- return commit
- def _parse_text(self):
- text = self._text
- count = 0
- assert text.startswith(TREE_ID), "Invalid commit object, " \
- "must start with %s" % TREE_ID
- count += len(TREE_ID)
- assert text[count] == ' ', "Invalid commit object, " \
- "%s must be followed by space not %s" % (TREE_ID, text[count])
- count += 1
- self._tree = text[count:count+40]
- count = count + 40
- assert text[count] == "\n", "Invalid commit object, " \
- "tree sha must be followed by newline"
- count += 1
- self._parents = []
- while text[count:].startswith(PARENT_ID):
- count += len(PARENT_ID)
- assert text[count] == ' ', "Invalid commit object, " \
- "%s must be followed by space not %s" % (PARENT_ID, text[count])
- count += 1
- self._parents.append(text[count:count+40])
- count += 40
- assert text[count] == "\n", "Invalid commit object, " \
- "parent sha must be followed by newline"
- count += 1
- self._author = None
- if text[count:].startswith(AUTHOR_ID):
- count += len(AUTHOR_ID)
- assert text[count] == ' ', "Invalid commit object, " \
- "%s must be followed by space not %s" % (AUTHOR_ID, text[count])
- count += 1
- self._author = ''
- while text[count] != '>':
- assert text[count] != '\n', "Malformed author information"
- self._author += text[count]
- count += 1
- self._author += text[count]
- count += 1
- assert text[count] == ' ', "Invalid commit object, " \
- "author information must be followed by space not %s" % text[count]
- count += 1
- self._author_time = int(text[count:count+10])
- while text[count] != ' ':
- assert text[count] != '\n', "Malformed author information"
- count += 1
- self._author_timezone = parse_timezone(text[count:count+6])
- count += 1
- while text[count] != '\n':
- count += 1
- count += 1
- self._committer = None
- if text[count:].startswith(COMMITTER_ID):
- count += len(COMMITTER_ID)
- assert text[count] == ' ', "Invalid commit object, " \
- "%s must be followed by space not %s" % (COMMITTER_ID, text[count])
- count += 1
- self._committer = ''
- while text[count] != '>':
- assert text[count] != '\n', "Malformed committer information"
- self._committer += text[count]
- count += 1
- self._committer += text[count]
- count += 1
- assert text[count] == ' ', "Invalid commit object, " \
- "commiter information must be followed by space not %s" % text[count]
- count += 1
- self._commit_time = int(text[count:count+10])
- while text[count] != ' ':
- assert text[count] != '\n', "Malformed committer information"
- count += 1
- self._commit_timezone = parse_timezone(text[count:count+6])
- count += 1
- while text[count] != '\n':
- count += 1
- count += 1
- assert text[count] == '\n', "There must be a new line after the headers"
- count += 1
- # XXX: There can be an encoding field.
- self._message = text[count:]
- self._needs_parsing = False
- def serialize(self):
- self._text = ""
- self._text += "%s %s\n" % (TREE_ID, self._tree)
- for p in self._parents:
- self._text += "%s %s\n" % (PARENT_ID, p)
- self._text += "%s %s %s %s\n" % (AUTHOR_ID, self._author, str(self._author_time), format_timezone(self._author_timezone))
- self._text += "%s %s %s %s\n" % (COMMITTER_ID, self._committer, str(self._commit_time), format_timezone(self._commit_timezone))
- self._text += "\n" # There must be a new line after the headers
- self._text += self._message
- self._needs_serialization = False
- tree = serializable_property("tree", "Tree that is the state of this commit")
- def get_parents(self):
- """Return a list of parents of this commit."""
- self._ensure_parsed()
- return self._parents
- parents = property(get_parents)
- author = serializable_property("author",
- "The name of the author of the commit")
- committer = serializable_property("committer",
- "The name of the committer of the commit")
- message = serializable_property("message",
- "The commit message")
- commit_time = serializable_property("commit_time",
- "The timestamp of the commit. As the number of seconds since the epoch.")
- commit_timezone = serializable_property("commit_timezone",
- "The zone the commit time is in")
- author_time = serializable_property("author_time",
- "The timestamp the commit was written. as the number of seconds since the epoch.")
- author_timezone = serializable_property("author_timezone",
- "Returns the zone the author time is in.")
- type_map = {
- BLOB_ID : Blob,
- TREE_ID : Tree,
- COMMIT_ID : Commit,
- TAG_ID: Tag,
- }
- num_type_map = {
- 0: None,
- 1: Commit,
- 2: Tree,
- 3: Blob,
- 4: Tag,
- # 5 Is reserved for further expansion
- }
- try:
- # Try to import C versions
- from dulwich._objects import hex_to_sha, sha_to_hex, parse_tree
- except ImportError:
- pass
|