summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--objects/util.py81
-rw-r--r--refs/__init__.py5
-rw-r--r--refs/log.py161
-rw-r--r--test/test_blob.py17
-rw-r--r--test/test_commit.py2
-rw-r--r--test/test_reflog.py32
-rw-r--r--test/test_refs.py2
-rw-r--r--test/test_tree.py3
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