Skip to content

Commit 6e5aae2

Browse files
committed
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
1 parent 739fa14 commit 6e5aae2

File tree

8 files changed

+255
-48
lines changed

8 files changed

+255
-48
lines changed

‎objects/util.py

+42-39
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ def get_user_id():
7171

7272
def utctz_to_altz(utctz):
7373
"""we convert utctz to the timezone in seconds, it is the format time.altzone
74-
returns. Git stores it as UTC timezon which has the opposite sign as well,
74+
returns. Git stores it as UTC timezone which has the opposite sign as well,
7575
which explains the -1 * ( that was made explicit here )
7676
:param utctz: git utc timezone string, i.e. +0200"""
7777
return -1 * int(float(utctz)/100*3600)
@@ -195,53 +195,55 @@ def parse_actor_and_date(line):
195195
#{ Classes
196196

197197
class Actor(object):
198-
"""Actors hold information about a person acting on the repository. They
199-
can be committers and authors or anything with a name and an email as
200-
mentioned in the git log entries."""
201-
# precompiled regex
202-
name_only_regex = re.compile( r'<(.+)>' )
203-
name_email_regex = re.compile( r'(.*) <(.+?)>' )
204-
205-
def __init__(self, name, email):
206-
self.name = name
207-
self.email = email
198+
"""Actors hold information about a person acting on the repository. They
199+
can be committers and authors or anything with a name and an email as
200+
mentioned in the git log entries."""
201+
# precompiled regex
202+
name_only_regex = re.compile( r'<(.+)>' )
203+
name_email_regex = re.compile( r'(.*) <(.+?)>' )
204+
205+
__slots__ = ('name', 'email')
206+
207+
def __init__(self, name, email):
208+
self.name = name
209+
self.email = email
208210

209-
def __eq__(self, other):
210-
return self.name == other.name and self.email == other.email
211-
212-
def __ne__(self, other):
213-
return not (self == other)
214-
215-
def __hash__(self):
216-
return hash((self.name, self.email))
211+
def __eq__(self, other):
212+
return self.name == other.name and self.email == other.email
213+
214+
def __ne__(self, other):
215+
return not (self == other)
216+
217+
def __hash__(self):
218+
return hash((self.name, self.email))
217219

218-
def __str__(self):
219-
return self.name
220+
def __str__(self):
221+
return self.name
220222

221-
def __repr__(self):
222-
return '<git.Actor "%s <%s>">' % (self.name, self.email)
223+
def __repr__(self):
224+
return '<git.Actor "%s <%s>">' % (self.name, self.email)
223225

224-
@classmethod
225-
def _from_string(cls, string):
226-
"""Create an Actor from a string.
226+
@classmethod
227+
def _from_string(cls, string):
228+
"""Create an Actor from a string.
227229
:param string: is the string, which is expected to be in regular git format
228230
229231
John Doe <jdoe@example.com>
230232
231233
:return: Actor """
232-
m = cls.name_email_regex.search(string)
233-
if m:
234-
name, email = m.groups()
235-
return Actor(name, email)
236-
else:
237-
m = cls.name_only_regex.search(string)
238-
if m:
239-
return Actor(m.group(1), None)
240-
else:
241-
# assume best and use the whole string as name
242-
return Actor(string, None)
243-
# END special case name
244-
# END handle name/email matching
234+
m = cls.name_email_regex.search(string)
235+
if m:
236+
name, email = m.groups()
237+
return Actor(name, email)
238+
else:
239+
m = cls.name_only_regex.search(string)
240+
if m:
241+
return Actor(m.group(1), None)
242+
else:
243+
# assume best and use the whole string as name
244+
return Actor(string, None)
245+
# END special case name
246+
# END handle name/email matching
245247

246248

247249
class ProcessStreamAdapter(object):
@@ -359,6 +361,7 @@ def addToStack( stack, item, branch_first, depth ):
359361

360362
class Serializable(object):
361363
"""Defines methods to serialize and deserialize objects from and into a data stream"""
364+
__slots__ = tuple()
362365

363366
def _serialize(self, stream):
364367
"""Serialize the data of this object into the given data stream

‎refs/__init__.py

+3-2
Original file line numberDiff line numberDiff line change
@@ -16,5 +16,6 @@
1616
for item in (HEAD, Head, RemoteReference, TagReference, Reference, SymbolicReference):
1717
setattr(symbolic, item.__name__, item)
1818
del(symbolic)
19-
# git.objects.Commit -> symbolic
20-
# git.config.SectionConstraint -> head
19+
20+
21+
from log import *

‎refs/log.py

