From dec4663129f72321a14efd6de63f14a7419e3ed2 Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Tue, 23 Nov 2010 09:14:17 +0100 Subject: Split ref implementation up into multiple files, to make room for the log implementation --- refs/__init__.py | 20 +++ refs/head.py | 295 +++++++++++++++++++++++++++++++ refs/reference.py | 111 ++++++++++++ refs/remote.py | 57 ++++++ refs/symbolic.py | 512 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ refs/tag.py | 91 ++++++++++ 6 files changed, 1086 insertions(+) create mode 100644 refs/__init__.py create mode 100644 refs/head.py create mode 100644 refs/reference.py create mode 100644 refs/remote.py create mode 100644 refs/symbolic.py create mode 100644 refs/tag.py (limited to 'refs') diff --git a/refs/__init__.py b/refs/__init__.py new file mode 100644 index 00000000..ca5ace02 --- /dev/null +++ b/refs/__init__.py @@ -0,0 +1,20 @@ + +# import all modules in order, fix the names they require +from symbolic import * +from reference import * +from head import * +from tag import * +from remote import * + +# name fixes +import head +head.RemoteReference = RemoteReference +del(head) + + +import symbolic +for item in (HEAD, Head, RemoteReference, TagReference, Reference, SymbolicReference): + setattr(symbolic, item.__name__, item) +del(symbolic) +# git.objects.Commit -> symbolic +# git.config.SectionConstraint -> head diff --git a/refs/head.py b/refs/head.py new file mode 100644 index 00000000..f8625bad --- /dev/null +++ b/refs/head.py @@ -0,0 +1,295 @@ +""" Module containing all ref based objects """ +from symbolic import SymbolicReference +from reference import Reference + +from git.config import SectionConstraint + +from git.exc import GitCommandError + +__all__ = ["HEAD", "Head"] + + + +class HEAD(SymbolicReference): + """Special case of a Symbolic Reference as it represents the repository's + HEAD reference.""" + _HEAD_NAME = 'HEAD' + _ORIG_HEAD_NAME = 'ORIG_HEAD' + __slots__ = tuple() + + def __init__(self, repo, path=_HEAD_NAME): + if path != self._HEAD_NAME: + raise ValueError("HEAD instance must point to %r, got %r" % (self._HEAD_NAME, path)) + super(HEAD, self).__init__(repo, path) + + def orig_head(self): + """ + :return: SymbolicReference pointing at the ORIG_HEAD, which is maintained + to contain the previous value of HEAD""" + return SymbolicReference(self.repo, self._ORIG_HEAD_NAME) + + def _set_reference(self, ref): + """If someone changes the reference through us, we must manually update + the ORIG_HEAD if we are detached. The underlying implementation can only + handle un-detached heads as it has to check whether the current head + is the checked-out one""" + if self.is_detached: + prev_commit = self.commit + super(HEAD, self)._set_reference(ref) + SymbolicReference.create(self.repo, self._ORIG_HEAD_NAME, prev_commit, force=True) + else: + super(HEAD, self)._set_reference(ref) + # END handle detached mode + + # aliased reference + reference = property(SymbolicReference._get_reference, _set_reference, doc="Returns the Reference we point to") + ref = reference + + 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. + + :param commit: + Commit object, Reference Object or string identifying a revision we + should reset HEAD to. + + :param index: + If True, the index will be set to match the given commit. Otherwise + it will not be touched. + + :param 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 + + :param paths: + Single path or list of paths relative to the git root directory + that are to be reset. This allows to partially reset individual files. + + :param kwargs: + Additional arguments passed to git-reset. + + :return: self""" + mode = "--soft" + add_arg = None + if index: + mode = "--mixed" + + # it appears, some git-versions declare mixed and paths deprecated + # see http://github.com/Byron/GitPython/issues#issue/2 + if paths: + mode = None + # END special case + # END handle index + + 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 + + if paths: + add_arg = "--" + # END nicely separate paths from rest + + try: + self.repo.git.reset(mode, commit, add_arg, paths, **kwargs) + except GitCommandError, e: + # git nowadays may use 1 as status to indicate there are still unstaged + # modifications after the reset + if e.status != 1: + raise + # END handle exception + + 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 + + + >>> 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 create(cls, repo, path, commit='HEAD', force=False, **kwargs): + """Create a new head. + :param repo: Repository to create the head in + :param path: + The name or path of the head, i.e. 'new_branch' or + feature/feature1. The prefix refs/heads is implied. + + :param commit: + Commit to which the new head should point, defaults to the + current HEAD + + :param force: + if True, force creation even if branch with that name already exists. + + :param kwargs: + Additional keyword arguments to be passed to git-branch, i.e. + track, no-track, l + + :return: 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 + :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/refs/reference.py b/refs/reference.py new file mode 100644 index 00000000..1f97b92e --- /dev/null +++ b/refs/reference.py @@ -0,0 +1,111 @@ +from symbolic import SymbolicReference +import os +from git.util import ( + LazyMixin, + Iterable, + ) + +from gitdb.util import ( + isfile, + hex_to_bin + ) + +__all__ = ["Reference"] + + +class Reference(SymbolicReference, LazyMixin, Iterable): + """Represents a named reference to any object. Subclasses may apply restrictions though, + i.e. Heads can only point to commits.""" + __slots__ = tuple() + _common_path_default = "refs" + + def __init__(self, repo, path): + """Initialize this instance + :param repo: Our parent repository + + :param path: + 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 )) + super(Reference, self).__init__(repo, path) + + + def __str__(self): + return self.name + + def _get_object(self): + """ + :return: + 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""" + # 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))) + + def _set_object(self, ref): + """ + Set our reference to point to the given ref. It will be converted + to a specific hexsha. + If the reference does not exist, it will be created. + + :note: + TypeChecking is done by the git command""" + abs_path = self._abs_path() + existed = True + if not isfile(abs_path): + existed = False + open(abs_path, 'wb').write(Object.NULL_HEX_SHA) + # END quick create + + # do it safely by specifying the old value + try: + self.repo.git.update_ref(self.path, ref, (existed and self._get_object().hexsha) or None) + except: + if not existed: + os.remove(abs_path) + # END remove file on error if it didn't exist before + raise + # END exception handling + + object = property(_get_object, _set_object, doc="Return the object our ref currently refers to") + + @property + def name(self): + """:return: (shortest) Name of this reference - it may contain path components""" + # first two path tokens are can be removed as they are + # refs/heads or refs/tags or refs/remotes + tokens = self.path.split('/') + if len(tokens) < 3: + return self.path # could be refs/HEAD + return '/'.join(tokens[2:]) + + + @classmethod + def create(cls, repo, path, commit='HEAD', force=False ): + """Create a new reference. + + :param repo: Repository to create the reference in + :param path: + The relative path of the reference, i.e. 'new_branch' or + feature/feature1. The path prefix 'refs/' is implied if not + given explicitly + + :param commit: + Commit to which the new reference should point, defaults to the + current HEAD + + :param force: + if True, force creation even if a reference with that name already exists. + Raise OSError otherwise + + :return: Newly created Reference + + :note: This does not alter the current HEAD, index or Working Tree""" + return cls._create(repo, path, True, commit, force) + + @classmethod + def iter_items(cls, repo, common_path = None): + """Equivalent to SymbolicReference.iter_items, but will return non-detached + references as well.""" + return cls._iter_items(repo, common_path) diff --git a/refs/remote.py b/refs/remote.py new file mode 100644 index 00000000..7ea9eb46 --- /dev/null +++ b/refs/remote.py @@ -0,0 +1,57 @@ +from head import Head +from git.util import join_path + +import os + + +__all__ = ["RemoteReference"] + + +class RemoteReference(Head): + """Represents a reference pointing to a remote head.""" + _common_path_default = "refs/remotes" + + + @classmethod + def iter_items(cls, repo, common_path = None, remote=None): + """Iterate remote references, and if given, constrain them to the given remote""" + common_path = common_path or cls._common_path_default + if remote is not None: + common_path = join_path(common_path, str(remote)) + # END handle remote constraint + return super(RemoteReference, cls).iter_items(repo, common_path) + + @property + def remote_name(self): + """ + :return: + Name of the remote we are a reference of, such as 'origin' for a reference + named 'origin/master'""" + tokens = self.path.split('/') + # /refs/remotes// + return tokens[2] + + @property + def remote_head(self): + """:return: 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, *refs, **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", *refs) + # the official deletion method will ignore remote symbolic refs - these + # are generally ignored in the refs/ folder. We don't though + # and delete remainders manually + for ref in refs: + try: + os.remove(join(repo.git_dir, ref.path)) + except OSError: + pass + # END for each ref diff --git a/refs/symbolic.py b/refs/symbolic.py new file mode 100644 index 00000000..3329d51c --- /dev/null +++ b/refs/symbolic.py @@ -0,0 +1,512 @@ +import os +from git.objects import Commit +from git.util import ( + join_path, + join_path_native, + to_native_path_linux + ) + +from gitdb.util import ( + join, + dirname, + isdir, + exists, + isfile, + rename, + hex_to_bin + ) + +__all__ = ["SymbolicReference"] + +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", "path") + _common_path_default = "" + _id_attribute_ = "name" + + def __init__(self, repo, path): + self.repo = repo + self.path = path + + def __str__(self): + return self.path + + def __repr__(self): + return '' % (self.__class__.__name__, self.path) + + def __eq__(self, other): + return self.path == other.path + + def __ne__(self, other): + return not ( self == other ) + + def __hash__(self): + return hash(self.path) + + @property + def name(self): + """ + :return: + In case of symbolic references, the shortest assumable name + is the path itself.""" + return self.path + + def _abs_path(self): + return join_path_native(self.repo.git_dir, self.path) + + @classmethod + def _get_packed_refs_path(cls, repo): + return join(repo.git_dir, 'packed-refs') + + @classmethod + def _iter_packed_refs(cls, repo): + """Returns an iterator yielding pairs of sha1/path pairs for the corresponding refs. + :note: The packed refs file will be kept open as long as we iterate""" + try: + fp = open(cls._get_packed_refs_path(repo), 'r') + for line in fp: + line = line.strip() + if not line: + continue + if line.startswith('#'): + if line.startswith('# pack-refs with:') and not line.endswith('peeled'): + raise TypeError("PackingType of packed-Refs not understood: %r" % line) + # END abort if we do not understand the packing scheme + continue + # END parse comment + + # skip dereferenced tag object entries - previous line was actual + # tag reference for it + if line[0] == '^': + continue + + yield tuple(line.split(' ', 1)) + # END for each line + except (OSError,IOError): + raise StopIteration + # END no packed-refs file handling + # NOTE: Had try-finally block around here to close the fp, + # but some python version woudn't allow yields within that. + # I believe files are closing themselves on destruction, so it is + # alright. + + @classmethod + def dereference_recursive(cls, repo, ref_path): + """ + :return: hexsha stored in the reference at the given ref_path, recursively dereferencing all + intermediate references as required + :param repo: the repository containing the reference at ref_path""" + while True: + ref = cls(repo, ref_path) + hexsha, ref_path = ref._get_ref_info() + if hexsha is not None: + return hexsha + # END recursive dereferencing + + def _get_ref_info(self): + """Return: (sha, target_ref_path) if available, the sha the file at + rela_path points to, or None. target_ref_path is the reference we + point to, or None""" + tokens = None + try: + fp = open(self._abs_path(), 'r') + value = fp.read().rstrip() + fp.close() + tokens = value.split(" ") + except (OSError,IOError): + # Probably we are just packed, find our entry in the packed refs file + # NOTE: We are not a symbolic ref if we are in a packed file, as these + # are excluded explictly + for sha, path in self._iter_packed_refs(self.repo): + if path != self.path: continue + tokens = (sha, path) + break + # END for each packed ref + # END handle packed refs + + if tokens is None: + raise ValueError("Reference at %r does not exist" % self.path) + + # is it a reference ? + if tokens[0] == 'ref:': + return (None, tokens[1]) + + # its a commit + if self.repo.re_hexsha_only.match(tokens[0]): + return (tokens[0], None) + + raise ValueError("Failed to parse reference information from %r" % self.path) + + def _get_commit(self): + """ + :return: + Commit object we point to, works for detached and non-detached + SymbolicReferences""" + # we partially reimplement it to prevent unnecessary file access + hexsha, target_ref_path = self._get_ref_info() + + # it is a detached reference + if hexsha: + return Commit(self.repo, hex_to_bin(hexsha)) + + return self.from_path(self.repo, target_ref_path).commit + + def _set_commit(self, commit): + """Set our commit, possibly dereference our symbolic reference first. + If the reference does not exist, it will be created""" + is_detached = True + try: + is_detached = self.is_detached + except ValueError: + pass + # END handle non-existing ones + if 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): + """:return: Reference Object we point to""" + sha, target_ref_path = self._get_ref_info() + if target_ref_path is None: + raise TypeError("%s is a detached symbolic reference as it points to %r" % (self, sha)) + return self.from_path(self.repo, target_ref_path) + + def _set_reference(self, ref): + """Set ourselves to the given ref. It will stay a symbol if the ref is a Reference. + 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, SymbolicReference): + write_value = "ref: %s" % ref.path + elif isinstance(ref, Commit): + write_value = ref.hexsha + else: + try: + write_value = ref.commit.hexsha + except AttributeError: + try: + obj = self.repo.rev_parse(ref+"^{}") # optionally deref tags + if obj.type != "commit": + raise TypeError("Invalid object type behind sha: %s" % sha) + write_value = obj.hexsha + except Exception: + raise ValueError("Could not extract object from %s" % ref) + # END end try string + # END try commit attribute + + # maintain the orig-head if we are currently checked-out + head = HEAD(self.repo) + try: + if head.ref == self: + try: + # TODO: implement this atomically, if we fail below, orig_head is at an incorrect spot + # Enforce the creation of ORIG_HEAD + SymbolicReference.create(self.repo, head.orig_head().name, self.commit, force=True) + except ValueError: + pass + #END exception handling + # END if we are checked-out + except TypeError: + pass + # END handle detached heads + + # 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. Besides, this works + # recursively automaitcally, but should be replaced with a python implementation + # soon + if write_value.startswith('ref:'): + self.repo.git.symbolic_ref(self.path, write_value[5:]) + return + # END non-detached handling + + path = self._abs_path() + directory = dirname(path) + if not isdir(directory): + os.makedirs(directory) + + fp = open(path, "wb") + try: + fp.write(write_value) + finally: + fp.close() + # END writing + + + # aliased reference + reference = property(_get_reference, _set_reference, doc="Returns the Reference we point to") + ref = reference + + def is_valid(self): + """ + :return: + True if the reference is valid, hence it can be read and points to + a valid object or reference.""" + try: + self.commit + except (OSError, ValueError): + return False + else: + return True + + @property + def is_detached(self): + """ + :return: + 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 to_full_path(cls, path): + """ + :return: string with a full repository-relative path which can be used to initialize + a Reference instance, for instance by using ``Reference.from_path``""" + if isinstance(path, SymbolicReference): + path = path.path + full_ref_path = path + if not cls._common_path_default: + return full_ref_path + if not path.startswith(cls._common_path_default+"/"): + full_ref_path = '%s/%s' % (cls._common_path_default, path) + return full_ref_path + + @classmethod + def delete(cls, repo, path): + """Delete the reference at the given path + + :param repo: + Repository to delete the reference from + + :param path: + Short or full path pointing to the reference, i.e. refs/myreference + or just "myreference", hence 'refs/' is implied. + Alternatively the symbolic reference to be deleted""" + full_ref_path = cls.to_full_path(path) + abs_path = join(repo.git_dir, full_ref_path) + if exists(abs_path): + os.remove(abs_path) + else: + # check packed refs + pack_file_path = cls._get_packed_refs_path(repo) + try: + reader = open(pack_file_path) + except (OSError,IOError): + pass # it didnt exist at all + else: + new_lines = list() + made_change = False + dropped_last_line = False + for line in reader: + # keep line if it is a comment or if the ref to delete is not + # in the line + # If we deleted the last line and this one is a tag-reference object, + # we drop it as well + if ( line.startswith('#') or full_ref_path not in line ) and \ + ( not dropped_last_line or dropped_last_line and not line.startswith('^') ): + new_lines.append(line) + dropped_last_line = False + continue + # END skip comments and lines without our path + + # drop this line + made_change = True + dropped_last_line = True + # END for each line in packed refs + reader.close() + + # write the new lines + if made_change: + open(pack_file_path, 'w').writelines(new_lines) + # END open exception handling + # END handle deletion + + @classmethod + def _create(cls, repo, path, resolve, reference, force): + """internal method used to create a new symbolic reference. + If resolve is False,, the reference will be taken as is, creating + a proper symbolic reference. Otherwise it will be resolved to the + corresponding object and a detached symbolic reference will be created + instead""" + full_ref_path = cls.to_full_path(path) + abs_ref_path = join(repo.git_dir, full_ref_path) + + # figure out target data + target = reference + if resolve: + target = repo.rev_parse(str(reference)) + + if not force and isfile(abs_ref_path): + target_data = str(target) + if isinstance(target, SymbolicReference): + target_data = target.path + if not resolve: + target_data = "ref: " + target_data + if open(abs_ref_path, 'rb').read().strip() != target_data: + raise OSError("Reference at %s does already exist" % full_ref_path) + # END no force handling + + ref = cls(repo, full_ref_path) + ref.reference = target + return ref + + @classmethod + def create(cls, repo, path, reference='HEAD', force=False ): + """Create a new symbolic reference, hence a reference pointing to another reference. + + :param repo: + Repository to create the reference in + + :param path: + full path at which the new symbolic reference is supposed to be + created at, i.e. "NEW_HEAD" or "symrefs/my_new_symref" + + :param reference: + The reference to which the new symbolic reference should point to + + :param force: + if True, force creation even if a symbolic reference with that name already exists. + Raise OSError otherwise + + :return: Newly created symbolic Reference + + :raise OSError: + If a (Symbolic)Reference with the same name but different contents + already exists. + + :note: This does not alter the current HEAD, index or Working Tree""" + return cls._create(repo, path, False, reference, force) + + def rename(self, new_path, force=False): + """Rename self to a new path + + :param new_path: + Either a simple name or a full path, i.e. new_name or features/new_name. + The prefix refs/ is implied for references and will be set as needed. + In case this is a symbolic ref, there is no implied prefix + + :param force: + If True, the rename will succeed even if a head with the target name + already exists. It will be overwritten in that case + + :return: self + :raise OSError: In case a file at path but a different contents already exists """ + new_path = self.to_full_path(new_path) + if self.path == new_path: + return self + + new_abs_path = join(self.repo.git_dir, new_path) + cur_abs_path = join(self.repo.git_dir, self.path) + if isfile(new_abs_path): + if not force: + # if they point to the same file, its not an error + if open(new_abs_path,'rb').read().strip() != open(cur_abs_path,'rb').read().strip(): + raise OSError("File at path %r already exists" % new_abs_path) + # else: we could remove ourselves and use the otherone, but + # but clarity we just continue as usual + # END not force handling + os.remove(new_abs_path) + # END handle existing target file + + dname = dirname(new_abs_path) + if not isdir(dname): + os.makedirs(dname) + # END create directory + + rename(cur_abs_path, new_abs_path) + self.path = new_path + + return self + + @classmethod + def _iter_items(cls, repo, common_path = None): + 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(join_path_native(repo.git_dir, common_path)): + if 'refs/' not in root: # skip non-refs subfolders + refs_id = [ i for i,d in enumerate(dirs) if d == 'refs' ] + if refs_id: + dirs[0:] = ['refs'] + # END prune non-refs folders + + for f in files: + abs_path = to_native_path_linux(join_path(root, f)) + rela_paths.add(abs_path.replace(to_native_path_linux(repo.git_dir) + '/', "")) + # END for each file in root directory + # END for each directory to walk + + # read packed refs + for sha, rela_path in cls._iter_packed_refs(repo): + if rela_path.startswith(common_path): + rela_paths.add(rela_path) + # END relative path matches common path + # END packed refs reading + + # return paths in sorted order + for path in sorted(rela_paths): + try: + yield cls.from_path(repo, path) + except ValueError: + continue + # END for each sorted relative refpath + + @classmethod + def iter_items(cls, repo, common_path = None): + """Find all refs in the repository + + :param repo: is the Repo + + :param common_path: + Optional keyword argument to the path which is to be shared by all + returned Ref objects. + Defaults to class specific portion if None assuring that only + 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. + + 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 ) + + @classmethod + def from_path(cls, repo, path): + """ + :param path: full .git-directory-relative path name to the Reference to instantiate + :note: use to_full_path() if you only have a partial path of a known Reference Type + :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, Head, RemoteReference, TagReference, Reference, SymbolicReference): + try: + instance = ref_type(repo, path) + if instance.__class__ == SymbolicReference and instance.is_detached: + raise ValueError("SymbolRef was detached, we drop it") + return instance + 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) diff --git a/refs/tag.py b/refs/tag.py new file mode 100644 index 00000000..c09d814d --- /dev/null +++ b/refs/tag.py @@ -0,0 +1,91 @@ +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, + 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 = 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): + """:return: Commit object the tag ref points to""" + obj = self.object + if obj.type == "commit": + return obj + elif obj.type == "tag": + # it is a tag object which carries the commit as an object - we can point to anything + return obj.object + else: + raise ValueError( "Tag %s points to a Blob or Tree - have never seen that before" % self ) + + @property + def tag(self): + """ + :return: Tag object this tag ref points to or None in case + we are a light weight tag""" + obj = self.object + if obj.type == "tag": + return obj + return None + + # 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. + + :param path: + The name of the tag, i.e. 1.0 or releases/1.0. + The prefix refs/tags is implied + + :param ref: + A reference to the object you want to tag. It can be a commit, tree or + blob. + + :param 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 + + :param force: + If True, to force creation of a tag even though that tag already exists. + + :param kwargs: + Additional keyword arguments to be passed to git-tag + + :return: 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 +Tag = TagReference -- cgit v1.2.3 From 739fa140235cc9d65c632eaf1f5cacc944d87cfb Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Tue, 23 Nov 2010 09:34:44 +0100 Subject: Fixed remaining tests - lets hope that everything is indeed working correctly - as imports changed, every line of code needs to be run to assure all names can be resolved --- refs/head.py | 2 ++ refs/reference.py | 1 + refs/remote.py | 1 + 3 files changed, 4 insertions(+) (limited to 'refs') diff --git a/refs/head.py b/refs/head.py index f8625bad..91a1132c 100644 --- a/refs/head.py +++ b/refs/head.py @@ -4,6 +4,8 @@ from reference import Reference from git.config import SectionConstraint +from git.util import join_path + from git.exc import GitCommandError __all__ = ["HEAD", "Head"] diff --git a/refs/reference.py b/refs/reference.py index 1f97b92e..a76e2d5d 100644 --- a/refs/reference.py +++ b/refs/reference.py @@ -1,5 +1,6 @@ from symbolic import SymbolicReference import os +from git.objects import Object from git.util import ( LazyMixin, Iterable, diff --git a/refs/remote.py b/refs/remote.py index 7ea9eb46..85dc0f1e 100644 --- a/refs/remote.py +++ b/refs/remote.py @@ -1,5 +1,6 @@ from head import Head from git.util import join_path +from gitdb.util import join import os -- cgit v1.2.3 From 6e5aae2fc8c3832bdae1cd5e0a269405fb059231 Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Tue, 23 Nov 2010 12:35:34 +0100 Subject: Initial interface including some of the implementation of the RefLog. TestCase scetched out for now tests: Added tests to verify that objects don't have a dict. Previously, due to a missing __slots__ member in Serializable, most objects would indeed have a dict, although the opposite was intended --- refs/__init__.py | 5 +- refs/log.py | 161 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 164 insertions(+), 2 deletions(-) create mode 100644 refs/log.py (limited to 'refs') diff --git a/refs/__init__.py b/refs/__init__.py index ca5ace02..fc8ce644 100644 --- a/refs/__init__.py +++ b/refs/__init__.py @@ -16,5 +16,6 @@ import symbolic for item in (HEAD, Head, RemoteReference, TagReference, Reference, SymbolicReference): setattr(symbolic, item.__name__, item) del(symbolic) -# git.objects.Commit -> symbolic -# git.config.SectionConstraint -> head + + +from log import * diff --git a/refs/log.py b/refs/log.py new file mode 100644 index 00000000..f67bea4d --- /dev/null +++ b/refs/log.py @@ -0,0 +1,161 @@ +from head import Head +from git.util import join_path +from gitdb.util import ( + join, + file_contents_ro_filepath + ) + +from git.objects.util import ( + Actor, + parse_actor_and_date, + Serializable, + utctz_to_altz, + altz_to_utctz_str, + ) + +import os + + +__all__ = ["RefLog", "RefLogEntry"] + + +class RefLogEntry(tuple): + """Named tuple allowing easy access to the revlog data fields""" + _fmt = "%s %s %s <%s> %i %s\t%s" + __slots__ = tuple() + + def __repr__(self): + """Representation of ourselves in git reflog format""" + act = self.actor + time = self.time + return self._fmt % (self.oldhexsha, self.newhexsha, act.name, act.email, + time[0], altz_to_utctz_str(time[1]), self.message) + + @property + def oldhexsha(self): + """The hexsha to the commit the ref pointed to before the change""" + return self[0] + + @property + def newhexsha(self): + """The hexsha to the commit the ref now points to, after the change""" + return self[1] + + @property + def actor(self): + """Actor instance, providing access""" + return self[2] + + @property + def time(self): + """time as tuple: + + * [0] = int(time) + * [1] = int(timezone_offset) in time.altzone format """ + return self[3] + + @property + def message(self): + """Message describing the operation that acted on the reference""" + return self[4] + + @classmethod + def new(self, oldhexsha, newhexsha, actor, time, tz_offset, message): + """:return: New instance of a RefLogEntry""" + if not isinstance(actor, Actor): + raise ValueError("Need actor instance, got %s" % actor) + # END check types + return RefLogEntry((oldhexsha, newhexsha, actor, (time, tz_offset), message)) + + @classmethod + def from_line(self, line): + """:return: New RefLogEntry instance from the given revlog line. + :param line: line without trailing newline + :raise ValueError: If line could not be parsed""" + raise NotImplementedError("todo") + + +class RefLog(list, Serializable): + """A reflog contains reflog entries, each of which defines a certain state + of the head in question. Custom query methods allow to retrieve log entries + by date or by other criteria. + + Reflog entries are orded, the first added entry is first in the list, the last + entry, i.e. the last change of the head or reference, is last in the list.""" + + __slots__ = tuple() + + #{ Interface + + @classmethod + def from_file(cls, filepath): + """ + :return: a new RefLog instance containing all entries from the reflog + at the given filepath + :param filepath: path to reflog + :raise ValueError: If the file could not be read or was corrupted in some way""" + inst = cls() + fmap = file_contents_ro_filepath(filepath, stream=False, allow_mmap=True) + try: + inst._deserialize(fmap) + finally: + fmap.close() + #END handle closing of handle + return inst + + @classmethod + def reflog_path(cls, ref): + """ + :return: string to absolute path at which the reflog of the given ref + instance would be found. The path is not guaranteed to point to a valid + file though. + :param ref: SymbolicReference instance""" + return join(ref.repo.git_dir, "logs", ref.path) + + @classmethod + def iter_entries(cls, stream): + """ + :return: Iterator yielding RefLogEntry instances, one for each line read + sfrom the given stream. + :param stream: file-like object containing the revlog in its native format + or basestring instance pointing to a file to read""" + new_entry = RefLogEntry.from_line + if isinstance(stream, basestring): + stream = file_contents_ro_filepath(stream) + #END handle stream type + return (new_entry(line.strip()) for line in stream) + + def to_file(self, filepath): + """Write the contents of the reflog instance to a file at the given filepath. + :param filepath: path to file, parent directories are assumed to exist""" + fp = open(filepath, 'wb') + try: + self._serialize(fp) + finally: + fp.close() + #END handle file streams + + #} END interface + + #{ Serializable Interface + def _serialize(self, stream): + lm1 = len(self) - 1 + write = stream.write() + + # write all entries + for i, e in self: + s = repr(e) + if i != lm1: + s += "\n" + #END handle line separator + write(s) + #END for each entry + + def _deserialize(self, stream): + new_entry = RefLogEntry.from_line + append = self.append + # NOTE: should use iter_entries, but this way it will be more direct and faster + for line in stream: + append(new_entry(line.strip())) + #END handle deserializatoin + #} END serializable interface -- cgit v1.2.3 From a93eb7e8484e5bb40f9b8d11ac64a1621cf4c9cd Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Tue, 23 Nov 2010 15:49:29 +0100 Subject: Implemented reflog reading and writing --- refs/log.py | 60 +++++++++++++++++++++++++++++++++++++++--------------------- 1 file changed, 39 insertions(+), 21 deletions(-) (limited to 'refs') diff --git a/refs/log.py b/refs/log.py index f67bea4d..e7f18c6a 100644 --- a/refs/log.py +++ b/refs/log.py @@ -7,21 +7,22 @@ from gitdb.util import ( from git.objects.util import ( Actor, - parse_actor_and_date, + parse_date, Serializable, utctz_to_altz, altz_to_utctz_str, ) import os - +import re __all__ = ["RefLog", "RefLogEntry"] class RefLogEntry(tuple): """Named tuple allowing easy access to the revlog data fields""" - _fmt = "%s %s %s <%s> %i %s\t%s" + _fmt = "%s %s %s <%s> %i %s\t%s\n" + _re_hexsha_only = re.compile('^[0-9A-Fa-f]{40}$') __slots__ = tuple() def __repr__(self): @@ -68,13 +69,34 @@ class RefLogEntry(tuple): return RefLogEntry((oldhexsha, newhexsha, actor, (time, tz_offset), message)) @classmethod - def from_line(self, line): + def from_line(cls, line): """:return: New RefLogEntry instance from the given revlog line. :param line: line without trailing newline :raise ValueError: If line could not be parsed""" - raise NotImplementedError("todo") + try: + info, msg = line.split('\t', 2) + except ValueError: + raise ValueError("line is missing tab separator") + #END handle first plit + oldhexsha = info[:40] + newhexsha = info[41:81] + for hexsha in (oldhexsha, newhexsha): + if not cls._re_hexsha_only.match(hexsha): + raise ValueError("Invalid hexsha: %s" % hexsha) + # END if hexsha re doesn't match + #END for each hexsha + + email_end = info.find('>', 82) + if email_end == -1: + raise ValueError("Missing token: >") + #END handle missing end brace + + actor = Actor._from_string(info[82:email_end+1]) + time, tz_offset = parse_date(info[email_end+2:]) + + return RefLogEntry((oldhexsha, newhexsha, actor, (time, tz_offset), msg)) + - class RefLog(list, Serializable): """A reflog contains reflog entries, each of which defines a certain state of the head in question. Custom query methods allow to retrieve log entries @@ -104,7 +126,7 @@ class RefLog(list, Serializable): return inst @classmethod - def reflog_path(cls, ref): + def path(cls, ref): """ :return: string to absolute path at which the reflog of the given ref instance would be found. The path is not guaranteed to point to a valid @@ -123,7 +145,12 @@ class RefLog(list, Serializable): if isinstance(stream, basestring): stream = file_contents_ro_filepath(stream) #END handle stream type - return (new_entry(line.strip()) for line in stream) + while True: + line = stream.readline() + if not line: + return + yield new_entry(line.strip()) + #END endless loop def to_file(self, filepath): """Write the contents of the reflog instance to a file at the given filepath. @@ -140,22 +167,13 @@ class RefLog(list, Serializable): #{ Serializable Interface def _serialize(self, stream): lm1 = len(self) - 1 - write = stream.write() + write = stream.write # write all entries - for i, e in self: - s = repr(e) - if i != lm1: - s += "\n" - #END handle line separator - write(s) + for i, e in enumerate(self): + write(repr(e)) #END for each entry def _deserialize(self, stream): - new_entry = RefLogEntry.from_line - append = self.append - # NOTE: should use iter_entries, but this way it will be more direct and faster - for line in stream: - append(new_entry(line.strip())) - #END handle deserializatoin + self.extend(self.iter_entries(stream)) #} END serializable interface -- cgit v1.2.3 From 8ad01ee239f9111133e52af29b78daed34c52e49 Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Tue, 23 Nov 2010 15:58:32 +0100 Subject: SymbolicReference: log method added, including test --- refs/log.py | 3 +-- refs/symbolic.py | 10 ++++++++++ 2 files changed, 11 insertions(+), 2 deletions(-) (limited to 'refs') diff --git a/refs/log.py b/refs/log.py index e7f18c6a..1d07ef9a 100644 --- a/refs/log.py +++ b/refs/log.py @@ -1,4 +1,3 @@ -from head import Head from git.util import join_path from gitdb.util import ( join, @@ -170,7 +169,7 @@ class RefLog(list, Serializable): write = stream.write # write all entries - for i, e in enumerate(self): + for e in self: write(repr(e)) #END for each entry diff --git a/refs/symbolic.py b/refs/symbolic.py index 3329d51c..9dd7629c 100644 --- a/refs/symbolic.py +++ b/refs/symbolic.py @@ -16,6 +16,8 @@ from gitdb.util import ( hex_to_bin ) +from log import RefLog + __all__ = ["SymbolicReference"] class SymbolicReference(object): @@ -270,6 +272,14 @@ class SymbolicReference(object): except TypeError: return True + def log(self): + """ + :return: RefLog for this reference. Its last entry reflects the latest change + applied to this reference + + .. note:: As the log is parsed every time, its recommended to cache it for use + instead of calling this method repeatedly""" + return RefLog.from_file(RefLog.path(self)) @classmethod def to_full_path(cls, path): -- cgit v1.2.3 From 9ccd777c386704911734ae4c33a922a5682ac6c8 Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Tue, 23 Nov 2010 16:32:43 +0100 Subject: Documentation is now being built for all new modules --- refs/head.py | 1 - 1 file changed, 1 deletion(-) (limited to 'refs') diff --git a/refs/head.py b/refs/head.py index 91a1132c..278cecc7 100644 --- a/refs/head.py +++ b/refs/head.py @@ -1,4 +1,3 @@ -""" Module containing all ref based objects """ from symbolic import SymbolicReference from reference import Reference -- cgit v1.2.3 From a21a9f6f13861ddc65671b278e93cf0984adaa30 Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Tue, 23 Nov 2010 21:14:59 +0100 Subject: Actor: Moved it from git.objects.util to git.util, adjusted all imports accordingly. Added methods to Actor to retrieve the global committer and author information Reflog: implemented and tested append_entry method --- refs/log.py | 64 ++++++++++++++++++++++++++++++++++++++++++++++---------- refs/symbolic.py | 1 + 2 files changed, 54 insertions(+), 11 deletions(-) (limited to 'refs') diff --git a/refs/log.py b/refs/log.py index 1d07ef9a..8cb0a5ab 100644 --- a/refs/log.py +++ b/refs/log.py @@ -1,17 +1,22 @@ -from git.util import join_path +from git.util import ( + join_path, + Actor, + ) + from gitdb.util import ( + bin_to_hex, join, file_contents_ro_filepath ) from git.objects.util import ( - Actor, parse_date, Serializable, utctz_to_altz, altz_to_utctz_str, ) +import time import os import re @@ -104,7 +109,28 @@ class RefLog(list, Serializable): Reflog entries are orded, the first added entry is first in the list, the last entry, i.e. the last change of the head or reference, is last in the list.""" - __slots__ = tuple() + __slots__ = ('_path', ) + + def __new__(cls, filepath=None): + inst = super(RefLog, cls).__new__(cls) + return inst + + def __init__(self, filepath=None): + """Initialize this instance with an optional filepath, from which we will + initialize our data. The path is also used to write changes back using + the write() method""" + self._path = filepath + if filepath is not None: + self._read_from_file() + # END handle filepath + + def _read_from_file(self): + fmap = file_contents_ro_filepath(self._path, stream=False, allow_mmap=True) + try: + self._deserialize(fmap) + finally: + fmap.close() + #END handle closing of handle #{ Interface @@ -115,14 +141,7 @@ class RefLog(list, Serializable): at the given filepath :param filepath: path to reflog :raise ValueError: If the file could not be read or was corrupted in some way""" - inst = cls() - fmap = file_contents_ro_filepath(filepath, stream=False, allow_mmap=True) - try: - inst._deserialize(fmap) - finally: - fmap.close() - #END handle closing of handle - return inst + return cls(filepath) @classmethod def path(cls, ref): @@ -154,12 +173,35 @@ class RefLog(list, Serializable): def to_file(self, filepath): """Write the contents of the reflog instance to a file at the given filepath. :param filepath: path to file, parent directories are assumed to exist""" + # TODO: Use locked fd fp = open(filepath, 'wb') try: self._serialize(fp) finally: fp.close() #END handle file streams + + def append_entry(self, oldbinsha, newbinsha, message, write=True): + """Append a new log entry to the revlog, changing it in place. + :param oldbinsha: binary sha of the previous commit + :param newbinsha: binary sha of the current commit + :param message: message describing the change to the reference + :param write: If True, the changes will be written right away. Otherwise + the change will not be written + :return: RefLogEntry objects which was appended to the log""" + if len(oldbinsha) != 20 or len(newbinsha) != 20: + raise ValueError("Shas need to be given in binary format") + #END handle sha type + entry = RefLogEntry((bin_to_hex(oldbinsha), bin_to_hex(newbinsha), Actor.committer(), (int(time.time()), time.altzone), message)) + self.append(entry) + if write: + self.write() + #END handle auto-write + return entry + + def write(self): + """Write this instance's data to the file we are originating from""" + return self.to_file(self._path) #} END interface diff --git a/refs/symbolic.py b/refs/symbolic.py index 9dd7629c..b978e484 100644 --- a/refs/symbolic.py +++ b/refs/symbolic.py @@ -236,6 +236,7 @@ class SymbolicReference(object): if not isdir(directory): os.makedirs(directory) + # TODO: Write using LockedFD fp = open(path, "wb") try: fp.write(write_value) -- cgit v1.2.3 From 61f3db7bd07ac2f3c2ff54615c13bf9219289932 Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Tue, 23 Nov 2010 22:47:34 +0100 Subject: Removed ORIG_HEAD handling which was downright wrong. ORIG_HEAD gets only set during merge and rebase, and probably everything that changes the ref more drastically. Probably I have to reread that. What needs to be adjusted though is the reflog --- refs/head.py | 17 ----------------- refs/log.py | 14 +++++++++----- refs/symbolic.py | 25 ++++++------------------- 3 files changed, 15 insertions(+), 41 deletions(-) (limited to 'refs') diff --git a/refs/head.py b/refs/head.py index 278cecc7..08ad581d 100644 --- a/refs/head.py +++ b/refs/head.py @@ -29,23 +29,6 @@ class HEAD(SymbolicReference): to contain the previous value of HEAD""" return SymbolicReference(self.repo, self._ORIG_HEAD_NAME) - def _set_reference(self, ref): - """If someone changes the reference through us, we must manually update - the ORIG_HEAD if we are detached. The underlying implementation can only - handle un-detached heads as it has to check whether the current head - is the checked-out one""" - if self.is_detached: - prev_commit = self.commit - super(HEAD, self)._set_reference(ref) - SymbolicReference.create(self.repo, self._ORIG_HEAD_NAME, prev_commit, force=True) - else: - super(HEAD, self)._set_reference(ref) - # END handle detached mode - - # aliased reference - reference = property(SymbolicReference._get_reference, _set_reference, doc="Returns the Reference we point to") - ref = reference - def reset(self, commit='HEAD', index=True, working_tree = False, paths=None, **kwargs): """Reset our HEAD to the given commit optionally synchronizing diff --git a/refs/log.py b/refs/log.py index 8cb0a5ab..c2799f79 100644 --- a/refs/log.py +++ b/refs/log.py @@ -1,6 +1,7 @@ from git.util import ( join_path, Actor, + LockedFD, ) from gitdb.util import ( @@ -173,13 +174,16 @@ class RefLog(list, Serializable): def to_file(self, filepath): """Write the contents of the reflog instance to a file at the given filepath. :param filepath: path to file, parent directories are assumed to exist""" - # TODO: Use locked fd - fp = open(filepath, 'wb') + lfd = LockedFD(filepath) + fp = lfd.open(write=True, stream=True) try: self._serialize(fp) - finally: - fp.close() - #END handle file streams + lfd.commit() + except: + # on failure it rolls back automatically, but we make it clear + lfd.rollback() + raise + #END handle change def append_entry(self, oldbinsha, newbinsha, message, write=True): """Append a new log entry to the revlog, changing it in place. diff --git a/refs/symbolic.py b/refs/symbolic.py index b978e484..94e8d726 100644 --- a/refs/symbolic.py +++ b/refs/symbolic.py @@ -13,7 +13,8 @@ from gitdb.util import ( exists, isfile, rename, - hex_to_bin + hex_to_bin, + LockedFD ) from log import RefLog @@ -181,11 +182,13 @@ class SymbolicReference(object): raise TypeError("%s is a detached symbolic reference as it points to %r" % (self, sha)) return self.from_path(self.repo, target_ref_path) - def _set_reference(self, ref): + def _set_reference(self, ref, msg = None): """Set ourselves to the given ref. It will stay a symbol if the ref is a Reference. 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""" + Strings are allowed but will be checked to be sure we have a commit + :param msg: If set to a string, the message will be used in the reflog. + Otherwise, a reflog entry is not written for the changed reference""" write_value = None if isinstance(ref, SymbolicReference): write_value = "ref: %s" % ref.path @@ -205,22 +208,6 @@ class SymbolicReference(object): # END end try string # END try commit attribute - # maintain the orig-head if we are currently checked-out - head = HEAD(self.repo) - try: - if head.ref == self: - try: - # TODO: implement this atomically, if we fail below, orig_head is at an incorrect spot - # Enforce the creation of ORIG_HEAD - SymbolicReference.create(self.repo, head.orig_head().name, self.commit, force=True) - except ValueError: - pass - #END exception handling - # END if we are checked-out - except TypeError: - pass - # END handle detached heads - # 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. Besides, this works -- cgit v1.2.3 From 7029773512eee5a0bb765b82cfdd90fd5ab34e15 Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Tue, 23 Nov 2010 23:20:11 +0100 Subject: Implemented revlog.append_entry as classmethod, to assure we will always actually write_append the new entry, instead of rewriting the whole file. Added file-locking and directory handling, so the implementation should be similar (enough) to the git reference implementation. Next up is to implement a way to update the reflog when changing references, which is going to be a little more complicated --- refs/log.py | 37 +++++++++++++++++++++++++++---------- refs/symbolic.py | 13 ++++++++++++- 2 files changed, 39 insertions(+), 11 deletions(-) (limited to 'refs') diff --git a/refs/log.py b/refs/log.py index c2799f79..9728911a 100644 --- a/refs/log.py +++ b/refs/log.py @@ -2,12 +2,15 @@ from git.util import ( join_path, Actor, LockedFD, + 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 ( @@ -151,7 +154,7 @@ class RefLog(list, Serializable): instance would be found. The path is not guaranteed to point to a valid file though. :param ref: SymbolicReference instance""" - return join(ref.repo.git_dir, "logs", ref.path) + return join(ref.repo.git_dir, "logs", to_native_path(ref.path)) @classmethod def iter_entries(cls, stream): @@ -175,6 +178,8 @@ class RefLog(list, Serializable): """Write the contents of the reflog instance to a file at the given filepath. :param filepath: path to file, parent directories are assumed to exist""" lfd = LockedFD(filepath) + assure_directory_exists(filepath, is_file=True) + fp = lfd.open(write=True, stream=True) try: self._serialize(fp) @@ -185,22 +190,34 @@ class RefLog(list, Serializable): raise #END handle change - def append_entry(self, oldbinsha, newbinsha, message, write=True): - """Append a new log entry to the revlog, changing it in place. + @classmethod + def append_entry(cls, filepath, oldbinsha, newbinsha, message): + """Append a new log entry to the revlog at filepath. :param oldbinsha: binary sha of the previous commit :param newbinsha: binary sha of the current commit :param message: message describing the change to the reference :param write: If True, the changes will be written right away. Otherwise the change will not be written - :return: RefLogEntry objects which was appended to the log""" + :return: RefLogEntry objects which was appended to the log + :note: As we are append-only, concurrent access is not a problem as we + do not interfere with readers.""" if len(oldbinsha) != 20 or len(newbinsha) != 20: raise ValueError("Shas need to be given in binary format") #END handle sha type - entry = RefLogEntry((bin_to_hex(oldbinsha), bin_to_hex(newbinsha), Actor.committer(), (int(time.time()), time.altzone), message)) - self.append(entry) - if write: - self.write() - #END handle auto-write + assure_directory_exists(filepath, is_file=True) + entry = RefLogEntry((bin_to_hex(oldbinsha), bin_to_hex(newbinsha), Actor.committer(), (int(time.time()), time.altzone), message)) + + lf = LockFile(filepath) + lf._obtain_lock_or_raise() + + fd = open(filepath, 'a') + try: + fd.write(repr(entry)) + finally: + fd.close() + lf._release_lock() + #END handle write operation + return entry def write(self): diff --git a/refs/symbolic.py b/refs/symbolic.py index 94e8d726..0d8fdfd1 100644 --- a/refs/symbolic.py +++ b/refs/symbolic.py @@ -266,8 +266,19 @@ class SymbolicReference(object): applied to this reference .. note:: As the log is parsed every time, its recommended to cache it for use - instead of calling this method repeatedly""" + instead of calling this method repeatedly. It should be considered read-only.""" return RefLog.from_file(RefLog.path(self)) + + def log_append(self, oldbinsha, message, newbinsha=None): + """Append a logentry to the logfile of this ref + :param oldbinsha: binary sha this ref used to point to + :param message: A message describing the change + :param newbinsha: The sha the ref points to now. If None, our current commit sha + will be used + :return: added RefLogEntry instance""" + return RefLog.append_entry(RefLog.path(self), oldbinsha, + (newbinsha is None and self.commit.binsha) or newbinsha, + message) @classmethod def to_full_path(cls, path): -- cgit v1.2.3 From a17c43d0662bab137903075f2cff34bcabc7e1d1 Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Wed, 24 Nov 2010 12:30:51 +0100 Subject: Made previously protected methods public to introduce a method with reflog support which cannot be exposed using the respective property. Ref-Creation is now fully implemented in python. For details, see doc/source/changes.rst --- refs/head.py | 33 ---------------- refs/reference.py | 27 +------------ refs/remote.py | 5 +++ refs/symbolic.py | 114 +++++++++++++++++++++++++++++++++--------------------- 4 files changed, 76 insertions(+), 103 deletions(-) (limited to 'refs') diff --git a/refs/head.py b/refs/head.py index 08ad581d..d8729434 100644 --- a/refs/head.py +++ b/refs/head.py @@ -112,38 +112,6 @@ class Head(Reference): k_config_remote = "remote" k_config_remote_ref = "merge" # branch to merge from remote - @classmethod - def create(cls, repo, path, commit='HEAD', force=False, **kwargs): - """Create a new head. - :param repo: Repository to create the head in - :param path: - The name or path of the head, i.e. 'new_branch' or - feature/feature1. The prefix refs/heads is implied. - - :param commit: - Commit to which the new head should point, defaults to the - current HEAD - - :param force: - if True, force creation even if branch with that name already exists. - - :param kwargs: - Additional keyword arguments to be passed to git-branch, i.e. - track, no-track, l - - :return: 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 @@ -157,7 +125,6 @@ class Head(Reference): 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 diff --git a/refs/reference.py b/refs/reference.py index a76e2d5d..446196c3 100644 --- a/refs/reference.py +++ b/refs/reference.py @@ -18,6 +18,7 @@ class Reference(SymbolicReference, LazyMixin, Iterable): """Represents a named reference to any object. Subclasses may apply restrictions though, i.e. Heads can only point to commits.""" __slots__ = tuple() + _resolve_ref_on_create = True _common_path_default = "refs" def __init__(self, repo, path): @@ -52,7 +53,7 @@ class Reference(SymbolicReference, LazyMixin, Iterable): :note: TypeChecking is done by the git command""" - abs_path = self._abs_path() + abs_path = self.abspath existed = True if not isfile(abs_path): existed = False @@ -81,31 +82,7 @@ class Reference(SymbolicReference, LazyMixin, Iterable): return self.path # could be refs/HEAD return '/'.join(tokens[2:]) - @classmethod - def create(cls, repo, path, commit='HEAD', force=False ): - """Create a new reference. - - :param repo: Repository to create the reference in - :param path: - The relative path of the reference, i.e. 'new_branch' or - feature/feature1. The path prefix 'refs/' is implied if not - given explicitly - - :param commit: - Commit to which the new reference should point, defaults to the - current HEAD - - :param force: - if True, force creation even if a reference with that name already exists. - Raise OSError otherwise - - :return: Newly created Reference - - :note: This does not alter the current HEAD, index or Working Tree""" - return cls._create(repo, path, True, commit, force) - - @classmethod def iter_items(cls, repo, common_path = None): """Equivalent to SymbolicReference.iter_items, but will return non-detached references as well.""" diff --git a/refs/remote.py b/refs/remote.py index 85dc0f1e..b7b07d4b 100644 --- a/refs/remote.py +++ b/refs/remote.py @@ -56,3 +56,8 @@ 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/refs/symbolic.py b/refs/symbolic.py index 0d8fdfd1..90fef8d5 100644 --- a/refs/symbolic.py +++ b/refs/symbolic.py @@ -3,7 +3,8 @@ from git.objects import Commit from git.util import ( join_path, join_path_native, - to_native_path_linux + to_native_path_linux, + assure_directory_exists ) from gitdb.util import ( @@ -28,6 +29,7 @@ class SymbolicReference(object): A typical example for a symbolic reference is HEAD.""" __slots__ = ("repo", "path") + _resolve_ref_on_create = False _common_path_default = "" _id_attribute_ = "name" @@ -58,7 +60,8 @@ class SymbolicReference(object): is the path itself.""" return self.path - def _abs_path(self): + @property + def abspath(self): return join_path_native(self.repo.git_dir, self.path) @classmethod @@ -116,7 +119,7 @@ class SymbolicReference(object): point to, or None""" tokens = None try: - fp = open(self._abs_path(), 'r') + fp = open(self.abspath, 'r') value = fp.read().rstrip() fp.close() tokens = value.split(" ") @@ -158,37 +161,48 @@ class SymbolicReference(object): return self.from_path(self.repo, target_ref_path).commit - def _set_commit(self, commit): + def set_commit(self, commit, msg = None): """Set our commit, possibly dereference our symbolic reference first. - If the reference does not exist, it will be created""" + If the reference does not exist, it will be created + + :param msg: If not None, the message will be used in the reflog entry to be + written. Otherwise the reflog is not altered""" is_detached = True try: is_detached = self.is_detached except ValueError: pass # END handle non-existing ones + if is_detached: - return self._set_reference(commit) + return self.set_reference(commit, msg) # set the commit on our reference - self._get_reference().commit = commit + self._get_reference().set_commit(commit, msg) - commit = property(_get_commit, _set_commit, doc="Query or set commits directly") + commit = property(_get_commit, set_commit, doc="Query or set commits directly") def _get_reference(self): - """:return: Reference Object we point to""" + """:return: Reference Object we point to + :raise TypeError: If this symbolic reference is detached, hence it doesn't point + to a reference, but to a commit""" sha, target_ref_path = self._get_ref_info() if target_ref_path is None: raise TypeError("%s is a detached symbolic reference as it points to %r" % (self, sha)) return self.from_path(self.repo, target_ref_path) - def _set_reference(self, ref, msg = None): + def set_reference(self, ref, msg = None): """Set ourselves to the given ref. It will stay a symbol if the ref is a Reference. - Otherwise we try to get a commit from it using our interface. + Otherwise a commmit, given as Commit object or refspec, is assumed and if valid, + will be set which effectively detaches the refererence if it was a purely + symbolic one. - Strings are allowed but will be checked to be sure we have a commit + :param ref: SymbolicReference instance, Commit instance or refspec string :param msg: If set to a string, the message will be used in the reflog. - Otherwise, a reflog entry is not written for the changed reference""" + Otherwise, a reflog entry is not written for the changed reference. + The previous commit of the entry will be the commit we point to now. + + See also: log_append()""" write_value = None if isinstance(ref, SymbolicReference): write_value = "ref: %s" % ref.path @@ -207,33 +221,31 @@ class SymbolicReference(object): raise ValueError("Could not extract object from %s" % ref) # END end try string # END try commit attribute + oldbinsha = None + if msg is not None: + try: + oldhexsha = self.commit.binsha + except ValueError: + oldbinsha = Commit.NULL_BIN_SHA + #END handle non-existing + #END retrieve old hexsha + + fpath = self.abspath + assure_directory_exists(fpath, is_file=True) + + lfd = LockedFD(fpath) + fd = lfd.open(write=True, stream=True) + fd.write(write_value) + lfd.commit() + + # Adjust the reflog + if msg is not None: + self.log_append(oldbinsha, msg) + #END handle reflog - # 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. Besides, this works - # recursively automaitcally, but should be replaced with a python implementation - # soon - if write_value.startswith('ref:'): - self.repo.git.symbolic_ref(self.path, write_value[5:]) - return - # END non-detached handling - - path = self._abs_path() - directory = dirname(path) - if not isdir(directory): - os.makedirs(directory) - - # TODO: Write using LockedFD - fp = open(path, "wb") - try: - fp.write(write_value) - finally: - fp.close() - # END writing - # aliased reference - reference = property(_get_reference, _set_reference, doc="Returns the Reference we point to") + reference = property(_get_reference, set_reference, doc="Returns the Reference we point to") ref = reference def is_valid(self): @@ -255,7 +267,7 @@ class SymbolicReference(object): True if we are a detached reference, hence we point to a specific commit instead to another reference""" try: - self.reference + self.ref return False except TypeError: return True @@ -343,11 +355,18 @@ class SymbolicReference(object): open(pack_file_path, 'w').writelines(new_lines) # END open exception handling # END handle deletion + + # delete the reflog + reflog_path = RefLog.path(cls(repo, full_ref_path)) + if os.path.isfile(reflog_path): + os.remove(reflog_path) + #END remove reflog + @classmethod - def _create(cls, repo, path, resolve, reference, force): + def _create(cls, repo, path, resolve, reference, force, msg=None): """internal method used to create a new symbolic reference. - If resolve is False,, the reference will be taken as is, creating + If resolve is False, the reference will be taken as is, creating a proper symbolic reference. Otherwise it will be resolved to the corresponding object and a detached symbolic reference will be created instead""" @@ -365,16 +384,17 @@ class SymbolicReference(object): target_data = target.path if not resolve: target_data = "ref: " + target_data - if open(abs_ref_path, 'rb').read().strip() != target_data: - raise OSError("Reference at %s does already exist" % full_ref_path) + existing_data = open(abs_ref_path, 'rb').read().strip() + if existing_data != target_data: + raise OSError("Reference at %r does already exist, pointing to %r, requested was %r" % (full_ref_path, existing_data, target_data)) # END no force handling ref = cls(repo, full_ref_path) - ref.reference = target + ref.set_reference(target, msg) return ref @classmethod - def create(cls, repo, path, reference='HEAD', force=False ): + def create(cls, repo, path, reference='HEAD', force=False, msg=None): """Create a new symbolic reference, hence a reference pointing to another reference. :param repo: @@ -391,6 +411,10 @@ class SymbolicReference(object): if True, force creation even if a symbolic reference with that name already exists. Raise OSError otherwise + :param msg: + If not None, the message to append to the reflog. Otherwise no reflog + entry is written. + :return: Newly created symbolic Reference :raise OSError: @@ -398,7 +422,7 @@ class SymbolicReference(object): already exists. :note: This does not alter the current HEAD, index or Working Tree""" - return cls._create(repo, path, False, reference, force) + return cls._create(repo, path, cls._resolve_ref_on_create, reference, force, msg) def rename(self, new_path, force=False): """Rename self to a new path -- cgit v1.2.3 From ec0657cf5de9aeb5629cc4f4f38b36f48490493e Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Wed, 24 Nov 2010 15:56:49 +0100 Subject: Unified object and commit handling which should make the reflog handling much easier. There is some bug in it though, it still needs fixing --- refs/log.py | 9 ++-- refs/reference.py | 37 +---------------- refs/symbolic.py | 122 ++++++++++++++++++++++++++++++++++-------------------- 3 files changed, 85 insertions(+), 83 deletions(-) (limited to 'refs') diff --git a/refs/log.py b/refs/log.py index 9728911a..129803b4 100644 --- a/refs/log.py +++ b/refs/log.py @@ -191,8 +191,11 @@ class RefLog(list, Serializable): #END handle change @classmethod - def append_entry(cls, filepath, oldbinsha, newbinsha, message): - """Append a new log entry to the revlog at filepath. + def append_entry(cls, config_reader, filepath, oldbinsha, newbinsha, message): + """Append a new log entry to the revlog at filepath. + :param config_reader: configuration reader of the repository - used to obtain + user information. May be None + :param filepath: full path to the log file :param oldbinsha: binary sha of the previous commit :param newbinsha: binary sha of the current commit :param message: message describing the change to the reference @@ -205,7 +208,7 @@ class RefLog(list, Serializable): raise ValueError("Shas need to be given in binary format") #END handle sha type assure_directory_exists(filepath, is_file=True) - entry = RefLogEntry((bin_to_hex(oldbinsha), bin_to_hex(newbinsha), Actor.committer(), (int(time.time()), time.altzone), message)) + entry = RefLogEntry((bin_to_hex(oldbinsha), bin_to_hex(newbinsha), Actor.committer(config_reader), (int(time.time()), time.altzone), message)) lf = LockFile(filepath) lf._obtain_lock_or_raise() diff --git a/refs/reference.py b/refs/reference.py index 446196c3..e7cdfdee 100644 --- a/refs/reference.py +++ b/refs/reference.py @@ -18,6 +18,7 @@ class Reference(SymbolicReference, LazyMixin, Iterable): """Represents a named reference to any object. Subclasses may apply restrictions though, i.e. Heads can only point to commits.""" __slots__ = tuple() + _points_to_commits_only = False _resolve_ref_on_create = True _common_path_default = "refs" @@ -36,42 +37,6 @@ class Reference(SymbolicReference, LazyMixin, Iterable): def __str__(self): return self.name - def _get_object(self): - """ - :return: - 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""" - # 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))) - - def _set_object(self, ref): - """ - Set our reference to point to the given ref. It will be converted - to a specific hexsha. - If the reference does not exist, it will be created. - - :note: - TypeChecking is done by the git command""" - abs_path = self.abspath - existed = True - if not isfile(abs_path): - existed = False - open(abs_path, 'wb').write(Object.NULL_HEX_SHA) - # END quick create - - # do it safely by specifying the old value - try: - self.repo.git.update_ref(self.path, ref, (existed and self._get_object().hexsha) or None) - except: - if not existed: - os.remove(abs_path) - # END remove file on error if it didn't exist before - raise - # END exception handling - - object = property(_get_object, _set_object, doc="Return the object our ref currently refers to") - @property def name(self): """:return: (shortest) Name of this reference - it may contain path components""" diff --git a/refs/symbolic.py b/refs/symbolic.py index 90fef8d5..2ecdee4a 100644 --- a/refs/symbolic.py +++ b/refs/symbolic.py @@ -1,5 +1,5 @@ import os -from git.objects import Commit +from git.objects import Object, Commit from git.util import ( join_path, join_path_native, @@ -30,6 +30,7 @@ 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" @@ -107,19 +108,19 @@ class SymbolicReference(object): intermediate references as required :param repo: the repository containing the reference at ref_path""" while True: - ref = cls(repo, ref_path) - hexsha, ref_path = ref._get_ref_info() + hexsha, ref_path = cls._get_ref_info(repo, ref_path) if hexsha is not None: return hexsha # END recursive dereferencing - def _get_ref_info(self): + @classmethod + def _get_ref_info(cls, repo, path): """Return: (sha, target_ref_path) if available, the sha the file at rela_path points to, or None. target_ref_path is the reference we point to, or None""" tokens = None try: - fp = open(self.abspath, 'r') + fp = open(join(repo.git_dir, path), 'r') value = fp.read().rstrip() fp.close() tokens = value.split(" ") @@ -127,46 +128,69 @@ class SymbolicReference(object): # Probably we are just packed, find our entry in the packed refs file # NOTE: We are not a symbolic ref if we are in a packed file, as these # are excluded explictly - for sha, path in self._iter_packed_refs(self.repo): - if path != self.path: continue + for sha, path in cls._iter_packed_refs(repo): + if path != path: continue tokens = (sha, path) break # END for each packed ref # END handle packed refs if tokens is None: - raise ValueError("Reference at %r does not exist" % self.path) + raise ValueError("Reference at %r does not exist" % path) # is it a reference ? if tokens[0] == 'ref:': return (None, tokens[1]) # its a commit - if self.repo.re_hexsha_only.match(tokens[0]): + if repo.re_hexsha_only.match(tokens[0]): return (tokens[0], None) - raise ValueError("Failed to parse reference information from %r" % self.path) - + raise ValueError("Failed to parse reference information from %r" % path) + + def _get_object(self): + """ + :return: + 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""" + # 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))) + def _get_commit(self): """ :return: Commit object we point to, works for detached and non-detached - SymbolicReferences""" - # we partially reimplement it to prevent unnecessary file access - hexsha, target_ref_path = self._get_ref_info() - - # it is a detached reference - if hexsha: - return Commit(self.repo, hex_to_bin(hexsha)) - - return self.from_path(self.repo, target_ref_path).commit + SymbolicReferences. The symbolic reference will be dereferenced recursively.""" + obj = self._get_object() + if obj.type != Commit.type: + raise TypeError("Symbolic Reference pointed to object %r, commit was required" % obj) + #END handle type + return obj def set_commit(self, commit, msg = None): - """Set our commit, possibly dereference our symbolic reference first. + """As set_object, but restricts the type of object to be a Commit + :note: To save cycles, we do not yet check whether the given Object + is actually referring to a commit - for now it may be any of our + Object or Reference types, as well as a refspec""" + # may have to check the type ... this is costly as we would have to use + # revparse + self.set_object(commit, msg) + + + def set_object(self, object, msg = 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 msg: If not None, the message will be used in the reflog entry to be - written. Otherwise the reflog is not altered""" + written. Otherwise the reflog is not altered + :note: plain SymbolicReferences may not actually point to objects by convention""" + if isinstance(object, SymbolicReference): + object = object.object + #END resolve references + is_detached = True try: is_detached = self.is_detached @@ -175,56 +199,66 @@ class SymbolicReference(object): # END handle non-existing ones if is_detached: - return self.set_reference(commit, msg) + return self.set_reference(object, msg) # set the commit on our reference - self._get_reference().set_commit(commit, msg) + self._get_reference().set_object(object, msg) 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") def _get_reference(self): """:return: Reference Object we point to :raise TypeError: If this symbolic reference is detached, hence it doesn't point to a reference, but to a commit""" - sha, target_ref_path = self._get_ref_info() + sha, target_ref_path = self._get_ref_info(self.repo, self.path) if target_ref_path is None: raise TypeError("%s is a detached symbolic reference as it points to %r" % (self, sha)) return self.from_path(self.repo, target_ref_path) def set_reference(self, ref, msg = None): """Set ourselves to the given ref. It will stay a symbol if the ref is a Reference. - Otherwise a commmit, given as Commit object or refspec, is assumed and if valid, + Otherwise an Object, given as Object instance or refspec, is assumed and if valid, will be set which effectively detaches the refererence if it was a purely symbolic one. - :param ref: SymbolicReference instance, Commit instance or refspec string + :param ref: SymbolicReference instance, Object instance 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 msg: If set to a string, the message will be used in the reflog. Otherwise, a reflog entry is not written for the changed reference. The previous commit of the entry will be the commit we point to now. - See also: log_append()""" + See also: log_append() + :note: This symbolic reference will not be dereferenced. For that, see + ``set_object(...)``""" write_value = None + obj = None if isinstance(ref, SymbolicReference): write_value = "ref: %s" % ref.path - elif isinstance(ref, Commit): + elif isinstance(ref, Object): + obj = ref write_value = ref.hexsha - else: + elif isinstance(ref, basestring): try: - write_value = ref.commit.hexsha - except AttributeError: - try: - obj = self.repo.rev_parse(ref+"^{}") # optionally deref tags - if obj.type != "commit": - raise TypeError("Invalid object type behind sha: %s" % sha) - write_value = obj.hexsha - except Exception: - raise ValueError("Could not extract object from %s" % ref) - # END end try string + obj = self.repo.rev_parse(ref+"^{}") # optionally deref tags + write_value = obj.hexsha + except Exception: + raise ValueError("Could not extract object from %s" % ref) + # END end try string + else: + raise ValueError("Unrecognized Value: %r" % ref) # END try commit attribute + + # typecheck + if obj is not None and self._points_to_commits_only and obj.type != Commit.type: + raise TypeError("Require commit, got %r" % obj) + #END verify type + oldbinsha = None if msg is not None: try: - oldhexsha = self.commit.binsha + oldbinsha = self.commit.binsha except ValueError: oldbinsha = Commit.NULL_BIN_SHA #END handle non-existing @@ -247,14 +281,14 @@ class SymbolicReference(object): # aliased reference reference = property(_get_reference, set_reference, doc="Returns the Reference we point to") ref = reference - + def is_valid(self): """ :return: True if the reference is valid, hence it can be read and points to a valid object or reference.""" try: - self.commit + self.object except (OSError, ValueError): return False else: @@ -288,7 +322,7 @@ class SymbolicReference(object): :param newbinsha: The sha the ref points to now. If None, our current commit sha will be used :return: added RefLogEntry instance""" - return RefLog.append_entry(RefLog.path(self), oldbinsha, + return RefLog.append_entry(self.repo.config_reader(), RefLog.path(self), oldbinsha, (newbinsha is None and self.commit.binsha) or newbinsha, message) -- cgit v1.2.3 From 264ba6f54f928da31a037966198a0849325b3732 Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Wed, 24 Nov 2010 17:12:36 +0100 Subject: Fixed remaining issues, all tests work as expected --- refs/symbolic.py | 44 ++++++++++++++++++++++++++++++++------------ 1 file changed, 32 insertions(+), 12 deletions(-) (limited to 'refs') diff --git a/refs/symbolic.py b/refs/symbolic.py index 2ecdee4a..83dbafd2 100644 --- a/refs/symbolic.py +++ b/refs/symbolic.py @@ -7,6 +7,7 @@ from git.util import ( assure_directory_exists ) +from gitdb.exc import BadObject from gitdb.util import ( join, dirname, @@ -114,13 +115,13 @@ class SymbolicReference(object): # END recursive dereferencing @classmethod - def _get_ref_info(cls, repo, path): + def _get_ref_info(cls, repo, ref_path): """Return: (sha, target_ref_path) if available, the sha the file at rela_path points to, or None. target_ref_path is the reference we point to, or None""" tokens = None try: - fp = open(join(repo.git_dir, path), 'r') + fp = open(join(repo.git_dir, ref_path), 'r') value = fp.read().rstrip() fp.close() tokens = value.split(" ") @@ -129,14 +130,13 @@ class SymbolicReference(object): # NOTE: We are not a symbolic ref if we are in a packed file, as these # are excluded explictly for sha, path in cls._iter_packed_refs(repo): - if path != path: continue + if path != ref_path: continue tokens = (sha, path) break # END for each packed ref # END handle packed refs - if tokens is None: - raise ValueError("Reference at %r does not exist" % path) + raise ValueError("Reference at %r does not exist" % ref_path) # is it a reference ? if tokens[0] == 'ref:': @@ -146,7 +146,7 @@ class SymbolicReference(object): if repo.re_hexsha_only.match(tokens[0]): return (tokens[0], None) - raise ValueError("Failed to parse reference information from %r" % path) + raise ValueError("Failed to parse reference information from %r" % ref_path) def _get_object(self): """ @@ -163,6 +163,10 @@ class SymbolicReference(object): Commit object we point to, works for detached and non-detached SymbolicReferences. The symbolic reference will be dereferenced recursively.""" obj = self._get_object() + if obj.type == 'tag': + obj = obj.object + #END dereference tag + if obj.type != Commit.type: raise TypeError("Symbolic Reference pointed to object %r, commit was required" % obj) #END handle type @@ -170,11 +174,27 @@ class SymbolicReference(object): def set_commit(self, commit, msg = None): """As set_object, but restricts the type of object to be a Commit - :note: To save cycles, we do not yet check whether the given Object - is actually referring to a commit - for now it may be any of our - Object or Reference types, as well as a refspec""" - # may have to check the type ... this is costly as we would have to use - # revparse + :raise ValueError: If commit is not a Commit object or doesn't point to + a commit""" + # 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 + elif isinstance(commit, SymbolicReference): + invalid_type = commit.object.type != Commit.type + else: + try: + invalid_type = self.repo.rev_parse(commit).type != Commit.type + except BadObject: + raise ValueError("Invalid object: %s" % commit) + #END handle exception + # END verify type + + if invalid_type: + raise ValueError("Need commit, got %r" % commit) + #END handle raise + + # we leave strings to the rev-parse method below self.set_object(commit, msg) @@ -243,7 +263,7 @@ class SymbolicReference(object): try: obj = self.repo.rev_parse(ref+"^{}") # optionally deref tags write_value = obj.hexsha - except Exception: + except BadObject: raise ValueError("Could not extract object from %s" % ref) # END end try string else: -- cgit v1.2.3 From c946bf260d3f7ca54bffb796a82218dce0eb703f Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Wed, 24 Nov 2010 17:51:22 +0100 Subject: Added tests for creation and adjustments of reference, verifying the log gets written --- refs/reference.py | 30 ++++++++++++++++++++++++++++++ refs/symbolic.py | 14 +++++++++++--- 2 files changed, 41 insertions(+), 3 deletions(-) (limited to 'refs') diff --git a/refs/reference.py b/refs/reference.py index e7cdfdee..c44ebf00 100644 --- a/refs/reference.py +++ b/refs/reference.py @@ -37,6 +37,36 @@ class Reference(SymbolicReference, LazyMixin, Iterable): def __str__(self): return self.name + def set_object(self, object, msg = None): + """Special version which checks if the head-log needs an update as well""" + oldbinsha = None + if msg is not None: + head = self.repo.head + if not head.is_detached and head.ref == self: + oldbinsha = self.commit.binsha + #END handle commit retrieval + #END handle message is set + + super(Reference, self).set_object(object, msg) + + if oldbinsha is not None: + # /* from refs.c in git-source + # * Special hack: If a branch is updated directly and HEAD + # * points to it (may happen on the remote side of a push + # * for example) then logically the HEAD reflog should be + # * updated too. + # * A generic solution implies reverse symref information, + # * but finding all symrefs pointing to the given branch + # * would be rather costly for this rare event (the direct + # * update of a branch) to be worth it. So let's cheat and + # * check with HEAD only which should cover 99% of all usage + # * scenarios (even 100% of the default ones). + # */ + self.repo.head.log_append(oldbinsha, msg) + #END check if the head + + # NOTE: Don't have to overwrite properties as the will only work without a the log + @property def name(self): """:return: (shortest) Name of this reference - it may contain path components""" diff --git a/refs/symbolic.py b/refs/symbolic.py index 83dbafd2..cdd6158a 100644 --- a/refs/symbolic.py +++ b/refs/symbolic.py @@ -175,7 +175,8 @@ class SymbolicReference(object): def set_commit(self, commit, msg = None): """As set_object, but restricts the type of object to be a Commit :raise ValueError: If commit is not a Commit object or doesn't point to - a commit""" + a commit + :return: self""" # check the type - assume the best if it is a base-string invalid_type = False if isinstance(commit, Object): @@ -197,6 +198,8 @@ class SymbolicReference(object): # we leave strings to the rev-parse method below self.set_object(commit, msg) + return self + def set_object(self, object, msg = None): """Set the object we point to, possibly dereference our symbolic reference first. @@ -206,7 +209,8 @@ class SymbolicReference(object): will be dereferenced beforehand to obtain the object they point to :param msg: 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""" + :note: plain SymbolicReferences may not actually point to objects by convention + :return: self""" if isinstance(object, SymbolicReference): object = object.object #END resolve references @@ -222,7 +226,7 @@ class SymbolicReference(object): return self.set_reference(object, msg) # set the commit on our reference - self._get_reference().set_object(object, msg) + return self._get_reference().set_object(object, msg) 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") @@ -250,6 +254,8 @@ class SymbolicReference(object): The previous commit of the entry will be the commit we point to now. See also: log_append() + + :return: self :note: This symbolic reference will not be dereferenced. For that, see ``set_object(...)``""" write_value = None @@ -297,6 +303,8 @@ class SymbolicReference(object): self.log_append(oldbinsha, msg) #END handle reflog + return self + # aliased reference reference = property(_get_reference, set_reference, doc="Returns the Reference we point to") -- cgit v1.2.3 From 86523260c495d9a29aa5ab29d50d30a5d1981a0c Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Wed, 24 Nov 2010 17:55:43 +0100 Subject: Renamed msg named parameter to logmsg, as it describes the purpose of the message much better Added test for deletion of reflog file when the corresponding ref is deleted --- refs/reference.py | 8 ++++---- refs/symbolic.py | 32 ++++++++++++++++---------------- 2 files changed, 20 insertions(+), 20 deletions(-) (limited to 'refs') diff --git a/refs/reference.py b/refs/reference.py index c44ebf00..1a745ee9 100644 --- a/refs/reference.py +++ b/refs/reference.py @@ -37,17 +37,17 @@ class Reference(SymbolicReference, LazyMixin, Iterable): def __str__(self): return self.name - def set_object(self, object, msg = None): + def set_object(self, object, logmsg = None): """Special version which checks if the head-log needs an update as well""" oldbinsha = None - if msg is not None: + 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 #END handle message is set - super(Reference, self).set_object(object, msg) + super(Reference, self).set_object(object, logmsg) if oldbinsha is not None: # /* from refs.c in git-source @@ -62,7 +62,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, msg) + self.repo.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/refs/symbolic.py b/refs/symbolic.py index cdd6158a..f333bd46 100644 --- a/refs/symbolic.py +++ b/refs/symbolic.py @@ -172,7 +172,7 @@ class SymbolicReference(object): #END handle type return obj - def set_commit(self, commit, msg = None): + def set_commit(self, commit, logmsg = None): """As set_object, but restricts the type of object to be a Commit :raise ValueError: If commit is not a Commit object or doesn't point to a commit @@ -196,18 +196,18 @@ class SymbolicReference(object): #END handle raise # we leave strings to the rev-parse method below - self.set_object(commit, msg) + self.set_object(commit, logmsg) return self - def set_object(self, object, msg = None): + 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 msg: If not None, the message will be used in the reflog entry to be + :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""" @@ -223,10 +223,10 @@ class SymbolicReference(object): # END handle non-existing ones if is_detached: - return self.set_reference(object, msg) + return self.set_reference(object, logmsg) # set the commit on our reference - return self._get_reference().set_object(object, msg) + 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") @@ -240,7 +240,7 @@ class SymbolicReference(object): raise TypeError("%s is a detached symbolic reference as it points to %r" % (self, sha)) return self.from_path(self.repo, target_ref_path) - def set_reference(self, ref, msg = None): + def set_reference(self, ref, logmsg = None): """Set ourselves to the given ref. It will stay a symbol if the ref is a Reference. Otherwise an Object, given as Object instance or refspec, is assumed and if valid, will be set which effectively detaches the refererence if it was a purely @@ -249,7 +249,7 @@ class SymbolicReference(object): :param ref: SymbolicReference instance, Object instance 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 msg: If set to a string, the message will be used in the reflog. + :param logmsg: If set to a string, the message will be used in the reflog. Otherwise, a reflog entry is not written for the changed reference. The previous commit of the entry will be the commit we point to now. @@ -282,7 +282,7 @@ class SymbolicReference(object): #END verify type oldbinsha = None - if msg is not None: + if logmsg is not None: try: oldbinsha = self.commit.binsha except ValueError: @@ -299,8 +299,8 @@ class SymbolicReference(object): lfd.commit() # Adjust the reflog - if msg is not None: - self.log_append(oldbinsha, msg) + if logmsg is not None: + self.log_append(oldbinsha, logmsg) #END handle reflog return self @@ -426,7 +426,7 @@ class SymbolicReference(object): @classmethod - def _create(cls, repo, path, resolve, reference, force, msg=None): + def _create(cls, repo, path, resolve, reference, force, logmsg=None): """internal method used to create a new symbolic reference. If resolve is False, the reference will be taken as is, creating a proper symbolic reference. Otherwise it will be resolved to the @@ -452,11 +452,11 @@ class SymbolicReference(object): # END no force handling ref = cls(repo, full_ref_path) - ref.set_reference(target, msg) + ref.set_reference(target, logmsg) return ref @classmethod - def create(cls, repo, path, reference='HEAD', force=False, msg=None): + def create(cls, repo, path, reference='HEAD', force=False, logmsg=None): """Create a new symbolic reference, hence a reference pointing to another reference. :param repo: @@ -473,7 +473,7 @@ class SymbolicReference(object): if True, force creation even if a symbolic reference with that name already exists. Raise OSError otherwise - :param msg: + :param logmsg: If not None, the message to append to the reflog. Otherwise no reflog entry is written. @@ -484,7 +484,7 @@ class SymbolicReference(object): already exists. :note: This does not alter the current HEAD, index or Working Tree""" - return cls._create(repo, path, cls._resolve_ref_on_create, reference, force, msg) + return cls._create(repo, path, cls._resolve_ref_on_create, reference, force, logmsg) def rename(self, new_path, force=False): """Rename self to a new path -- cgit v1.2.3 From 98a313305f0d554a179b93695d333199feb5266c Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Wed, 24 Nov 2010 19:36:34 +0100 Subject: RefLog: added entry_at method, which is a faster way of reading single entries, including test --- refs/log.py | 31 +++++++++++++++++++++++++++++++ refs/symbolic.py | 9 +++++++++ 2 files changed, 40 insertions(+) (limited to 'refs') diff --git a/refs/log.py b/refs/log.py index 129803b4..6c734ad4 100644 --- a/refs/log.py +++ b/refs/log.py @@ -173,6 +173,37 @@ class RefLog(list, Serializable): return yield new_entry(line.strip()) #END endless loop + + @classmethod + def entry_at(cls, filepath, index): + """:return: RefLogEntry at the given index + :param filepath: full path to the index file from which to read the entry + :param index: python list compatible index, i.e. it may be negative to + specifiy an entry counted from the end of the list + + :raise IndexError: If the entry didn't exist + .. note:: This method is faster as it only parses the entry at index, skipping + all other lines. Nonetheless, the whole file has to be read if + the index is negative + """ + fp = open(filepath, 'rb') + if index < 0: + return RefLogEntry.from_line(fp.readlines()[index].strip()) + else: + # read until index is reached + for i in xrange(index+1): + line = fp.readline() + if not line: + break + #END abort on eof + #END handle runup + + if i != index or not line: + raise IndexError + #END handle exception + + return RefLogEntry.from_line(line.strip()) + #END handle index def to_file(self, filepath): """Write the contents of the reflog instance to a file at the given filepath. diff --git a/refs/symbolic.py b/refs/symbolic.py index f333bd46..6ba8083f 100644 --- a/refs/symbolic.py +++ b/refs/symbolic.py @@ -354,6 +354,15 @@ class SymbolicReference(object): (newbinsha is None and self.commit.binsha) or newbinsha, message) + def log_entry(self, index): + """:return: RefLogEntry at the given index + :param index: python list compatible positive or negative index + + .. note:: This method must read part of the reflog during execution, hence + it should be used sparringly, or only if you need just one index. + In that case, it will be faster than the ``log()`` method""" + return RefLog.entry_at(RefLog.path(self), index) + @classmethod def to_full_path(cls, path): """ -- cgit v1.2.3 From 3203cd7629345d32806f470a308975076b2b4686 Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Wed, 24 Nov 2010 19:48:44 +0100 Subject: Fixed doc strings, improved error checking on RefLog.write method --- refs/log.py | 11 +++++++++-- refs/symbolic.py | 2 ++ 2 files changed, 11 insertions(+), 2 deletions(-) (limited to 'refs') diff --git a/refs/log.py b/refs/log.py index 6c734ad4..f49c07fd 100644 --- a/refs/log.py +++ b/refs/log.py @@ -182,6 +182,7 @@ class RefLog(list, Serializable): specifiy an entry counted from the end of the list :raise IndexError: If the entry didn't exist + .. note:: This method is faster as it only parses the entry at index, skipping all other lines. Nonetheless, the whole file has to be read if the index is negative @@ -224,6 +225,7 @@ class RefLog(list, Serializable): @classmethod def append_entry(cls, config_reader, filepath, oldbinsha, newbinsha, message): """Append a new log entry to the revlog at filepath. + :param config_reader: configuration reader of the repository - used to obtain user information. May be None :param filepath: full path to the log file @@ -255,8 +257,13 @@ class RefLog(list, Serializable): return entry def write(self): - """Write this instance's data to the file we are originating from""" - return self.to_file(self._path) + """Write this instance's data to the file we are originating from + :return: self""" + if self._path is None: + raise ValueError("Instance was not initialized with a path, use to_file(...) instead") + #END assert path + self.to_file(self._path) + return self #} END interface diff --git a/refs/symbolic.py b/refs/symbolic.py index 6ba8083f..9937cf0c 100644 --- a/refs/symbolic.py +++ b/refs/symbolic.py @@ -174,6 +174,7 @@ class SymbolicReference(object): def set_commit(self, commit, logmsg = None): """As set_object, but restricts the type of object to be a Commit + :raise ValueError: If commit is not a Commit object or doesn't point to a commit :return: self""" @@ -345,6 +346,7 @@ class SymbolicReference(object): def log_append(self, oldbinsha, message, newbinsha=None): """Append a logentry to the logfile of this ref + :param oldbinsha: binary sha this ref used to point to :param message: A message describing the change :param newbinsha: The sha the ref points to now. If None, our current commit sha -- cgit v1.2.3