diff options
author | Sebastian Thiel <byronimo@gmail.com> | 2009-10-26 10:57:11 +0100 |
---|---|---|
committer | Sebastian Thiel <byronimo@gmail.com> | 2009-10-26 10:57:11 +0100 |
commit | 9dfd6bcca5499e1cd472c092bc58426e9b72cccc (patch) | |
tree | 12f2418d0843460d1d51c346dd35325fabc12a09 | |
parent | f9cec00938d9059882bb8eabdaf2f775943e00e5 (diff) | |
parent | 44a601a068f4f543f73fd9c49e264c931b1e1652 (diff) | |
download | gitpython-9dfd6bcca5499e1cd472c092bc58426e9b72cccc.tar.gz |
Merge branch 'refs' into index
* refs:
Added notes about git-update-ref
Refs can now set the reference they are pointing to in a controlled fashion by writing their ref file directly
Added TagRefernce creation and deletion including tests
Implemented head methods: create, delete, rename, including tests
refs: added create, delete and rename methods where appropriate. Tests are marked, implementation is needed for most of them
-rw-r--r-- | TODO | 19 | ||||
-rw-r--r-- | lib/git/refs.py | 196 | ||||
-rw-r--r-- | test/git/test_refs.py | 90 |
3 files changed, 282 insertions, 23 deletions
@@ -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 |