diff options
Diffstat (limited to 'git/refs')
| -rw-r--r-- | git/refs/__init__.py | 11 | ||||
| -rw-r--r-- | git/refs/head.py | 169 | ||||
| -rw-r--r-- | git/refs/headref.py | 170 | ||||
| -rw-r--r-- | git/refs/log.py | 5 | ||||
| -rw-r--r-- | git/refs/reference.py | 18 | ||||
| -rw-r--r-- | git/refs/remote.py | 22 | ||||
| -rw-r--r-- | git/refs/symbolic.py | 158 | ||||
| -rw-r--r-- | git/refs/tag.py | 7 |
8 files changed, 299 insertions, 261 deletions
diff --git a/git/refs/__init__.py b/git/refs/__init__.py index fc8ce644..35b69fca 100644 --- a/git/refs/__init__.py +++ b/git/refs/__init__.py @@ -2,19 +2,20 @@ # import all modules in order, fix the names they require from symbolic import * from reference import * +from headref import * from head import * from tag import * from remote import * # name fixes -import head -head.RemoteReference = RemoteReference -del(head) +import headref +headref.Head.RemoteReferenceCls = RemoteReference +del(headref) import symbolic -for item in (HEAD, Head, RemoteReference, TagReference, Reference, SymbolicReference): - setattr(symbolic, item.__name__, item) +for item in (HEAD, Head, RemoteReference, TagReference, Reference): + setattr(symbolic.SymbolicReference, item.__name__+'Cls', item) del(symbolic) diff --git a/git/refs/head.py b/git/refs/head.py index d8729434..4345528b 100644 --- a/git/refs/head.py +++ b/git/refs/head.py @@ -1,19 +1,14 @@ -from symbolic import SymbolicReference -from reference import Reference - -from git.config import SectionConstraint - -from git.util import join_path +from symbolic import SymbolicReference from git.exc import GitCommandError -__all__ = ["HEAD", "Head"] - +__all__ = ["HEAD"] class HEAD(SymbolicReference): - """Special case of a Symbolic Reference as it represents the repository's - HEAD reference.""" + """Provides additional functionality using the git command""" + __slots__ = tuple() + _HEAD_NAME = 'HEAD' _ORIG_HEAD_NAME = 'ORIG_HEAD' __slots__ = tuple() @@ -90,157 +85,3 @@ class HEAD(SymbolicReference): 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.hexsha - '1c09f116cbc2cb4100fb6935bb162daa4723f455'""" - _common_path_default = "refs/heads" - k_config_remote = "remote" - k_config_remote_ref = "merge" # branch to merge from remote - - @classmethod - def delete(cls, repo, *heads, **kwargs): - """Delete the given heads - :param 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 set_tracking_branch(self, remote_reference): - """ - Configure this branch to track the given remote reference. This will alter - this branch's configuration accordingly. - - :param remote_reference: The remote reference to track or None to untrack - any references - :return: self""" - if remote_reference is not None and not isinstance(remote_reference, RemoteReference): - raise ValueError("Incorrect parameter type: %r" % remote_reference) - # END handle type - - writer = self.config_writer() - if remote_reference is None: - writer.remove_option(self.k_config_remote) - writer.remove_option(self.k_config_remote_ref) - if len(writer.options()) == 0: - writer.remove_section() - # END handle remove section - else: - writer.set_value(self.k_config_remote, remote_reference.remote_name) - writer.set_value(self.k_config_remote_ref, Head.to_full_path(remote_reference.remote_head)) - # END handle ref value - - return self - - - def tracking_branch(self): - """ - :return: The remote_reference we are tracking, or None if we are - not a tracking branch""" - reader = self.config_reader() - if reader.has_option(self.k_config_remote) and reader.has_option(self.k_config_remote_ref): - ref = Head(self.repo, Head.to_full_path(reader.get_value(self.k_config_remote_ref))) - remote_refpath = RemoteReference.to_full_path(join_path(reader.get_value(self.k_config_remote), ref.name)) - return RemoteReference(self.repo, remote_refpath) - # END handle have tracking branch - - # we are not a tracking branch - return None - - def rename(self, new_path, force=False): - """Rename self to a new path - - :param new_path: - Either a simple name or a path, i.e. new_name or features/new_name. - The prefix refs/heads is implied - - :param force: - If True, the rename will succeed even if a head with the target name - already exists. - - :return: self - :note: respects the ref log as git commands are used""" - 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 - - def checkout(self, force=False, **kwargs): - """Checkout this head by setting the HEAD to this reference, by updating the index - to reflect the tree we point to and by updating the working tree to reflect - the latest index. - - The command will fail if changed working tree files would be overwritten. - - :param force: - If True, changes to the index and the working tree will be discarded. - If False, GitCommandError will be raised in that situation. - - :param kwargs: - Additional keyword arguments to be passed to git checkout, i.e. - b='new_branch' to create a new branch at the given spot. - - :return: - The active branch after the checkout operation, usually self unless - a new branch has been created. - - :note: - By default it is only allowed to checkout heads - everything else - will leave the HEAD detached which is allowed and possible, but remains - a special state that some tools might not be able to handle.""" - args = list() - kwargs['f'] = force - if kwargs['f'] == False: - kwargs.pop('f') - - self.repo.git.checkout(self, **kwargs) - return self.repo.active_branch - - #{ Configruation - - def _config_parser(self, read_only): - if read_only: - parser = self.repo.config_reader() - else: - parser = self.repo.config_writer() - # END handle parser instance - - return SectionConstraint(parser, 'branch "%s"' % self.name) - - def config_reader(self): - """ - :return: A configuration parser instance constrained to only read - this instance's values""" - return self._config_parser(read_only=True) - - def config_writer(self): - """ - :return: A configuration writer instance with read-and write acccess - to options of this head""" - return self._config_parser(read_only=False) - - #} END configuration - - diff --git a/git/refs/headref.py b/git/refs/headref.py new file mode 100644 index 00000000..67117e96 --- /dev/null +++ b/git/refs/headref.py @@ -0,0 +1,170 @@ +from reference import Reference +from git.config import SectionConstraint +from git.util import join_path + +__all__ = ["Head"] + +class Head(Reference): + """The GitPyhton Head implementation provides more git-command based features + + 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.hexsha + '1c09f116cbc2cb4100fb6935bb162daa4723f455'""" + __slots__ = tuple() + + _common_path_default = "refs/heads" + k_config_remote = "remote" + k_config_remote_ref = "merge" # branch to merge from remote + + # will be set by init method ! + RemoteReferenceCls = None + + #{ Configuration + + def set_tracking_branch(self, remote_reference): + """ + Configure this branch to track the given remote reference. This will alter + this branch's configuration accordingly. + + :param remote_reference: The remote reference to track or None to untrack + any references + :return: self""" + if remote_reference is not None and not isinstance(remote_reference, self.RemoteReferenceCls): + raise ValueError("Incorrect parameter type: %r" % remote_reference) + # END handle type + + writer = self.config_writer() + if remote_reference is None: + writer.remove_option(self.k_config_remote) + writer.remove_option(self.k_config_remote_ref) + if len(writer.options()) == 0: + writer.remove_section() + # END handle remove section + else: + writer.set_value(self.k_config_remote, remote_reference.remote_name) + writer.set_value(self.k_config_remote_ref, Head.to_full_path(remote_reference.remote_head)) + # END handle ref value + + return self + + def tracking_branch(self): + """ + :return: The remote_reference we are tracking, or None if we are + not a tracking branch""" + reader = self.config_reader() + if reader.has_option(self.k_config_remote) and reader.has_option(self.k_config_remote_ref): + ref = Head(self.repo, Head.to_full_path(reader.get_value(self.k_config_remote_ref))) + remote_refpath = self.RemoteReferenceCls.to_full_path(join_path(reader.get_value(self.k_config_remote), ref.name)) + return self.RemoteReferenceCls(self.repo, remote_refpath) + # END handle have tracking branch + + # we are not a tracking branch + return None + + + #{ Configruation + + def _config_parser(self, read_only): + if read_only: + parser = self.repo.config_reader() + else: + parser = self.repo.config_writer() + # END handle parser instance + + return SectionConstraint(parser, 'branch "%s"' % self.name) + + def config_reader(self): + """ + :return: A configuration parser instance constrained to only read + this instance's values""" + return self._config_parser(read_only=True) + + def config_writer(self): + """ + :return: A configuration writer instance with read-and write acccess + to options of this head""" + return self._config_parser(read_only=False) + + #} END configuration + + @classmethod + def delete(cls, repo, *heads, **kwargs): + """Delete the given heads + :param 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 + + :param new_path: + Either a simple name or a path, i.e. new_name or features/new_name. + The prefix refs/heads is implied + + :param force: + If True, the rename will succeed even if a head with the target name + already exists. + + :return: self + :note: respects the ref log as git commands are used""" + 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 + + def checkout(self, force=False, **kwargs): + """Checkout this head by setting the HEAD to this reference, by updating the index + to reflect the tree we point to and by updating the working tree to reflect + the latest index. + + The command will fail if changed working tree files would be overwritten. + + :param force: + If True, changes to the index and the working tree will be discarded. + If False, GitCommandError will be raised in that situation. + + :param kwargs: + Additional keyword arguments to be passed to git checkout, i.e. + b='new_branch' to create a new branch at the given spot. + + :return: + The active branch after the checkout operation, usually self unless + a new branch has been created. + + :note: + By default it is only allowed to checkout heads - everything else + will leave the HEAD detached which is allowed and possible, but remains + a special state that some tools might not be able to handle.""" + args = list() + kwargs['f'] = force + if kwargs['f'] == False: + kwargs.pop('f') + + self.repo.git.checkout(self, **kwargs) + return self.repo.active_branch + + + diff --git a/git/refs/log.py b/git/refs/log.py index f49c07fd..3b9d8514 100644 --- a/git/refs/log.py +++ b/git/refs/log.py @@ -5,12 +5,9 @@ from git.util import ( LockFile, assure_directory_exists, to_native_path, - ) - -from gitdb.util import ( bin_to_hex, join, - file_contents_ro_filepath, + file_contents_ro_filepath ) from git.objects.util import ( diff --git a/git/refs/reference.py b/git/refs/reference.py index 1a745ee9..5cff74bb 100644 --- a/git/refs/reference.py +++ b/git/refs/reference.py @@ -1,12 +1,10 @@ -from symbolic import SymbolicReference import os -from git.objects import Object -from git.util import ( - LazyMixin, - Iterable, - ) -from gitdb.util import ( +from symbolic import SymbolicReference +from head import HEAD +from git.util import ( + LazyMixin, + Iterable, isfile, hex_to_bin ) @@ -30,7 +28,7 @@ class Reference(SymbolicReference, LazyMixin, Iterable): Path relative to the .git/ directory pointing to the ref in question, i.e. refs/heads/master""" if not path.startswith(self._common_path_default+'/'): - raise ValueError("Cannot instantiate %r from path %s" % ( self.__class__.__name__, path )) + raise ValueError("Cannot instantiate %r from path %s, maybe use %s.to_full_path(name) to safely generate a valid full path from a name" % ( self.__class__.__name__, path, type(self).__name__)) super(Reference, self).__init__(repo, path) @@ -40,8 +38,8 @@ class Reference(SymbolicReference, LazyMixin, Iterable): def set_object(self, object, logmsg = None): """Special version which checks if the head-log needs an update as well""" oldbinsha = None + head = HEAD(self.repo) if logmsg is not None: - head = self.repo.head if not head.is_detached and head.ref == self: oldbinsha = self.commit.binsha #END handle commit retrieval @@ -62,7 +60,7 @@ class Reference(SymbolicReference, LazyMixin, Iterable): # * check with HEAD only which should cover 99% of all usage # * scenarios (even 100% of the default ones). # */ - self.repo.head.log_append(oldbinsha, logmsg) + head.log_append(oldbinsha, logmsg) #END check if the head # NOTE: Don't have to overwrite properties as the will only work without a the log diff --git a/git/refs/remote.py b/git/refs/remote.py index b7b07d4b..f2dc72ee 100644 --- a/git/refs/remote.py +++ b/git/refs/remote.py @@ -1,15 +1,17 @@ -from head import Head -from git.util import join_path -from gitdb.util import join - import os - +from headref import Head +from git.util import ( + join, + join_path + ) __all__ = ["RemoteReference"] class RemoteReference(Head): """Represents a reference pointing to a remote head.""" + __slots__ = tuple() + _common_path_default = "refs/remotes" @@ -41,6 +43,11 @@ class RemoteReference(Head): return '/'.join(tokens[3:]) @classmethod + def create(cls, *args, **kwargs): + """Used to disable this method""" + raise TypeError("Cannot explicitly create remote references") + + @classmethod def delete(cls, repo, *refs, **kwargs): """Delete the given remote references. :note: @@ -56,8 +63,3 @@ class RemoteReference(Head): except OSError: pass # END for each ref - - @classmethod - def create(cls, *args, **kwargs): - """Used to disable this method""" - raise TypeError("Cannot explicitly create remote references") diff --git a/git/refs/symbolic.py b/git/refs/symbolic.py index aec68750..ddee3809 100644 --- a/git/refs/symbolic.py +++ b/git/refs/symbolic.py @@ -1,24 +1,26 @@ import os -from git.objects import Object, Commit +import re + +from git.objects import ( + Object, + Commit + ) from git.util import ( join_path, join_path_native, to_native_path_linux, - assure_directory_exists + assure_directory_exists, + join, + dirname, + isdir, + exists, + isfile, + rename, + hex_to_bin, + LockedFD ) -from gitdb.exc import BadObject -from gitdb.util import ( - join, - dirname, - isdir, - exists, - isfile, - rename, - hex_to_bin, - LockedFD - ) - +from git.exc import BadObject from log import RefLog __all__ = ["SymbolicReference"] @@ -30,11 +32,27 @@ class SymbolicReference(object): A typical example for a symbolic reference is HEAD.""" __slots__ = ("repo", "path") + _resolve_ref_on_create = False _points_to_commits_only = True _common_path_default = "" _id_attribute_ = "name" + re_hexsha_only = re.compile('^[0-9A-Fa-f]{40}$') + + #{ Configuration + # Object class to be used when instantiating objects + ObjectCls = Object + CommitCls = Commit + + # all of the following are set by the package initializer + HEADCls = None + HeadCls = None + RemoteReferenceCls = None + TagReferenceCls = None + ReferenceCls = None + #}END configuration + def __init__(self, repo, path): self.repo = repo self.path = path @@ -143,20 +161,53 @@ class SymbolicReference(object): return (None, tokens[1]) # its a commit - if repo.re_hexsha_only.match(tokens[0]): + if cls.re_hexsha_only.match(tokens[0]): return (tokens[0], None) raise ValueError("Failed to parse reference information from %r" % ref_path) - def _get_object(self): + def _get_object_sha(self): """ :return: - The object our ref currently refers to. Refs can be cached, they will + The binary sha to the object our ref currently refers to. Refs can be cached, they will always point to the actual object as it gets re-created on each query""" + return hex_to_bin(self.dereference_recursive(self.repo, self.path)) + + def _get_object(self): + """ + :return: + The object our ref currently refers to.""" # 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_from_sha(self.repo, hex_to_bin(self.dereference_recursive(self.repo, self.path))) + return self.ObjectCls.new_from_sha(self.repo, self._get_object_sha()) + def set_object(self, object_id, logmsg = None): + """Set the object we point to, possibly dereference our symbolic reference first. + If the reference does not exist, it will be created + + :param object: a reference specifier string, a SymbolicReference or an object hex sha. + SymbolicReferences will be dereferenced beforehand to obtain the object they point to + :param logmsg: If not None, the message will be used in the reflog entry to be + written. Otherwise the reflog is not altered + :note: plain SymbolicReferences may not actually point to objects by convention + :return: self""" + if isinstance(object_id, SymbolicReference): + object = object.object + #END resolve references + + is_detached = True + try: + is_detached = self.is_detached + except ValueError: + pass + # END handle non-existing ones + + if is_detached: + return self.set_reference(object_id, logmsg) + + # set the commit on our reference + return self._get_reference().set_object(object_id, logmsg) + def _get_commit(self): """ :return: @@ -167,7 +218,7 @@ class SymbolicReference(object): obj = obj.object #END dereference tag - if obj.type != Commit.type: + if obj.type != self.CommitCls.type: raise TypeError("Symbolic Reference pointed to object %r, commit was required" % obj) #END handle type return obj @@ -179,20 +230,20 @@ class SymbolicReference(object): a commit :return: self""" # check the type - assume the best if it is a base-string - invalid_type = False - if isinstance(commit, Object): - invalid_type = commit.type != Commit.type + is_invalid_type = False + if isinstance(commit, self.ObjectCls): + is_invalid_type = commit.type != self.CommitCls.type elif isinstance(commit, SymbolicReference): - invalid_type = commit.object.type != Commit.type + is_invalid_type = commit.object.type != self.CommitCls.type else: try: - invalid_type = self.repo.rev_parse(commit).type != Commit.type + is_invalid_type = self.repo.resolve_object(commit).type != self.CommitCls.type except BadObject: raise ValueError("Invalid object: %s" % commit) #END handle exception # END verify type - if invalid_type: + if is_invalid_type: raise ValueError("Need commit, got %r" % commit) #END handle raise @@ -202,35 +253,9 @@ class SymbolicReference(object): return self - def set_object(self, object, logmsg = None): - """Set the object we point to, possibly dereference our symbolic reference first. - If the reference does not exist, it will be created - - :param object: a refspec, a SymbolicReference or an Object instance. SymbolicReferences - will be dereferenced beforehand to obtain the object they point to - :param logmsg: If not None, the message will be used in the reflog entry to be - written. Otherwise the reflog is not altered - :note: plain SymbolicReferences may not actually point to objects by convention - :return: self""" - if isinstance(object, SymbolicReference): - object = object.object - #END resolve references - - is_detached = True - try: - is_detached = self.is_detached - except ValueError: - pass - # END handle non-existing ones - - if is_detached: - return self.set_reference(object, logmsg) - - # set the commit on our reference - return self._get_reference().set_object(object, logmsg) - commit = property(_get_commit, set_commit, doc="Query or set commits directly") object = property(_get_object, set_object, doc="Return the object our ref currently refers to") + object_binsha = property(_get_object_sha, set_object, doc="Return the object our ref currently refers to") def _get_reference(self): """:return: Reference Object we point to @@ -247,7 +272,7 @@ class SymbolicReference(object): will be set which effectively detaches the refererence if it was a purely symbolic one. - :param ref: SymbolicReference instance, Object instance or refspec string + :param ref: SymbolicReference instance, hexadecimal sha string or refspec string Only if the ref is a SymbolicRef instance, we will point to it. Everthiny else is dereferenced to obtain the actual object. :param logmsg: If set to a string, the message will be used in the reflog. @@ -263,12 +288,12 @@ class SymbolicReference(object): obj = None if isinstance(ref, SymbolicReference): write_value = "ref: %s" % ref.path - elif isinstance(ref, Object): + elif isinstance(ref, self.ObjectCls): obj = ref write_value = ref.hexsha elif isinstance(ref, basestring): try: - obj = self.repo.rev_parse(ref+"^{}") # optionally deref tags + obj = self.repo.resolve_object(ref+"^{}") # optionally deref tags write_value = obj.hexsha except BadObject: raise ValueError("Could not extract object from %s" % ref) @@ -318,7 +343,7 @@ class SymbolicReference(object): a valid object or reference.""" try: self.object - except (OSError, ValueError): + except (OSError, ValueError, BadObject): return False else: return True @@ -449,7 +474,16 @@ class SymbolicReference(object): # figure out target data target = reference if resolve: - target = repo.rev_parse(str(reference)) + # could just use the resolve method, but it could be expensive + # so we handle most common cases ourselves + if isinstance(reference, cls.ObjectCls): + target = reference.hexsha + elif isinstance(reference, SymbolicReference): + target = reference.object.hexsha + else: + target = repo.resolve_object(str(reference)) + #END handle resoltion + #END need resolution if not force and isfile(abs_ref_path): target_data = str(target) @@ -579,7 +613,7 @@ class SymbolicReference(object): def iter_items(cls, repo, common_path = None): """Find all refs in the repository - :param repo: is the Repo + :param repo: is the repo :param common_path: Optional keyword argument to the path which is to be shared by all @@ -588,12 +622,12 @@ class SymbolicReference(object): refs suitable for the actual class are returned. :return: - git.SymbolicReference[], each of them is guaranteed to be a symbolic - ref which is not detached. + git.SymbolicReference[], each of them is guaranteed to be a *only* a symbolic + ref, or a derived class which is not detached List is lexigraphically sorted The returned objects represent actual subclasses, such as Head or TagReference""" - return ( r for r in cls._iter_items(repo, common_path) if r.__class__ == SymbolicReference or not r.is_detached ) + return ( r for r in cls._iter_items(repo, common_path) if r.__class__ == cls or not r.is_detached ) @classmethod def from_path(cls, repo, path): @@ -606,7 +640,7 @@ class SymbolicReference(object): if not path: raise ValueError("Cannot create Reference from %r" % path) - for ref_type in (HEAD, Head, RemoteReference, TagReference, Reference, SymbolicReference): + for ref_type in (cls.HEADCls, cls.HeadCls, cls.RemoteReferenceCls, cls.TagReferenceCls, cls.ReferenceCls, cls): try: instance = ref_type(repo, path) if instance.__class__ == SymbolicReference and instance.is_detached: diff --git a/git/refs/tag.py b/git/refs/tag.py index c09d814d..3a1433be 100644 --- a/git/refs/tag.py +++ b/git/refs/tag.py @@ -2,8 +2,6 @@ from reference import Reference __all__ = ["TagReference", "Tag"] - - class TagReference(Reference): """Class representing a lightweight tag reference which either points to a commit ,a tag object or any other object. In the latter case additional information, @@ -16,7 +14,6 @@ class TagReference(Reference): print tagref.commit.message if tagref.tag is not None: print tagref.tag.message""" - __slots__ = tuple() _common_path_default = "refs/tags" @@ -45,7 +42,7 @@ class TagReference(Reference): # make object read-only # It should be reasonably hard to adjust an existing tag object = property(Reference._get_object) - + @classmethod def create(cls, repo, path, ref='HEAD', message=None, force=False, **kwargs): """Create a new tag reference. @@ -85,7 +82,5 @@ class TagReference(Reference): """Delete the given existing tag or tags""" repo.git.tag("-d", *tags) - - # provide an alias Tag = TagReference |
