diff options
-rw-r--r-- | CHANGES | 9 | ||||
-rw-r--r-- | TODO | 14 | ||||
-rw-r--r-- | lib/git/refs.py | 169 | ||||
-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_refs.py | 84 | ||||
-rw-r--r-- | test/git/test_tag.py | 33 |
9 files changed, 239 insertions, 154 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..97d0a6eb 100644 --- a/lib/git/refs.py +++ b/lib/git/refs.py @@ -6,6 +6,7 @@ """ Module containing all ref based objects """ +import os from objects.base import Object 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,91 @@ 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) + @property - def commit(self): + def reference(self): """ Returns - Commit object the head points to + Reference Object we point to """ - return self.object + fp = open(os.path.join(self.repo.path, self.name), 'r') + try: + tokens = fp.readline().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, + # 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. + """ + __slots__ = tuple() + + def __init__(self, repo, name): + if name != 'HEAD': + raise ValueError("HEAD instance must point to 'HEAD', got %s" % 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 +286,7 @@ class Head(Reference): Additional arguments passed to git-reset. Returns - Head pointing to the specified commit + self """ mode = "--soft" if index: @@ -219,9 +301,32 @@ class Head(Reference): repo.git.reset(mode, commit, paths, **kwargs) # we always point to the active branch as it is the one changing - return repo.active_branch + 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 +335,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_refs.py b/test/git/test_refs.py new file mode 100644 index 00000000..ece6bf3e --- /dev/null +++ b/test/git/test_refs.py @@ -0,0 +1,84 @@ +# 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] + 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") 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) - - |