+161
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
from head import Head
2+
from git.util import join_path
3+
from gitdb.util import (
4+
join,
5+
file_contents_ro_filepath
6+
)
7+
8+
from git.objects.util import (
9+
Actor,
10+
parse_actor_and_date,
11+
Serializable,
12+
utctz_to_altz,
13+
altz_to_utctz_str,
14+
)
15+
16+
import os
17+
18+
19+
__all__ = ["RefLog", "RefLogEntry"]
20+
21+
22+
class RefLogEntry(tuple):
23+
"""Named tuple allowing easy access to the revlog data fields"""
24+
_fmt = "%s %s %s <%s> %i %s\t%s"
25+
__slots__ = tuple()
26+
27+
def __repr__(self):
28+
"""Representation of ourselves in git reflog format"""
29+
act = self.actor
30+
time = self.time
31+
return self._fmt % (self.oldhexsha, self.newhexsha, act.name, act.email,
32+
time[0], altz_to_utctz_str(time[1]), self.message)
33+
34+
@property
35+
def oldhexsha(self):
36+
"""The hexsha to the commit the ref pointed to before the change"""
37+
return self[0]
38+
39+
@property
40+
def newhexsha(self):
41+
"""The hexsha to the commit the ref now points to, after the change"""
42+
return self[1]
43+
44+
@property
45+
def actor(self):
46+
"""Actor instance, providing access"""
47+
return self[2]
48+
49+
@property
50+
def time(self):
51+
"""time as tuple:
52+
53+
* [0] = int(time)
54+
* [1] = int(timezone_offset) in time.altzone format """
55+
return self[3]
56+
57+
@property
58+
def message(self):
59+
"""Message describing the operation that acted on the reference"""
60+
return self[4]
61+
62+
@classmethod
63+
def new(self, oldhexsha, newhexsha, actor, time, tz_offset, message):
64+
""":return: New instance of a RefLogEntry"""
65+
if not isinstance(actor, Actor):
66+
raise ValueError("Need actor instance, got %s" % actor)
67+
# END check types
68+
return RefLogEntry((oldhexsha, newhexsha, actor, (time, tz_offset), message))
69+
70+
@classmethod
71+
def from_line(self, line):
72+
""":return: New RefLogEntry instance from the given revlog line.
73+
:param line: line without trailing newline
74+
:raise ValueError: If line could not be parsed"""
75+
raise NotImplementedError("todo")
76+
77+
78+
class RefLog(list, Serializable):
79+
"""A reflog contains reflog entries, each of which defines a certain state
80+
of the head in question. Custom query methods allow to retrieve log entries
81+
by date or by other criteria.
82+
83+
Reflog entries are orded, the first added entry is first in the list, the last
84+
entry, i.e. the last change of the head or reference, is last in the list."""
85+
86+
__slots__ = tuple()
87+
88+
#{ Interface
89+
90+
@classmethod
91+
def from_file(cls, filepath):
92+
"""
93+
:return: a new RefLog instance containing all entries from the reflog
94+
at the given filepath
95+
:param filepath: path to reflog
96+
:raise ValueError: If the file could not be read or was corrupted in some way"""
97+
inst = cls()
98+
fmap = file_contents_ro_filepath(filepath, stream=False, allow_mmap=True)
99+
try:
100+
inst._deserialize(fmap)
101+
finally:
102+
fmap.close()
103+
#END handle closing of handle
104+
return inst
105+
106+
@classmethod
107+
def reflog_path(cls, ref):
108+
"""
109+
:return: string to absolute path at which the reflog of the given ref
110+
instance would be found. The path is not guaranteed to point to a valid
111+
file though.
112+
:param ref: SymbolicReference instance"""
113+
return join(ref.repo.git_dir, "logs", ref.path)
114+
115+
@classmethod
116+
def iter_entries(cls, stream):
117+
"""
118+
:return: Iterator yielding RefLogEntry instances, one for each line read
119+
sfrom the given stream.
120+
:param stream: file-like object containing the revlog in its native format
121+
or basestring instance pointing to a file to read"""
122+
new_entry = RefLogEntry.from_line
123+
if isinstance(stream, basestring):
124+
stream = file_contents_ro_filepath(stream)
125+
#END handle stream type
126+
return (new_entry(line.strip()) for line in stream)
127+
128+
def to_file(self, filepath):
129+
"""Write the contents of the reflog instance to a file at the given filepath.
130+
:param filepath: path to file, parent directories are assumed to exist"""
131+
fp = open(filepath, 'wb')
132+
try:
133+
self._serialize(fp)
134+
finally:
135+
fp.close()
136+
#END handle file streams
137+
138+
#} END interface
139+
140+
#{ Serializable Interface
141+
def _serialize(self, stream):
142+
lm1 = len(self) - 1
143+
write = stream.write()
144+
145+
# write all entries
146+
for i, e in self:
147+
s = repr(e)
148+
if i != lm1:
149+
s += "\n"
150+
#END handle line separator
151+
write(s)
152+
#END for each entry
153+
154+
def _deserialize(self, stream):
155+
new_entry = RefLogEntry.from_line
156+
append = self.append
157+
# NOTE: should use iter_entries, but this way it will be more direct and faster
158+
for line in stream:
159+
append(new_entry(line.strip()))
160+
#END handle deserializatoin
161+
#} END serializable interface

