summaryrefslogtreecommitdiff
path: root/lib/git/refs.py
diff options
context:
space:
mode:
Diffstat (limited to 'lib/git/refs.py')
-rw-r--r--lib/git/refs.py613
1 files changed, 530 insertions, 83 deletions
diff --git a/lib/git/refs.py b/lib/git/refs.py
index a4d7bbb1..5b94ea07 100644
--- a/lib/git/refs.py
+++ b/lib/git/refs.py
@@ -3,20 +3,23 @@
#
# This module is part of GitPython and is released under
# the BSD License: http://www.opensource.org/licenses/bsd-license.php
-"""
-Module containing all ref based objects
-"""
-from objects.base import Object
+""" Module containing all ref based objects """
+
+import os
+from objects import Object, Commit
from objects.utils import get_object_type_by_name
from utils import LazyMixin, Iterable
class Reference(LazyMixin, Iterable):
"""
- Represents a named reference to any object
+ Represents a named reference to any object. Subclasses may apply restrictions though,
+ i.e. Heads can only point to commits.
"""
__slots__ = ("repo", "path")
+ _common_path_default = "refs"
+ _id_attribute_ = "name"
- def __init__(self, repo, path, object = None):
+ def __init__(self, repo, path):
"""
Initialize this instance
``repo``
@@ -26,13 +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 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
@@ -63,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
@@ -72,10 +73,44 @@ 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(self.repo, self.path)
+ return Object.new(self.repo, self.path)
+
+ def _set_object(self, ref):
+ """
+ Set our reference to point to the given ref. It will be converted
+ to a specific hexsha.
+
+ Note:
+ TypeChecking is done by the git command
+ """
+ # do it safely by specifying the old value
+ self.repo.git.update_ref(self.path, ref, self._get_object().sha)
+
+ 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)
+
+ def _get_commit(self):
+ """
+ Returns
+ 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, but to %r" % (self, commit))
+ return commit
+
+ commit = property(_get_commit, _set_commit, doc="Return Commit object the reference points to")
@classmethod
- def iter_items(cls, repo, common_path = "refs", **kwargs):
+ def iter_items(cls, repo, common_path = None, **kwargs):
"""
Find all refs in the repository
@@ -84,59 +119,303 @@ class Reference(LazyMixin, Iterable):
``common_path``
Optional keyword argument to the path which is to be shared by all
- returned Ref objects
-
- ``kwargs``
- Additional options given as keyword arguments, will be passed
- to git-for-each-ref
+ returned Ref objects.
+ Defaults to class specific portion if None assuring that only
+ refs suitable for the actual class are returned.
Returns
- git.Ref[]
+ git.Reference[]
- List is sorted by committerdate
- The returned objects are compatible to the Ref base, but represent the
- actual type, such as Head or Tag
+ List is lexigraphically sorted
+ The returned objects represent actual subclasses, such as Head or TagReference
"""
-
- options = {'sort': "committerdate",
- 'format': "%(refname)%00%(objectname)%00%(objecttype)%00%(objectsize)"}
-
- options.update(kwargs)
-
- output = repo.git.for_each_ref(common_path, **options)
- return cls._iter_from_stream(repo, iter(output.splitlines()))
-
+ if common_path is None:
+ common_path = cls._common_path_default
+
+ rela_paths = set()
+
+ # walk loose refs
+ # Currently we do not follow links
+ for root, dirs, files in os.walk(os.path.join(repo.path, common_path)):
+ for f in files:
+ abs_path = os.path.join(root, f)
+ rela_paths.add(abs_path.replace(repo.path + '/', ""))
+ # END for each file in root directory
+ # END for each directory to walk
+
+ # read packed refs
+ packed_refs_path = os.path.join(repo.path, 'packed-refs')
+ if os.path.isfile(packed_refs_path):
+ fp = open(packed_refs_path, 'r')
+ try:
+ for line in fp.readlines():
+ if line.startswith('#'):
+ continue
+ # 439689865b9c6e2a0dad61db22a0c9855bacf597 refs/heads/hello
+ line = line.rstrip()
+ first_space = line.find(' ')
+ if first_space == -1:
+ continue
+
+ rela_path = line[first_space+1:]
+ if rela_path.startswith(common_path):
+ rela_paths.add(rela_path)
+ # END relative path matches common path
+ # END for each line in packed-refs
+ finally:
+ fp.close()
+ # END packed refs reading
+
+ # return paths in sorted order
+ for path in sorted(rela_paths):
+ if path.endswith('/HEAD'):
+ continue
+ # END skip remote heads
+ yield cls.from_path(repo, path)
+ # END for each sorted relative refpath
+
+
@classmethod
- def _iter_from_stream(cls, repo, stream):
- """ Parse out ref information into a list of Ref compatible objects
- Returns git.Ref[] list of Ref objects """
- heads = []
-
- for line in stream:
- heads.append(cls._from_string(repo, line))
-
- return heads
+ def from_path(cls, repo, path):
+ """
+ Return
+ Instance of type Reference, Head, or Tag
+ depending on the given path
+ """
+ if not path:
+ raise ValueError("Cannot create Reference from %r" % 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)
+
+class SymbolicReference(object):
+ """
+ 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.
+ """
+ __slots__ = ("repo", "name")
+
+ def __init__(self, repo, name):
+ if '/' in name:
+ # NOTE: Actually they can be looking like ordinary refs. Theoretically we handle this
+ # case incorrectly
+ raise ValueError("SymbolicReferences are not located within a directory, got %s" % name)
+ # END error handling
+ 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)
+
+ def _get_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 self.repo.re_hexsha_only.match(tokens[0]):
+ 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
+ if tokens[0] != "ref:":
+ raise ValueError("Failed to parse symbolic refernce: wanted 'ref: <hexsha>', got %r" % value)
+ return Head(self.repo, tokens[1]).commit
+
+ def _set_commit(self, commit):
+ """
+ Set our commit, possibly dereference our symbolic reference first.
+ """
+ if self.is_detached:
+ return self._set_reference(commit)
+
+ # set the commit on our reference
+ self._get_reference().commit = commit
+
+ commit = property(_get_commit, _set_commit, doc="Query or set commits directly")
+
+ def _get_reference(self):
+ """
+ Returns
+ Reference Object we point to
+ """
+ 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" % (self, tokens[0]))
+ return Reference.from_path(self.repo, tokens[1])
+ finally:
+ fp.close()
+
+ 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.sha
+ else:
+ try:
+ write_value = ref.commit.sha
+ 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.sha
+ except Exception:
+ raise ValueError("Could not extract object from %s" % ref)
+ # END end try string
+ # END try commit attribute
+
+ # if we are writing a ref, use symbolic ref to get the reflog and more
+ # checking
+ # Otherwise we detach it and have to do it manually
+ if write_value.startswith('ref:'):
+ self.repo.git.symbolic_ref(self.name, write_value[5:])
+ return
+ # END non-detached handling
+
+ 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
+
@classmethod
- def _from_string(cls, repo, line):
- """ Create a new Ref instance from the given string.
- Format
- name: [a-zA-Z_/]+
- <null byte>
- id: [0-9A-Fa-f]{40}
- Returns git.Head """
- full_path, hexsha, type_name, object_size = line.split("\x00")
-
- # No, we keep the object dynamic by allowing it to be retrieved by
- # our path on demand - due to perstent commands it is fast.
- # This reduces the risk that the object does not match
- # the changed ref anymore in case it changes in the meanwhile
- return cls(repo, full_path)
-
- # obj = get_object_type_by_name(type_name)(repo, hexsha)
- # obj.size = object_size
- # return cls(repo, full_path, obj)
+ def from_path(cls, repo, path):
+ """
+ Return
+ Instance of SymbolicReference or HEAD
+ depending on the given path
+ """
+ if not path:
+ raise ValueError("Cannot create Symbolic Reference from %r" % path)
+
+ if path == 'HEAD':
+ return HEAD(repo, path)
+
+ if '/' not in path:
+ return SymbolicReference(repo, path)
+
+ raise ValueError("Could not find symbolic reference type suitable to handle path %r" % path)
+
+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 our HEAD to the given commit optionally synchronizing
+ the index and working tree. The reference we refer to will be set to
+ commit as well.
+
+ ``commit``
+ 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
+ it will not be touched.
+ ``working_tree``
+ If True, the working tree will be forcefully adjusted to match the given
+ commit, possibly overwriting uncommitted changes without warning.
+ If working_tree is True, index must be true as well
+
+ ``paths``
+ Single path or list of paths relative to the git root directory
+ that are to be reset. This allow to partially reset individual files.
+
+ ``kwargs``
+ Additional arguments passed to git-reset.
+
+ Returns
+ self
+ """
+ mode = "--soft"
+ if index:
+ mode = "--mixed"
+
+ if working_tree:
+ mode = "--hard"
+ if not index:
+ raise ValueError( "Cannot reset the working tree if the index is not reset as well")
+ # END working tree handling
+
+ self.repo.git.reset(mode, commit, paths, **kwargs)
+
+ return self
+
class Head(Reference):
"""
@@ -154,49 +433,141 @@ class Head(Reference):
>>> head.commit
<git.Commit "1c09f116cbc2cb4100fb6935bb162daa4723f455">
- >>> head.commit.id
+ >>> head.commit.sha
'1c09f116cbc2cb4100fb6935bb162daa4723f455'
"""
-
- @property
- def commit(self):
+ _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
- Commit object the head points to
+ Newly created Head
+
+ Note
+ This does not alter the current HEAD, index or Working Tree
"""
- return self.object
+ 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 iter_items(cls, repo, common_path = "refs/heads", **kwargs):
+ 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
- Iterator yielding Head items
+ self
+ """
+ flag = "-m"
+ if force:
+ flag = "-M"
- For more documentation, please refer to git.base.Ref.list_items
+ 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):
"""
- return super(Head,cls).iter_items(repo, common_path, **kwargs)
-
- def __repr__(self):
- return '<git.Head "%s">' % self.name
+ 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.
+ ``force``
+ If True, changes to the index and the working tree will be discarded.
+ If False, GitCommandError will be raised in that situation.
+
+ ``**kwargs``
+ Additional keyword arguments to be passed to git checkout, i.e.
+ b='new_branch' to create a new branch at the given spot.
+
+ Returns
+ 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
-class TagRef(Reference):
+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::
- 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
"""
__slots__ = tuple()
+ _common_path_default = "refs/tags"
@property
def commit(self):
@@ -222,17 +593,93 @@ class TagRef(Reference):
if self.object.type == "tag":
return self.object
return None
-
+
@classmethod
- def iter_items(cls, repo, common_path = "refs/tags", **kwargs):
+ def create(cls, repo, path, ref='HEAD', message=None, force=False, **kwargs):
"""
- Returns
- Iterator yielding commit items
+ Create a new tag reference.
+
+ ``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.
- For more documentation, please refer to git.base.Ref.list_items
+ ``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
"""
- return super(TagRef,cls).iter_items(repo, common_path, **kwargs)
+ 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
-Tag = TagRef
+Tag = TagReference
+
+class RemoteReference(Head):
+ """
+ Represents a reference pointing to a remote head.
+ """
+ _common_path_default = "refs/remotes"
+
+ @property
+ def remote_name(self):
+ """
+ Returns
+ Name of the remote we are a reference of, such as 'origin' for a reference
+ named 'origin/master'
+ """
+ tokens = self.path.split('/')
+ # /refs/remotes/<remote name>/<branch_name>
+ return tokens[2]
+
+ @property
+ def remote_head(self):
+ """
+ Returns
+ Name of the remote head itself, i.e. master.
+
+ NOTE: The returned name is usually not qualified enough to uniquely identify
+ a branch
+ """
+ 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)