summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--TODO19
-rw-r--r--lib/git/refs.py196
-rw-r--r--test/git/test_refs.py90
3 files changed, 282 insertions, 23 deletions
diff --git a/TODO b/TODO
index 17c6480a..69fbc307 100644
--- a/TODO
+++ b/TODO
@@ -87,22 +87,21 @@ Refs
-----
* When adjusting the reference of a symbolic reference, the ref log might need
adjustments as well. This is not critical, but would make things totally 'right'
-* Reference Objects should be able to set the commit they are pointing to, making
- the commit property read-write. Tags are a special case of this and would need
- to be handled as well !
-* Ability to create new heads and tags in the Repository ( but using the respective
- Reference Type ), i.e. Head.create(repo, name, commit = 'HEAD') or
- TagReference.create(repo, name
-* Ability to rename references and tags
-* Ability to remove references and tags
-* Ability to checkout a reference -
-* Check whether we are the active reference HEAD.commit == self.commit
+ - same with adjusting references directly
+ !! - Could simply rewrite it using git-update-ref which works nicely for symbolic
+ and for normal refs !!
+* Check whether we are the active reference HEAD.reference == this_ref
+ - NO: The reference dosnt need to know - in fact it does not know about the
+ main HEAD, so it may not use it. This is to be done in client code only.
+ Remove me
Remote
------
* 'push' method needs a test, a true test repository is required though, a fork
of a fork would do :)!
* Fetch should return heads that where updated, pull as well.
+* Creation and deletion methods for references should be part of the interface, allowing
+ repo.create_head(...) instaed of Head.create(repo, ...). Its a convenience thing, clearly
Repo
----
diff --git a/lib/git/refs.py b/lib/git/refs.py
index c8857e97..47c37af6 100644
--- a/lib/git/refs.py
+++ b/lib/git/refs.py
@@ -19,7 +19,7 @@ class Reference(LazyMixin, Iterable):
_common_path_default = "refs"
_id_attribute_ = "name"
- def __init__(self, repo, path, object = None):
+ def __init__(self, repo, path):
"""
Initialize this instance
``repo``
@@ -29,16 +29,12 @@ class Reference(LazyMixin, Iterable):
Path relative to the .git/ directory pointing to the ref in question, i.e.
refs/heads/master
- ``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:
- self.object = object
def __str__(self):
return self.name
@@ -69,8 +65,7 @@ class Reference(LazyMixin, Iterable):
return '/'.join(tokens[2:])
- @property
- def object(self):
+ def _get_object(self):
"""
Returns
The object our ref currently refers to. Refs can be cached, they will
@@ -80,17 +75,54 @@ class Reference(LazyMixin, Iterable):
# Our path will be resolved to the hexsha which will be used accordingly
return Object.new(self.repo, self.path)
- @property
- def commit(self):
+ def _set_object(self, ref, type=None):
+ """
+ Set our reference to point to the given ref. It will be converted
+ to a specific hexsha.
+
+ ``type``
+ If not None, string type of that the object must have, other we raise
+ a type error. Only used internally
+
+ Returns
+ Object we have set. This is used internally only to reduce the amount
+ of calls to the git command
+ """
+ obj = Object.new(self.repo, ref)
+ if type is not None and obj.type != type:
+ raise TypeError("Reference %r cannot point to object of type %r" % (self,obj.type))
+
+ full_ref_path = os.path.join(self.repo.path, self.path)
+ fp = open(full_ref_path, "w")
+ try:
+ fp.write(str(obj))
+ finally:
+ fp.close()
+ return obj
+
+ object = property(_get_object, _set_object, doc="Return the object our ref currently refers to")
+
+ def _set_commit(self, commit):
+ """
+ Set ourselves to point to the given commit.
+
+ Raise
+ ValueError if commit does not actually point to a commit
+ """
+ self._set_object(commit, type="commit")
+
+ def _get_commit(self):
"""
Returns
- Commit object the head points to
+ Commit object the reference points to
"""
commit = self.object
if commit.type != "commit":
raise TypeError("Object of reference %s did not point to a commit" % self)
return commit
+ commit = property(_get_commit, _set_commit, doc="Return Commit object the reference points to")
+
@classmethod
def iter_items(cls, repo, common_path = None, **kwargs):
"""
@@ -182,6 +214,7 @@ class Reference(LazyMixin, Iterable):
# obj.size = object_size
# return cls(repo, full_path, obj)
+
class SymbolicReference(object):
"""
@@ -247,7 +280,7 @@ class SymbolicReference(object):
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])
+ raise TypeError("%s is a detached symbolic reference as it points to %r" % (self, tokens[0]))
return Reference.from_path(self.repo, tokens[1])
finally:
fp.close()
@@ -384,12 +417,92 @@ class Head(Reference):
"""
_common_path_default = "refs/heads"
+ @classmethod
+ def create(cls, repo, path, commit='HEAD', force=False, **kwargs ):
+ """
+ Create a new head.
+ ``repo``
+ Repository to create the head in
+
+ ``path``
+ The name or path of the head, i.e. 'new_branch' or
+ feature/feature1. The prefix refs/heads is implied.
+
+ ``commit``
+ Commit to which the new head should point, defaults to the
+ current HEAD
+
+ ``force``
+ if True, force creation even if branch with that name already exists.
+
+ ``**kwargs``
+ Additional keyword arguments to be passed to git-branch, i.e.
+ track, no-track, l
+
+ Returns
+ Newly created Head
+
+ Note
+ This does not alter the current HEAD, index or Working Tree
+ """
+ if cls is not Head:
+ raise TypeError("Only Heads can be created explicitly, not objects of type %s" % cls.__name__)
+
+ args = ( path, commit )
+ if force:
+ kwargs['f'] = True
+
+ repo.git.branch(*args, **kwargs)
+ return cls(repo, "%s/%s" % ( cls._common_path_default, path))
+
+
+ @classmethod
+ def delete(cls, repo, *heads, **kwargs):
+ """
+ Delete the given heads
+
+ ``force``
+ If True, the heads will be deleted even if they are not yet merged into
+ the main development stream.
+ Default False
+ """
+ force = kwargs.get("force", False)
+ flag = "-d"
+ if force:
+ flag = "-D"
+ repo.git.branch(flag, *heads)
+
+
+ def rename(self, new_path, force=False):
+ """
+ Rename self to a new path
+
+ ``new_path``
+ Either a simple name or a path, i.e. new_name or features/new_name.
+ The prefix refs/heads is implied
+
+ ``force``
+ If True, the rename will succeed even if a head with the target name
+ already exists.
+
+ Returns
+ self
+ """
+ flag = "-m"
+ if force:
+ flag = "-M"
+
+ self.repo.git.branch(flag, self, new_path)
+ self.path = "%s/%s" % (self._common_path_default, new_path)
+ return self
+
+
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
- or the tag-creator, is available.
+ ,a tag object or any other object. In the latter case additional information,
+ like the signature or the tag-creator, is available.
This tag object will always point to a commit object, but may carray additional
information in a tag object::
@@ -427,6 +540,52 @@ class TagReference(Reference):
if self.object.type == "tag":
return self.object
return None
+
+ @classmethod
+ def create(cls, repo, path, ref='HEAD', message=None, force=False, **kwargs):
+ """
+ Create a new tag object.
+
+ ``path``
+ The name of the tag, i.e. 1.0 or releases/1.0.
+ The prefix refs/tags is implied
+
+ ``ref``
+ A reference to the object you want to tag. It can be a commit, tree or
+ blob.
+
+ ``message``
+ If not None, the message will be used in your tag object. This will also
+ create an additional tag object that allows to obtain that information, i.e.::
+ tagref.tag.message
+
+ ``force``
+ If True, to force creation of a tag even though that tag already exists.
+
+ ``**kwargs``
+ Additional keyword arguments to be passed to git-tag
+
+ Returns
+ A new TagReference
+ """
+ args = ( path, ref )
+ if message:
+ kwargs['m'] = message
+ if force:
+ kwargs['f'] = True
+
+ repo.git.tag(*args, **kwargs)
+ return TagReference(repo, "%s/%s" % (cls._common_path_default, path))
+
+ @classmethod
+ def delete(cls, repo, *tags):
+ """
+ Delete the given existing tag or tags
+ """
+ repo.git.tag("-d", *tags)
+
+
+
# provide an alias
@@ -460,3 +619,14 @@ class RemoteReference(Head):
"""
tokens = self.path.split('/')
return '/'.join(tokens[3:])
+
+ @classmethod
+ def delete(cls, repo, *remotes, **kwargs):
+ """
+ Delete the given remote references.
+
+ Note
+ kwargs are given for compatability with the base class method as we
+ should not narrow the signature.
+ """
+ repo.git.branch("-d", "-r", *remotes)
diff --git a/test/git/test_refs.py b/test/git/test_refs.py
index 1562310a..696b95c7 100644
--- a/test/git/test_refs.py
+++ b/test/git/test_refs.py
@@ -108,3 +108,93 @@ class TestRefs(TestBase):
# type check
self.failUnlessRaises(ValueError, setattr, cur_head, "reference", "that")
+
+ # head handling
+ commit = 'HEAD'
+ prev_head_commit = cur_head.commit
+ for count, new_name in enumerate(("my_new_head", "feature/feature1")):
+ actual_commit = commit+"^"*count
+ new_head = Head.create(rw_repo, new_name, actual_commit)
+ assert cur_head.commit == prev_head_commit
+ assert isinstance(new_head, Head)
+ # already exists
+ self.failUnlessRaises(GitCommandError, Head.create, rw_repo, new_name)
+
+ # force it
+ new_head = Head.create(rw_repo, new_name, actual_commit, force=True)
+ old_path = new_head.path
+ old_name = new_head.name
+
+ assert new_head.rename("hello").name == "hello"
+ assert new_head.rename("hello/world").name == "hello/world"
+ assert new_head.rename(old_name).name == old_name and new_head.path == old_path
+
+ # rename with force
+ tmp_head = Head.create(rw_repo, "tmphead")
+ self.failUnlessRaises(GitCommandError, tmp_head.rename, new_head)
+ tmp_head.rename(new_head, force=True)
+ assert tmp_head == new_head and tmp_head.object == new_head.object
+
+ Head.delete(rw_repo, tmp_head)
+ heads = rw_repo.heads
+ assert tmp_head not in heads and new_head not in heads
+ # force on deletion testing would be missing here, code looks okay though ;)
+ # END for each new head name
+ self.failUnlessRaises(TypeError, RemoteReference.create, rw_repo, "some_name")
+
+ # tag ref
+ tag_name = "1.0.2"
+ light_tag = TagReference.create(rw_repo, tag_name)
+ self.failUnlessRaises(GitCommandError, TagReference.create, rw_repo, tag_name)
+ light_tag = TagReference.create(rw_repo, tag_name, "HEAD~1", force = True)
+ assert isinstance(light_tag, TagReference)
+ assert light_tag.name == tag_name
+ assert light_tag.commit == cur_head.commit.parents[0]
+ assert light_tag.tag is None
+
+ # tag with tag object
+ other_tag_name = "releases/1.0.2RC"
+ msg = "my mighty tag\nsecond line"
+ obj_tag = TagReference.create(rw_repo, other_tag_name, message=msg)
+ assert isinstance(obj_tag, TagReference)
+ assert obj_tag.name == other_tag_name
+ assert obj_tag.commit == cur_head.commit
+ assert obj_tag.tag is not None
+
+ TagReference.delete(rw_repo, light_tag, obj_tag)
+ tags = rw_repo.tags
+ assert light_tag not in tags and obj_tag not in tags
+
+ # remote deletion
+ remote_refs_so_far = 0
+ remotes = rw_repo.remotes
+ assert remotes
+ for remote in remotes:
+ refs = remote.refs
+ RemoteReference.delete(rw_repo, *refs)
+ remote_refs_so_far += len(refs)
+ # END for each ref to delete
+ assert remote_refs_so_far
+
+ for remote in remotes:
+ # remotes without references throw
+ self.failUnlessRaises(AssertionError, getattr, remote, 'refs')
+ # END for each remote
+
+ # change where the active head points to
+ if cur_head.is_detached:
+ cur_head.reference = rw_repo.heads[0]
+
+ head = cur_head.reference
+ old_commit = head.commit
+ head.commit = old_commit.parents[0]
+ assert head.commit == old_commit.parents[0]
+ assert head.commit == cur_head.commit
+ head.commit = old_commit
+
+ # setting a non-commit as commit fails, but succeeds as object
+ head_tree = head.commit.tree
+ self.failUnlessRaises(TypeError, setattr, head, 'commit', head_tree)
+ assert head.commit == old_commit # and the ref did not change
+ head.object = head_tree
+ assert head.object == head_tree