‎test/test_blob.py

+10-7
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,15 @@
99
from gitdb.util import hex_to_bin
1010

1111
class TestBlob(TestBase):
12-
13-
def test_mime_type_should_return_mime_type_for_known_types(self):
14-
blob = Blob(self.rorepo, **{'binsha': Blob.NULL_BIN_SHA, 'path': 'foo.png'})
15-
assert_equal("image/png", blob.mime_type)
12+
13+
def test_mime_type_should_return_mime_type_for_known_types(self):
14+
blob = Blob(self.rorepo, **{'binsha': Blob.NULL_BIN_SHA, 'path': 'foo.png'})
15+
assert_equal("image/png", blob.mime_type)
1616

17-
def test_mime_type_should_return_text_plain_for_unknown_types(self):
18-
blob = Blob(self.rorepo, **{'binsha': Blob.NULL_BIN_SHA,'path': 'something'})
19-
assert_equal("text/plain", blob.mime_type)
17+
def test_mime_type_should_return_text_plain_for_unknown_types(self):
18+
blob = Blob(self.rorepo, **{'binsha': Blob.NULL_BIN_SHA,'path': 'something'})
19+
assert_equal("text/plain", blob.mime_type)
2020

21+
def test_nodict(self):
22+
self.failUnlessRaises(AttributeError, setattr, self.rorepo.tree()['AUTHORS'], 'someattr', 2)
23+

‎test/test_commit.py

+2
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,8 @@ class TestCommit(TestBase):
7070
def test_bake(self):
7171

7272
commit = self.rorepo.commit('2454ae89983a4496a445ce347d7a41c0bb0ea7ae')
73+
# commits have no dict
74+
self.failUnlessRaises(AttributeError, setattr, commit, 'someattr', 1)
7375
commit.author # bake
7476

7577
assert_equal("Sebastian Thiel", commit.author.name)

‎test/test_reflog.py

+32
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
from git.test.lib import *
2+
from git.objects import IndexObject, Actor
3+
from git.refs import *
4+
5+
class TestRefLog(TestBase):
6+
7+
def test_reflogentry(self):
8+
nullhexsha = IndexObject.NULL_HEX_SHA
9+
hexsha = 'F' * 40
10+
actor = Actor('name', 'email')
11+
msg = "message"
12+
13+
self.failUnlessRaises(ValueError, RefLogEntry.new, nullhexsha, hexsha, 'noactor', 0, 0, "")
14+
e = RefLogEntry.new(nullhexsha, hexsha, actor, 0, 1, msg)
15+
16+
assert e.oldhexsha == nullhexsha
17+
assert e.newhexsha == hexsha
18+
assert e.actor == actor
19+
assert e.time[0] == 0
20+
assert e.time[1] == 1
21+
assert e.message == msg
22+
23+
# check representation (roughly)
24+
assert repr(e).startswith(nullhexsha)
25+
26+
def test_base(self):
27+
pass
28+
# raise on invalid revlog
29+
# TODO: Try multiple corrupted ones !
30+
31+
32+
# test serialize and deserialize - results must match exactly

‎test/test_refs.py

+2
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,8 @@ def test_tag_base(self):
3333
if tag.tag is not None:
3434
tag_object_refs.append( tag )
3535
tagobj = tag.tag
36+
# have no dict
37+
self.failUnlessRaises(AttributeError, setattr, tagobj, 'someattr', 1)
3638
assert isinstance( tagobj, TagObject )
3739
assert tagobj.tag == tag.name
3840
assert isinstance( tagobj.tagger, Actor )

‎test/test_tree.py

+3
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,9 @@ def test_serializable(self):
2323
continue
2424
# END skip non-trees
2525
tree = item
26+
# trees have no dict
27+
self.failUnlessRaises(AttributeError, setattr, tree, 'someattr', 1)
28+
2629
orig_data = tree.data_stream.read()
2730
orig_cache = tree._cache
2831

0 commit comments

Comments
 (0)