diff options
-rw-r--r-- | CHANGES | 9 | ||||
-rw-r--r-- | TODO | 14 | ||||
-rw-r--r-- | lib/git/refs.py | 231 | ||||
-rw-r--r-- | lib/git/repo.py | 12 | ||||
-rw-r--r-- | test/git/test_base.py | 46 | ||||
-rw-r--r-- | test/git/test_commit.py | 2 | ||||
-rw-r--r-- | test/git/test_head.py | 24 | ||||
-rw-r--r-- | test/git/test_index.py | 4 | ||||
-rw-r--r-- | test/git/test_refs.py | 110 | ||||
-rw-r--r-- | test/git/test_repo.py | 10 | ||||
-rw-r--r-- | test/git/test_tag.py | 33 |
11 files changed, 329 insertions, 166 deletions
@@ -87,9 +87,12 @@ Index * A new Index class allows to read and write index files directly, and to perform simple two and three way merges based on an arbitrary index. -Refs ----- -* Will dynmically retrieve their object at the time of query to assure the information +Referernces +------------ +* References are object that point to a Commit +* SymbolicReference are a pointer to a Reference Object, which itself points to a specific + Commit +* They will dynmically retrieve their object at the time of query to assure the information is actual. Recently objects would be cached, hence ref object not be safely kept persistent. @@ -58,14 +58,12 @@ Index creating several tree objects, so in the end it might be slower. Hmm, probably its okay to use the command unless we go c(++) - -Head.reset ----------- -* Should better be an instance method. Problem was that there is no class specifying - the HEAD - in a way reset would always effect the active branch. - Probably it would be okay to have a special type called SymbolicReference - which represents items like HEAD. These could naturally carry the reset - instance method. +Refs +----- +* If the HEAD is detached as it points to a specific commit, its not technically + a symbolic reference anymore. Currently, we cannot handle this that well + as we do not check for this case. This should be added though as it is + valid to have a detached head in some cases. Remote ------ diff --git a/lib/git/refs.py b/lib/git/refs.py index 9a03b6f5..a83628ce 100644 --- a/lib/git/refs.py +++ b/lib/git/refs.py @@ -6,7 +6,8 @@ """ Module containing all ref based objects """ -from objects.base import Object +import os +from objects import Object, Commit from objects.utils import get_object_type_by_name from utils import LazyMixin, Iterable @@ -31,6 +32,9 @@ class Reference(LazyMixin, Iterable): ``object`` Object instance, will be retrieved on demand if None """ + if not path.startswith(self._common_path_default): + raise ValueError("Cannot instantiate %s Reference from path %s" % ( self.__class__.__name__, path )) + self.repo = repo self.path = path if object is not None: @@ -75,6 +79,17 @@ class Reference(LazyMixin, Iterable): # have to be dynamic here as we may be a tag which can point to anything # Our path will be resolved to the hexsha which will be used accordingly return Object.new(self.repo, self.path) + + @property + def commit(self): + """ + Returns + Commit object the head points to + """ + commit = self.object + if commit.type != "commit": + raise TypeError("Object of reference %s did not point to a commit" % self) + return commit @classmethod def iter_items(cls, repo, common_path = None, **kwargs): @@ -112,6 +127,29 @@ class Reference(LazyMixin, Iterable): output = repo.git.for_each_ref(common_path, **options) return cls._iter_from_stream(repo, iter(output.splitlines())) + + @classmethod + def from_path(cls, repo, path): + """ + Return + Instance of type Reference, Head, Tag, SymbolicReference or HEAD + depending on the given path + """ + if path == 'HEAD': + return HEAD(repo, path) + + if '/' not in path: + return SymbolicReference(repo, path) + + for ref_type in (Head, RemoteReference, TagReference, Reference): + try: + return ref_type(repo, path) + except ValueError: + pass + # END exception handling + # END for each type to try + raise ValueError("Could not find reference type suitable to handle path %r" % path) + @classmethod def _iter_from_stream(cls, repo, stream): @@ -145,47 +183,150 @@ class Reference(LazyMixin, Iterable): # return cls(repo, full_path, obj) -class Head(Reference): +class SymbolicReference(object): """ - A Head is a named reference to a Commit. Every Head instance contains a name - and a Commit object. - - Examples:: - - >>> repo = Repo("/path/to/repo") - >>> head = repo.heads[0] - - >>> head.name - 'master' - - >>> head.commit - <git.Commit "1c09f116cbc2cb4100fb6935bb162daa4723f455"> - - >>> head.commit.id - '1c09f116cbc2cb4100fb6935bb162daa4723f455' + Represents a special case of a reference such that this reference is symbolic. + It does not point to a specific commit, but to another Head, which itself + specifies a commit. + + A typical example for a symbolic reference is HEAD. """ - _common_path_default = "refs/heads" + __slots__ = ("repo", "name") + def __init__(self, repo, name): + if '/' in name: + raise ValueError("SymbolicReferences are not located within a directory, got %s" % name) + self.repo = repo + self.name = name + + def __str__(self): + return self.name + + def __repr__(self): + return '<git.%s "%s">' % (self.__class__.__name__, self.name) + + def __eq__(self, other): + return self.name == other.name + + def __ne__(self, other): + return not ( self == other ) + + def __hash__(self): + return hash(self.name) + + def _get_path(self): + return os.path.join(self.repo.path, self.name) + @property def commit(self): """ + Returns: + Commit object we point to, works for detached and non-detached + SymbolicReferences + """ + # we partially reimplement it to prevent unnecessary file access + fp = open(self._get_path(), 'r') + value = fp.read().rstrip() + fp.close() + tokens = value.split(" ") + + # it is a detached reference + if len(tokens) == 1 and len(tokens[0]) == 40: + return Commit(self.repo, tokens[0]) + + # must be a head ! Git does not allow symbol refs to other things than heads + # Otherwise it would have detached it + return Head(self.repo, tokens[1]).commit + + def _get_reference(self): + """ Returns - Commit object the head points to + Reference Object we point to """ - return self.object + fp = open(self._get_path(), 'r') + try: + tokens = fp.readline().rstrip().split(' ') + if tokens[0] != 'ref:': + raise TypeError("%s is a detached symbolic reference as it points to %r" % tokens[0]) + return Reference.from_path(self.repo, tokens[1]) + finally: + fp.close() - @classmethod - def reset(cls, repo, commit='HEAD', index=True, working_tree = False, + def _set_reference(self, ref): + """ + Set ourselves to the given ref. It will stay a symbol if the ref is a Head. + Otherwise we try to get a commit from it using our interface. + + Strings are allowed but will be checked to be sure we have a commit + """ + write_value = None + if isinstance(ref, Head): + write_value = "ref: %s" % ref.path + elif isinstance(ref, Commit): + write_value = ref.id + else: + try: + write_value = ref.commit.id + except AttributeError: + sha = str(ref) + try: + obj = Object.new(self.repo, sha) + if obj.type != "commit": + raise TypeError("Invalid object type behind sha: %s" % sha) + write_value = obj.id + except Exception: + raise ValueError("Could not extract object from %s" % ref) + # END end try string + # END try commit attribute + fp = open(self._get_path(), "w") + try: + fp.write(write_value) + finally: + fp.close() + # END writing + + reference = property(_get_reference, _set_reference, doc="Returns the Reference we point to") + + # alias + ref = reference + + @property + def is_detached(self): + """ + Returns + True if we are a detached reference, hence we point to a specific commit + instead to another reference + """ + try: + self.reference + return False + except TypeError: + return True + + +class HEAD(SymbolicReference): + """ + Special case of a Symbolic Reference as it represents the repository's + HEAD reference. + """ + _HEAD_NAME = 'HEAD' + __slots__ = tuple() + + def __init__(self, repo, name=_HEAD_NAME): + if name != self._HEAD_NAME: + raise ValueError("HEAD instance must point to %r, got %r" % (self._HEAD_NAME, name)) + super(HEAD, self).__init__(repo, name) + + + def reset(self, commit='HEAD', index=True, working_tree = False, paths=None, **kwargs): """ - Reset the current head to the given commit optionally synchronizing + Reset our HEAD to the given commit optionally synchronizing the index and working tree. - ``repo`` - Repository containing commit - ``commit`` - Commit object, Reference Object or string identifying a revision + Commit object, Reference Object or string identifying a revision we + should reset HEAD to. ``index`` If True, the index will be set to match the given commit. Otherwise @@ -204,7 +345,7 @@ class Head(Reference): Additional arguments passed to git-reset. Returns - Head pointing to the specified commit + self """ mode = "--soft" if index: @@ -216,12 +357,34 @@ class Head(Reference): raise ValueError( "Cannot reset the working tree if the index is not reset as well") # END working tree handling - repo.git.reset(mode, commit, paths, **kwargs) + self.repo.git.reset(mode, commit, paths, **kwargs) - # we always point to the active branch as it is the one changing - return repo.active_branch + return self + + +class Head(Reference): + """ + A Head is a named reference to a Commit. Every Head instance contains a name + and a Commit object. + + Examples:: + + >>> repo = Repo("/path/to/repo") + >>> head = repo.heads[0] + + >>> head.name + 'master' + + >>> head.commit + <git.Commit "1c09f116cbc2cb4100fb6935bb162daa4723f455"> + + >>> head.commit.id + '1c09f116cbc2cb4100fb6935bb162daa4723f455' + """ + _common_path_default = "refs/heads" + -class TagReference(Head): +class TagReference(Reference): """ Class representing a lightweight tag reference which either points to a commit or to a tag object. In the latter case additional information, like the signature @@ -230,7 +393,7 @@ class TagReference(Head): This tag object will always point to a commit object, but may carray additional information in a tag object:: - tagref = TagRef.list_items(repo)[0] + tagref = TagReference.list_items(repo)[0] print tagref.commit.message if tagref.tag is not None: print tagref.tag.message diff --git a/lib/git/repo.py b/lib/git/repo.py index b6624d8b..94555a31 100644 --- a/lib/git/repo.py +++ b/lib/git/repo.py @@ -147,15 +147,12 @@ class Repo(object): branches = heads @property - def head(self,path="HEAD"): + def head(self): """ Return - Head Object, reference pointing to commit - - ``path`` - path to the head or its name, i.e. master or heads/master + HEAD Object pointing to the current head reference """ - return Head(self,path) + return HEAD(self,'HEAD') @property def remotes(self): @@ -486,8 +483,7 @@ class Repo(object): Returns Head to the active branch """ - return Head( self, self.git.symbolic_ref('HEAD').strip() ) - + return self.head.reference def blame(self, rev, file): """ diff --git a/test/git/test_base.py b/test/git/test_base.py index 3472608e..1b78786a 100644 --- a/test/git/test_base.py +++ b/test/git/test_base.py @@ -66,50 +66,6 @@ class TestBase(TestBase): assert len(s|s) == num_objs assert num_index_objs == 2 - - def test_tags(self): - # tag refs can point to tag objects or to commits - s = set() - ref_count = 0 - for ref in chain(self.rorepo.tags, self.rorepo.heads): - ref_count += 1 - assert isinstance(ref, refs.Reference) - assert str(ref) == ref.name - assert repr(ref) - assert ref == ref - assert not ref != ref - s.add(ref) - # END for each ref - assert len(s) == ref_count - assert len(s|s) == ref_count - - def test_heads(self): - # see how it dynmically updates its object - for head in self.rorepo.heads: - head.name - head.path - prev_object = head.object - cur_object = head.object - assert prev_object == cur_object # represent the same git object - assert prev_object is not cur_object # but are different instances - # END for each head - - @with_rw_repo('0.1.6') - def test_head_reset(self, rw_repo): - cur_head = rw_repo.head - new_head_commit = cur_head.commit.parents[0] - reset_head = Head.reset(rw_repo, new_head_commit, index=True) # index only - assert reset_head.commit == new_head_commit - - self.failUnlessRaises(ValueError, Head.reset, rw_repo, new_head_commit, index=False, working_tree=True) - new_head_commit = new_head_commit.parents[0] - reset_head = Head.reset(rw_repo, new_head_commit, index=True, working_tree=True) # index + wt - assert reset_head.commit == new_head_commit - - # paths - Head.reset(rw_repo, new_head_commit, paths = "lib") - - def test_get_object_type_by_name(self): for tname in base.Object.TYPES: assert base.Object in get_object_type_by_name(tname).mro() @@ -119,7 +75,7 @@ class TestBase(TestBase): def test_object_resolution(self): # objects must be resolved to shas so they compare equal - assert self.rorepo.head.object == self.rorepo.active_branch.object + assert self.rorepo.head.reference.object == self.rorepo.active_branch.object @with_bare_rw_repo def test_with_bare_rw_repo(self, bare_rw_repo): diff --git a/test/git/test_commit.py b/test/git/test_commit.py index 1a74593d..c4ed4b72 100644 --- a/test/git/test_commit.py +++ b/test/git/test_commit.py @@ -64,7 +64,7 @@ class TestCommit(TestBase): assert_equal(sha1, commit.id) def test_count(self): - assert self.rorepo.tag('0.1.5').commit.count( ) == 141 + assert self.rorepo.tag('refs/tags/0.1.5').commit.count( ) == 141 def test_list(self): assert isinstance(Commit.list_items(self.rorepo, '0.1.5', max_count=5)['5117c9c8a4d3af19a9958677e45cda9269de1541'], Commit) diff --git a/test/git/test_head.py b/test/git/test_head.py deleted file mode 100644 index 9b18ad7c..00000000 --- a/test/git/test_head.py +++ /dev/null @@ -1,24 +0,0 @@ -# test_head.py -# Copyright (C) 2008, 2009 Michael Trier (mtrier@gmail.com) and contributors -# -# This module is part of GitPython and is released under -# the BSD License: http://www.opensource.org/licenses/bsd-license.php - -from test.testlib import * -from git import * - -class TestHead(TestBase): - - def test_base(self): - for head in self.rorepo.heads: - assert head.name - assert "refs/heads" in head.path - # END for each head - - @patch_object(Git, '_call_process') - def test_ref_with_path_component(self, git): - git.return_value = fixture('for_each_ref_with_path_component') - head = self.rorepo.heads[0] - - assert_equal('refactoring/feature1', head.name) - assert_true(git.called) diff --git a/test/git/test_index.py b/test/git/test_index.py index 257acf10..7bc2ad7e 100644 --- a/test/git/test_index.py +++ b/test/git/test_index.py @@ -116,8 +116,8 @@ class TestTree(TestBase): # test diff # resetting the head will leave the index in a different state, and the # diff will yield a few changes - cur_head_commit = rw_repo.head.commit - ref = rw_repo.head.reset(rw_repo, 'HEAD~6', index=True, working_tree=False) + cur_head_commit = rw_repo.head.reference.commit + ref = rw_repo.head.reset('HEAD~6', index=True, working_tree=False) # diff against same index is 0 diff = index.diff() diff --git a/test/git/test_refs.py b/test/git/test_refs.py new file mode 100644 index 00000000..1562310a --- /dev/null +++ b/test/git/test_refs.py @@ -0,0 +1,110 @@ +# test_refs.py +# Copyright (C) 2008, 2009 Michael Trier (mtrier@gmail.com) and contributors +# +# This module is part of GitPython and is released under +# the BSD License: http://www.opensource.org/licenses/bsd-license.php + +from mock import * +from test.testlib import * +from git import * +import git.refs as refs +from git.objects.tag import TagObject +from itertools import chain + +class TestRefs(TestBase): + + def test_tag_base(self): + tag_object_refs = list() + for tag in self.rorepo.tags: + assert "refs/tags" in tag.path + assert tag.name + assert isinstance( tag.commit, Commit ) + if tag.tag is not None: + tag_object_refs.append( tag ) + tagobj = tag.tag + assert isinstance( tagobj, TagObject ) + assert tagobj.tag == tag.name + assert isinstance( tagobj.tagger, Actor ) + assert isinstance( tagobj.tagged_date, int ) + assert tagobj.message + # END if we have a tag object + # END for tag in repo-tags + assert tag_object_refs + assert isinstance(self.rorepo.tags['0.1.5'], TagReference) + + @patch_object(Git, '_call_process') + def test_ref_with_path_component(self, git): + git.return_value = fixture('for_each_ref_with_path_component') + head = self.rorepo.heads[0] + + assert_equal('refactoring/feature1', head.name) + assert_true(git.called) + + + def test_tags(self): + # tag refs can point to tag objects or to commits + s = set() + ref_count = 0 + for ref in chain(self.rorepo.tags, self.rorepo.heads): + ref_count += 1 + assert isinstance(ref, refs.Reference) + assert str(ref) == ref.name + assert repr(ref) + assert ref == ref + assert not ref != ref + s.add(ref) + # END for each ref + assert len(s) == ref_count + assert len(s|s) == ref_count + + def test_heads(self): + for head in self.rorepo.heads: + assert head.name + assert head.path + assert "refs/heads" in head.path + prev_object = head.object + cur_object = head.object + assert prev_object == cur_object # represent the same git object + assert prev_object is not cur_object # but are different instances + # END for each head + + @with_rw_repo('0.1.6') + def test_head_reset(self, rw_repo): + cur_head = rw_repo.head + new_head_commit = cur_head.ref.commit.parents[0] + cur_head.reset(new_head_commit, index=True) # index only + assert cur_head.reference.commit == new_head_commit + + self.failUnlessRaises(ValueError, cur_head.reset, new_head_commit, index=False, working_tree=True) + new_head_commit = new_head_commit.parents[0] + cur_head.reset(new_head_commit, index=True, working_tree=True) # index + wt + assert cur_head.reference.commit == new_head_commit + + # paths + cur_head.reset(new_head_commit, paths = "lib") + + + # now that we have a write write repo, change the HEAD reference - its + # like git-reset --soft + heads = rw_repo.heads + assert heads + for head in heads: + cur_head.reference = head + assert cur_head.reference == head + assert cur_head.commit == head.commit + assert not cur_head.is_detached + # END for each head + + # detach + cur_head.reference = heads[0].commit + assert cur_head.commit == heads[0].commit + assert cur_head.is_detached + self.failUnlessRaises(TypeError, getattr, cur_head, "reference") + + some_tag = rw_repo.tags[0] + cur_head.reference = some_tag + assert cur_head.is_detached + assert cur_head.commit == some_tag.commit + + # type check + self.failUnlessRaises(ValueError, setattr, cur_head, "reference", "that") diff --git a/test/git/test_repo.py b/test/git/test_repo.py index 02eea7de..b02610f4 100644 --- a/test/git/test_repo.py +++ b/test/git/test_repo.py @@ -168,17 +168,11 @@ class TestRepo(TestBase): self.rorepo._bare = True assert self.rorepo.is_dirty == False - @patch_object(Git, '_call_process') - def test_active_branch(self, git): - git.return_value = 'refs/heads/major-refactoring' - assert_equal(self.rorepo.active_branch.name, 'major-refactoring') - assert_equal(git.call_args, (('symbolic_ref', 'HEAD'), {})) - def test_head(self): - assert self.rorepo.head.object == self.rorepo.active_branch.object + assert self.rorepo.head.reference.object == self.rorepo.active_branch.object def test_tag(self): - assert self.rorepo.tag('0.1.5').commit + assert self.rorepo.tag('refs/tags/0.1.5').commit def test_archive(self): tmpfile = os.tmpfile() diff --git a/test/git/test_tag.py b/test/git/test_tag.py deleted file mode 100644 index 97e0acd1..00000000 --- a/test/git/test_tag.py +++ /dev/null @@ -1,33 +0,0 @@ -# test_tag.py -# Copyright (C) 2008, 2009 Michael Trier (mtrier@gmail.com) and contributors -# -# This module is part of GitPython and is released under -# the BSD License: http://www.opensource.org/licenses/bsd-license.php - -from mock import * -from test.testlib import * -from git import * -from git.objects.tag import TagObject - -class TestTag(TestBase): - - def test_tag_base(self): - tag_object_refs = list() - for tag in self.rorepo.tags: - assert "refs/tags" in tag.path - assert tag.name - assert isinstance( tag.commit, Commit ) - if tag.tag is not None: - tag_object_refs.append( tag ) - tagobj = tag.tag - assert isinstance( tagobj, TagObject ) - assert tagobj.tag == tag.name - assert isinstance( tagobj.tagger, Actor ) - assert isinstance( tagobj.tagged_date, int ) - assert tagobj.message - # END if we have a tag object - # END for tag in repo-tags - assert tag_object_refs - assert isinstance(self.rorepo.tags['0.1.5'], TagReference) - - |