diff options
author | Sebastian Thiel <byronimo@gmail.com> | 2010-11-23 12:35:34 +0100 |
---|---|---|
committer | Sebastian Thiel <byronimo@gmail.com> | 2010-11-23 12:35:59 +0100 |
commit | 6e5aae2fc8c3832bdae1cd5e0a269405fb059231 (patch) | |
tree | 0436d71c5f99eb004acf95604edcecd040cd2724 | |
parent | 739fa140235cc9d65c632eaf1f5cacc944d87cfb (diff) | |
download | gitpython-6e5aae2fc8c3832bdae1cd5e0a269405fb059231.tar.gz |
Initial interface including some of the implementation of the RefLog. TestCase scetched out for now
tests: Added tests to verify that objects don't have a dict. Previously, due to a missing __slots__ member in Serializable, most objects would indeed have a dict, although the opposite was intended
-rw-r--r-- | objects/util.py | 81 | ||||
-rw-r--r-- | refs/__init__.py | 5 | ||||
-rw-r--r-- | refs/log.py | 161 | ||||
-rw-r--r-- | test/test_blob.py | 17 | ||||
-rw-r--r-- | test/test_commit.py | 2 | ||||
-rw-r--r-- | test/test_reflog.py | 32 | ||||
-rw-r--r-- | test/test_refs.py | 2 | ||||
-rw-r--r-- | test/test_tree.py | 3 |
8 files changed, 255 insertions, 48 deletions
diff --git a/objects/util.py b/objects/util.py index a9e1143c..dfaaf7c4 100644 --- a/objects/util.py +++ b/objects/util.py @@ -71,7 +71,7 @@ def get_user_id(): def utctz_to_altz(utctz): """we convert utctz to the timezone in seconds, it is the format time.altzone - returns. Git stores it as UTC timezon which has the opposite sign as well, + returns. Git stores it as UTC timezone which has the opposite sign as well, which explains the -1 * ( that was made explicit here ) :param utctz: git utc timezone string, i.e. +0200""" return -1 * int(float(utctz)/100*3600) @@ -195,53 +195,55 @@ def parse_actor_and_date(line): #{ Classes class Actor(object): - """Actors hold information about a person acting on the repository. They - can be committers and authors or anything with a name and an email as - mentioned in the git log entries.""" - # precompiled regex - name_only_regex = re.compile( r'<(.+)>' ) - name_email_regex = re.compile( r'(.*) <(.+?)>' ) - - def __init__(self, name, email): - self.name = name - self.email = email + """Actors hold information about a person acting on the repository. They + can be committers and authors or anything with a name and an email as + mentioned in the git log entries.""" + # precompiled regex + name_only_regex = re.compile( r'<(.+)>' ) + name_email_regex = re.compile( r'(.*) <(.+?)>' ) + + __slots__ = ('name', 'email') + + def __init__(self, name, email): + self.name = name + self.email = email - def __eq__(self, other): - return self.name == other.name and self.email == other.email - - def __ne__(self, other): - return not (self == other) - - def __hash__(self): - return hash((self.name, self.email)) + def __eq__(self, other): + return self.name == other.name and self.email == other.email + + def __ne__(self, other): + return not (self == other) + + def __hash__(self): + return hash((self.name, self.email)) - def __str__(self): - return self.name + def __str__(self): + return self.name - def __repr__(self): - return '<git.Actor "%s <%s>">' % (self.name, self.email) + def __repr__(self): + return '<git.Actor "%s <%s>">' % (self.name, self.email) - @classmethod - def _from_string(cls, string): - """Create an Actor from a string. + @classmethod + def _from_string(cls, string): + """Create an Actor from a string. :param string: is the string, which is expected to be in regular git format John Doe <jdoe@example.com> :return: Actor """ - m = cls.name_email_regex.search(string) - if m: - name, email = m.groups() - return Actor(name, email) - else: - m = cls.name_only_regex.search(string) - if m: - return Actor(m.group(1), None) - else: - # assume best and use the whole string as name - return Actor(string, None) - # END special case name - # END handle name/email matching + m = cls.name_email_regex.search(string) + if m: + name, email = m.groups() + return Actor(name, email) + else: + m = cls.name_only_regex.search(string) + if m: + return Actor(m.group(1), None) + else: + # assume best and use the whole string as name + return Actor(string, None) + # END special case name + # END handle name/email matching class ProcessStreamAdapter(object): @@ -359,6 +361,7 @@ class Traversable(object): class Serializable(object): """Defines methods to serialize and deserialize objects from and into a data stream""" + __slots__ = tuple() def _serialize(self, stream): """Serialize the data of this object into the given data stream diff --git a/refs/__init__.py b/refs/__init__.py index ca5ace02..fc8ce644 100644 --- a/refs/__init__.py +++ b/refs/__init__.py @@ -16,5 +16,6 @@ import symbolic for item in (HEAD, Head, RemoteReference, TagReference, Reference, SymbolicReference): setattr(symbolic, item.__name__, item) del(symbolic) -# git.objects.Commit -> symbolic -# git.config.SectionConstraint -> head + + +from log import * diff --git a/refs/log.py b/refs/log.py new file mode 100644 index 00000000..f67bea4d --- /dev/null +++ b/refs/log.py @@ -0,0 +1,161 @@ +from head import Head +from git.util import join_path +from gitdb.util import ( + join, + file_contents_ro_filepath + ) + +from git.objects.util import ( + Actor, + parse_actor_and_date, + Serializable, + utctz_to_altz, + altz_to_utctz_str, + ) + +import os + + +__all__ = ["RefLog", "RefLogEntry"] + + +class RefLogEntry(tuple): + """Named tuple allowing easy access to the revlog data fields""" + _fmt = "%s %s %s <%s> %i %s\t%s" + __slots__ = tuple() + + def __repr__(self): + """Representation of ourselves in git reflog format""" + act = self.actor + time = self.time + return self._fmt % (self.oldhexsha, self.newhexsha, act.name, act.email, + time[0], altz_to_utctz_str(time[1]), self.message) + + @property + def oldhexsha(self): + """The hexsha to the commit the ref pointed to before the change""" + return self[0] + + @property + def newhexsha(self): + """The hexsha to the commit the ref now points to, after the change""" + return self[1] + + @property + def actor(self): + """Actor instance, providing access""" + return self[2] + + @property + def time(self): + """time as tuple: + + * [0] = int(time) + * [1] = int(timezone_offset) in time.altzone format """ + return self[3] + + @property + def message(self): + """Message describing the operation that acted on the reference""" + return self[4] + + @classmethod + def new(self, oldhexsha, newhexsha, actor, time, tz_offset, message): + """:return: New instance of a RefLogEntry""" + if not isinstance(actor, Actor): + raise ValueError("Need actor instance, got %s" % actor) + # END check types + return RefLogEntry((oldhexsha, newhexsha, actor, (time, tz_offset), message)) + + @classmethod + def from_line(self, line): + """:return: New RefLogEntry instance from the given revlog line. + :param line: line without trailing newline + :raise ValueError: If line could not be parsed""" + raise NotImplementedError("todo") + + +class RefLog(list, Serializable): + """A reflog contains reflog entries, each of which defines a certain state + of the head in question. Custom query methods allow to retrieve log entries + by date or by other criteria. + + Reflog entries are orded, the first added entry is first in the list, the last + entry, i.e. the last change of the head or reference, is last in the list.""" + + __slots__ = tuple() + + #{ Interface + + @classmethod + def from_file(cls, filepath): + """ + :return: a new RefLog instance containing all entries from the reflog + at the given filepath + :param filepath: path to reflog + :raise ValueError: If the file could not be read or was corrupted in some way""" + inst = cls() + fmap = file_contents_ro_filepath(filepath, stream=False, allow_mmap=True) + try: + inst._deserialize(fmap) + finally: + fmap.close() + #END handle closing of handle + return inst + + @classmethod + def reflog_path(cls, ref): + """ + :return: string to absolute path at which the reflog of the given ref + instance would be found. The path is not guaranteed to point to a valid + file though. + :param ref: SymbolicReference instance""" + return join(ref.repo.git_dir, "logs", ref.path) + + @classmethod + def iter_entries(cls, stream): + """ + :return: Iterator yielding RefLogEntry instances, one for each line read + sfrom the given stream. + :param stream: file-like object containing the revlog in its native format + or basestring instance pointing to a file to read""" + new_entry = RefLogEntry.from_line + if isinstance(stream, basestring): + stream = file_contents_ro_filepath(stream) + #END handle stream type + return (new_entry(line.strip()) for line in stream) + + def to_file(self, filepath): + """Write the contents of the reflog instance to a file at the given filepath. + :param filepath: path to file, parent directories are assumed to exist""" + fp = open(filepath, 'wb') + try: + self._serialize(fp) + finally: + fp.close() + #END handle file streams + + #} END interface + + #{ Serializable Interface + def _serialize(self, stream): + lm1 = len(self) - 1 + write = stream.write() + + # write all entries + for i, e in self: + s = repr(e) + if i != lm1: + s += "\n" + #END handle line separator + write(s) + #END for each entry + + def _deserialize(self, stream): + new_entry = RefLogEntry.from_line + append = self.append + # NOTE: should use iter_entries, but this way it will be more direct and faster + for line in stream: + append(new_entry(line.strip())) + #END handle deserializatoin + #} END serializable interface diff --git a/test/test_blob.py b/test/test_blob.py index 8513328e..661c0501 100644 --- a/test/test_blob.py +++ b/test/test_blob.py @@ -9,12 +9,15 @@ from git import * from gitdb.util import hex_to_bin class TestBlob(TestBase): - - def test_mime_type_should_return_mime_type_for_known_types(self): - blob = Blob(self.rorepo, **{'binsha': Blob.NULL_BIN_SHA, 'path': 'foo.png'}) - assert_equal("image/png", blob.mime_type) + + def test_mime_type_should_return_mime_type_for_known_types(self): + blob = Blob(self.rorepo, **{'binsha': Blob.NULL_BIN_SHA, 'path': 'foo.png'}) + assert_equal("image/png", blob.mime_type) - def test_mime_type_should_return_text_plain_for_unknown_types(self): - blob = Blob(self.rorepo, **{'binsha': Blob.NULL_BIN_SHA,'path': 'something'}) - assert_equal("text/plain", blob.mime_type) + def test_mime_type_should_return_text_plain_for_unknown_types(self): + blob = Blob(self.rorepo, **{'binsha': Blob.NULL_BIN_SHA,'path': 'something'}) + assert_equal("text/plain", blob.mime_type) + def test_nodict(self): + self.failUnlessRaises(AttributeError, setattr, self.rorepo.tree()['AUTHORS'], 'someattr', 2) + diff --git a/test/test_commit.py b/test/test_commit.py index a00a8de8..4a8d8b87 100644 --- a/test/test_commit.py +++ b/test/test_commit.py @@ -70,6 +70,8 @@ class TestCommit(TestBase): def test_bake(self): commit = self.rorepo.commit('2454ae89983a4496a445ce347d7a41c0bb0ea7ae') + # commits have no dict + self.failUnlessRaises(AttributeError, setattr, commit, 'someattr', 1) commit.author # bake assert_equal("Sebastian Thiel", commit.author.name) diff --git a/test/test_reflog.py b/test/test_reflog.py new file mode 100644 index 00000000..efcc7f33 --- /dev/null +++ b/test/test_reflog.py @@ -0,0 +1,32 @@ +from git.test.lib import * +from git.objects import IndexObject, Actor +from git.refs import * + +class TestRefLog(TestBase): + + def test_reflogentry(self): + nullhexsha = IndexObject.NULL_HEX_SHA + hexsha = 'F' * 40 + actor = Actor('name', 'email') + msg = "message" + + self.failUnlessRaises(ValueError, RefLogEntry.new, nullhexsha, hexsha, 'noactor', 0, 0, "") + e = RefLogEntry.new(nullhexsha, hexsha, actor, 0, 1, msg) + + assert e.oldhexsha == nullhexsha + assert e.newhexsha == hexsha + assert e.actor == actor + assert e.time[0] == 0 + assert e.time[1] == 1 + assert e.message == msg + + # check representation (roughly) + assert repr(e).startswith(nullhexsha) + + def test_base(self): + pass + # raise on invalid revlog + # TODO: Try multiple corrupted ones ! + + + # test serialize and deserialize - results must match exactly diff --git a/test/test_refs.py b/test/test_refs.py index 700e5fac..ebf1a00d 100644 --- a/test/test_refs.py +++ b/test/test_refs.py @@ -33,6 +33,8 @@ class TestRefs(TestBase): if tag.tag is not None: tag_object_refs.append( tag ) tagobj = tag.tag + # have no dict + self.failUnlessRaises(AttributeError, setattr, tagobj, 'someattr', 1) assert isinstance( tagobj, TagObject ) assert tagobj.tag == tag.name assert isinstance( tagobj.tagger, Actor ) diff --git a/test/test_tree.py b/test/test_tree.py index 80db2e4b..ec10e962 100644 --- a/test/test_tree.py +++ b/test/test_tree.py @@ -23,6 +23,9 @@ class TestTree(TestBase): continue # END skip non-trees tree = item + # trees have no dict + self.failUnlessRaises(AttributeError, setattr, tree, 'someattr', 1) + orig_data = tree.data_stream.read() orig_cache = tree._cache |