|
@@ -1,6 +1,6 @@
|
|
|
# repo.py -- For dealing with git repositories.
|
|
|
# Copyright (C) 2007 James Westby <jw+debian@jameswestby.net>
|
|
|
-# Copyright (C) 2008-2009 Jelmer Vernooij <jelmer@samba.org>
|
|
|
+# Copyright (C) 2008-2013 Jelmer Vernooij <jelmer@samba.org>
|
|
|
#
|
|
|
# This program is free software; you can redistribute it and/or
|
|
|
# modify it under the terms of the GNU General Public License
|
|
@@ -38,39 +38,50 @@ from dulwich.errors import (
|
|
|
NotGitRepository,
|
|
|
NotTreeError,
|
|
|
NotTagError,
|
|
|
- PackedRefsException,
|
|
|
CommitError,
|
|
|
RefFormatError,
|
|
|
HookError,
|
|
|
)
|
|
|
from dulwich.file import (
|
|
|
- ensure_dir_exists,
|
|
|
GitFile,
|
|
|
)
|
|
|
from dulwich.object_store import (
|
|
|
DiskObjectStore,
|
|
|
MemoryObjectStore,
|
|
|
+ ObjectStoreGraphWalker,
|
|
|
)
|
|
|
from dulwich.objects import (
|
|
|
+ check_hexsha,
|
|
|
Blob,
|
|
|
Commit,
|
|
|
ShaFile,
|
|
|
Tag,
|
|
|
Tree,
|
|
|
- hex_to_sha,
|
|
|
)
|
|
|
|
|
|
from dulwich.hooks import (
|
|
|
PreCommitShellHook,
|
|
|
PostCommitShellHook,
|
|
|
CommitMsgShellHook,
|
|
|
-)
|
|
|
+ )
|
|
|
+
|
|
|
+from dulwich.refs import (
|
|
|
+ check_ref_format,
|
|
|
+ RefsContainer,
|
|
|
+ DictRefsContainer,
|
|
|
+ InfoRefsContainer,
|
|
|
+ DiskRefsContainer,
|
|
|
+ read_packed_refs,
|
|
|
+ read_packed_refs_with_peeled,
|
|
|
+ write_packed_refs,
|
|
|
+ SYMREF,
|
|
|
+ )
|
|
|
+
|
|
|
|
|
|
import warnings
|
|
|
|
|
|
|
|
|
OBJECTDIR = 'objects'
|
|
|
-SYMREF = 'ref: '
|
|
|
REFSDIR = 'refs'
|
|
|
REFSDIR_TAGS = 'tags'
|
|
|
REFSDIR_HEADS = 'heads'
|
|
@@ -86,710 +97,55 @@ BASE_DIRECTORIES = [
|
|
|
]
|
|
|
|
|
|
|
|
|
-def check_ref_format(refname):
|
|
|
- """Check if a refname is correctly formatted.
|
|
|
-
|
|
|
- Implements all the same rules as git-check-ref-format[1].
|
|
|
-
|
|
|
- [1] http://www.kernel.org/pub/software/scm/git/docs/git-check-ref-format.html
|
|
|
-
|
|
|
- :param refname: The refname to check
|
|
|
- :return: True if refname is valid, False otherwise
|
|
|
- """
|
|
|
- # These could be combined into one big expression, but are listed separately
|
|
|
- # to parallel [1].
|
|
|
- if '/.' in refname or refname.startswith('.'):
|
|
|
- return False
|
|
|
- if '/' not in refname:
|
|
|
- return False
|
|
|
- if '..' in refname:
|
|
|
- return False
|
|
|
- for c in refname:
|
|
|
- if ord(c) < 040 or c in '\177 ~^:?*[':
|
|
|
- return False
|
|
|
- if refname[-1] in '/.':
|
|
|
- return False
|
|
|
- if refname.endswith('.lock'):
|
|
|
- return False
|
|
|
- if '@{' in refname:
|
|
|
- return False
|
|
|
- if '\\' in refname:
|
|
|
- return False
|
|
|
- return True
|
|
|
-
|
|
|
-
|
|
|
-class RefsContainer(object):
|
|
|
- """A container for refs."""
|
|
|
-
|
|
|
- def set_ref(self, name, other):
|
|
|
- warnings.warn("RefsContainer.set_ref() is deprecated."
|
|
|
- "Use set_symblic_ref instead.",
|
|
|
- category=DeprecationWarning, stacklevel=2)
|
|
|
- return self.set_symbolic_ref(name, other)
|
|
|
-
|
|
|
- def set_symbolic_ref(self, name, other):
|
|
|
- """Make a ref point at another ref.
|
|
|
-
|
|
|
- :param name: Name of the ref to set
|
|
|
- :param other: Name of the ref to point at
|
|
|
- """
|
|
|
- raise NotImplementedError(self.set_symbolic_ref)
|
|
|
-
|
|
|
- def get_packed_refs(self):
|
|
|
- """Get contents of the packed-refs file.
|
|
|
-
|
|
|
- :return: Dictionary mapping ref names to SHA1s
|
|
|
-
|
|
|
- :note: Will return an empty dictionary when no packed-refs file is
|
|
|
- present.
|
|
|
- """
|
|
|
- raise NotImplementedError(self.get_packed_refs)
|
|
|
-
|
|
|
- def get_peeled(self, name):
|
|
|
- """Return the cached peeled value of a ref, if available.
|
|
|
-
|
|
|
- :param name: Name of the ref to peel
|
|
|
- :return: The peeled value of the ref. If the ref is known not point to a
|
|
|
- tag, this will be the SHA the ref refers to. If the ref may point to
|
|
|
- a tag, but no cached information is available, None is returned.
|
|
|
- """
|
|
|
- return None
|
|
|
-
|
|
|
- def import_refs(self, base, other):
|
|
|
- for name, value in other.iteritems():
|
|
|
- self["%s/%s" % (base, name)] = value
|
|
|
-
|
|
|
- def allkeys(self):
|
|
|
- """All refs present in this container."""
|
|
|
- raise NotImplementedError(self.allkeys)
|
|
|
-
|
|
|
- def keys(self, base=None):
|
|
|
- """Refs present in this container.
|
|
|
+def parse_graftpoints(graftpoints):
|
|
|
+ """Convert a list of graftpoints into a dict
|
|
|
|
|
|
- :param base: An optional base to return refs under.
|
|
|
- :return: An unsorted set of valid refs in this container, including
|
|
|
- packed refs.
|
|
|
- """
|
|
|
- if base is not None:
|
|
|
- return self.subkeys(base)
|
|
|
- else:
|
|
|
- return self.allkeys()
|
|
|
-
|
|
|
- def subkeys(self, base):
|
|
|
- """Refs present in this container under a base.
|
|
|
-
|
|
|
- :param base: The base to return refs under.
|
|
|
- :return: A set of valid refs in this container under the base; the base
|
|
|
- prefix is stripped from the ref names returned.
|
|
|
- """
|
|
|
- keys = set()
|
|
|
- base_len = len(base) + 1
|
|
|
- for refname in self.allkeys():
|
|
|
- if refname.startswith(base):
|
|
|
- keys.add(refname[base_len:])
|
|
|
- return keys
|
|
|
-
|
|
|
- def as_dict(self, base=None):
|
|
|
- """Return the contents of this container as a dictionary.
|
|
|
+ :param graftpoints: Iterator of graftpoint lines
|
|
|
|
|
|
- """
|
|
|
- ret = {}
|
|
|
- keys = self.keys(base)
|
|
|
- if base is None:
|
|
|
- base = ""
|
|
|
- for key in keys:
|
|
|
- try:
|
|
|
- ret[key] = self[("%s/%s" % (base, key)).strip("/")]
|
|
|
- except KeyError:
|
|
|
- continue # Unable to resolve
|
|
|
-
|
|
|
- return ret
|
|
|
-
|
|
|
- def _check_refname(self, name):
|
|
|
- """Ensure a refname is valid and lives in refs or is HEAD.
|
|
|
+ Each line is formatted as:
|
|
|
+ <commit sha1> <parent sha1> [<parent sha1>]*
|
|
|
|
|
|
- HEAD is not a valid refname according to git-check-ref-format, but this
|
|
|
- class needs to be able to touch HEAD. Also, check_ref_format expects
|
|
|
- refnames without the leading 'refs/', but this class requires that
|
|
|
- so it cannot touch anything outside the refs dir (or HEAD).
|
|
|
+ Resulting dictionary is:
|
|
|
+ <commit sha1>: [<parent sha1>*]
|
|
|
|
|
|
- :param name: The name of the reference.
|
|
|
- :raises KeyError: if a refname is not HEAD or is otherwise not valid.
|
|
|
- """
|
|
|
- if name in ('HEAD', 'refs/stash'):
|
|
|
- return
|
|
|
- if not name.startswith('refs/') or not check_ref_format(name[5:]):
|
|
|
- raise RefFormatError(name)
|
|
|
-
|
|
|
- def read_ref(self, refname):
|
|
|
- """Read a reference without following any references.
|
|
|
-
|
|
|
- :param refname: The name of the reference
|
|
|
- :return: The contents of the ref file, or None if it does
|
|
|
- not exist.
|
|
|
- """
|
|
|
- contents = self.read_loose_ref(refname)
|
|
|
- if not contents:
|
|
|
- contents = self.get_packed_refs().get(refname, None)
|
|
|
- return contents
|
|
|
-
|
|
|
- def read_loose_ref(self, name):
|
|
|
- """Read a loose reference and return its contents.
|
|
|
-
|
|
|
- :param name: the refname to read
|
|
|
- :return: The contents of the ref file, or None if it does
|
|
|
- not exist.
|
|
|
- """
|
|
|
- raise NotImplementedError(self.read_loose_ref)
|
|
|
-
|
|
|
- def _follow(self, name):
|
|
|
- """Follow a reference name.
|
|
|
-
|
|
|
- :return: a tuple of (refname, sha), where refname is the name of the
|
|
|
- last reference in the symbolic reference chain
|
|
|
- """
|
|
|
- contents = SYMREF + name
|
|
|
- depth = 0
|
|
|
- while contents.startswith(SYMREF):
|
|
|
- refname = contents[len(SYMREF):]
|
|
|
- contents = self.read_ref(refname)
|
|
|
- if not contents:
|
|
|
- break
|
|
|
- depth += 1
|
|
|
- if depth > 5:
|
|
|
- raise KeyError(name)
|
|
|
- return refname, contents
|
|
|
-
|
|
|
- def __contains__(self, refname):
|
|
|
- if self.read_ref(refname):
|
|
|
- return True
|
|
|
- return False
|
|
|
-
|
|
|
- def __getitem__(self, name):
|
|
|
- """Get the SHA1 for a reference name.
|
|
|
-
|
|
|
- This method follows all symbolic references.
|
|
|
- """
|
|
|
- _, sha = self._follow(name)
|
|
|
- if sha is None:
|
|
|
- raise KeyError(name)
|
|
|
- return sha
|
|
|
-
|
|
|
- def set_if_equals(self, name, old_ref, new_ref):
|
|
|
- """Set a refname to new_ref only if it currently equals old_ref.
|
|
|
-
|
|
|
- This method follows all symbolic references if applicable for the
|
|
|
- subclass, and can be used to perform an atomic compare-and-swap
|
|
|
- operation.
|
|
|
-
|
|
|
- :param name: The refname to set.
|
|
|
- :param old_ref: The old sha the refname must refer to, or None to set
|
|
|
- unconditionally.
|
|
|
- :param new_ref: The new sha the refname will refer to.
|
|
|
- :return: True if the set was successful, False otherwise.
|
|
|
- """
|
|
|
- raise NotImplementedError(self.set_if_equals)
|
|
|
-
|
|
|
- def add_if_new(self, name, ref):
|
|
|
- """Add a new reference only if it does not already exist."""
|
|
|
- raise NotImplementedError(self.add_if_new)
|
|
|
-
|
|
|
- def __setitem__(self, name, ref):
|
|
|
- """Set a reference name to point to the given SHA1.
|
|
|
-
|
|
|
- This method follows all symbolic references if applicable for the
|
|
|
- subclass.
|
|
|
-
|
|
|
- :note: This method unconditionally overwrites the contents of a
|
|
|
- reference. To update atomically only if the reference has not
|
|
|
- changed, use set_if_equals().
|
|
|
- :param name: The refname to set.
|
|
|
- :param ref: The new sha the refname will refer to.
|
|
|
- """
|
|
|
- self.set_if_equals(name, None, ref)
|
|
|
-
|
|
|
- def remove_if_equals(self, name, old_ref):
|
|
|
- """Remove a refname only if it currently equals old_ref.
|
|
|
-
|
|
|
- This method does not follow symbolic references, even if applicable for
|
|
|
- the subclass. It can be used to perform an atomic compare-and-delete
|
|
|
- operation.
|
|
|
-
|
|
|
- :param name: The refname to delete.
|
|
|
- :param old_ref: The old sha the refname must refer to, or None to delete
|
|
|
- unconditionally.
|
|
|
- :return: True if the delete was successful, False otherwise.
|
|
|
- """
|
|
|
- raise NotImplementedError(self.remove_if_equals)
|
|
|
-
|
|
|
- def __delitem__(self, name):
|
|
|
- """Remove a refname.
|
|
|
-
|
|
|
- This method does not follow symbolic references, even if applicable for
|
|
|
- the subclass.
|
|
|
-
|
|
|
- :note: This method unconditionally deletes the contents of a reference.
|
|
|
- To delete atomically only if the reference has not changed, use
|
|
|
- remove_if_equals().
|
|
|
-
|
|
|
- :param name: The refname to delete.
|
|
|
- """
|
|
|
- self.remove_if_equals(name, None)
|
|
|
-
|
|
|
-
|
|
|
-class DictRefsContainer(RefsContainer):
|
|
|
- """RefsContainer backed by a simple dict.
|
|
|
-
|
|
|
- This container does not support symbolic or packed references and is not
|
|
|
- threadsafe.
|
|
|
+ https://git.wiki.kernel.org/index.php/GraftPoint
|
|
|
"""
|
|
|
+ grafts = {}
|
|
|
+ for l in graftpoints:
|
|
|
+ raw_graft = l.split(None, 1)
|
|
|
|
|
|
- def __init__(self, refs):
|
|
|
- self._refs = refs
|
|
|
- self._peeled = {}
|
|
|
-
|
|
|
- def allkeys(self):
|
|
|
- return self._refs.keys()
|
|
|
-
|
|
|
- def read_loose_ref(self, name):
|
|
|
- return self._refs.get(name, None)
|
|
|
-
|
|
|
- def get_packed_refs(self):
|
|
|
- return {}
|
|
|
-
|
|
|
- def set_symbolic_ref(self, name, other):
|
|
|
- self._refs[name] = SYMREF + other
|
|
|
-
|
|
|
- def set_if_equals(self, name, old_ref, new_ref):
|
|
|
- if old_ref is not None and self._refs.get(name, None) != old_ref:
|
|
|
- return False
|
|
|
- realname, _ = self._follow(name)
|
|
|
- self._check_refname(realname)
|
|
|
- self._refs[realname] = new_ref
|
|
|
- return True
|
|
|
-
|
|
|
- def add_if_new(self, name, ref):
|
|
|
- if name in self._refs:
|
|
|
- return False
|
|
|
- self._refs[name] = ref
|
|
|
- return True
|
|
|
-
|
|
|
- def remove_if_equals(self, name, old_ref):
|
|
|
- if old_ref is not None and self._refs.get(name, None) != old_ref:
|
|
|
- return False
|
|
|
- del self._refs[name]
|
|
|
- return True
|
|
|
-
|
|
|
- def get_peeled(self, name):
|
|
|
- return self._peeled.get(name)
|
|
|
-
|
|
|
- def _update(self, refs):
|
|
|
- """Update multiple refs; intended only for testing."""
|
|
|
- # TODO(dborowitz): replace this with a public function that uses
|
|
|
- # set_if_equal.
|
|
|
- self._refs.update(refs)
|
|
|
-
|
|
|
- def _update_peeled(self, peeled):
|
|
|
- """Update cached peeled refs; intended only for testing."""
|
|
|
- self._peeled.update(peeled)
|
|
|
-
|
|
|
-
|
|
|
-class InfoRefsContainer(RefsContainer):
|
|
|
- """Refs container that reads refs from a info/refs file."""
|
|
|
-
|
|
|
- def __init__(self, f):
|
|
|
- self._refs = {}
|
|
|
- self._peeled = {}
|
|
|
- for l in f.readlines():
|
|
|
- sha, name = l.rstrip("\n").split("\t")
|
|
|
- if name.endswith("^{}"):
|
|
|
- name = name[:-3]
|
|
|
- if not check_ref_format(name):
|
|
|
- raise ValueError("invalid ref name '%s'" % name)
|
|
|
- self._peeled[name] = sha
|
|
|
- else:
|
|
|
- if not check_ref_format(name):
|
|
|
- raise ValueError("invalid ref name '%s'" % name)
|
|
|
- self._refs[name] = sha
|
|
|
-
|
|
|
- def allkeys(self):
|
|
|
- return self._refs.keys()
|
|
|
-
|
|
|
- def read_loose_ref(self, name):
|
|
|
- return self._refs.get(name, None)
|
|
|
-
|
|
|
- def get_packed_refs(self):
|
|
|
- return {}
|
|
|
-
|
|
|
- def get_peeled(self, name):
|
|
|
- try:
|
|
|
- return self._peeled[name]
|
|
|
- except KeyError:
|
|
|
- return self._refs[name]
|
|
|
-
|
|
|
-
|
|
|
-class DiskRefsContainer(RefsContainer):
|
|
|
- """Refs container that reads refs from disk."""
|
|
|
-
|
|
|
- def __init__(self, path):
|
|
|
- self.path = path
|
|
|
- self._packed_refs = None
|
|
|
- self._peeled_refs = None
|
|
|
-
|
|
|
- def __repr__(self):
|
|
|
- return "%s(%r)" % (self.__class__.__name__, self.path)
|
|
|
-
|
|
|
- def subkeys(self, base):
|
|
|
- keys = set()
|
|
|
- path = self.refpath(base)
|
|
|
- for root, dirs, files in os.walk(path):
|
|
|
- dir = root[len(path):].strip(os.path.sep).replace(os.path.sep, "/")
|
|
|
- for filename in files:
|
|
|
- refname = ("%s/%s" % (dir, filename)).strip("/")
|
|
|
- # check_ref_format requires at least one /, so we prepend the
|
|
|
- # base before calling it.
|
|
|
- if check_ref_format("%s/%s" % (base, refname)):
|
|
|
- keys.add(refname)
|
|
|
- for key in self.get_packed_refs():
|
|
|
- if key.startswith(base):
|
|
|
- keys.add(key[len(base):].strip("/"))
|
|
|
- return keys
|
|
|
-
|
|
|
- def allkeys(self):
|
|
|
- keys = set()
|
|
|
- if os.path.exists(self.refpath("HEAD")):
|
|
|
- keys.add("HEAD")
|
|
|
- path = self.refpath("")
|
|
|
- for root, dirs, files in os.walk(self.refpath("refs")):
|
|
|
- dir = root[len(path):].strip(os.path.sep).replace(os.path.sep, "/")
|
|
|
- for filename in files:
|
|
|
- refname = ("%s/%s" % (dir, filename)).strip("/")
|
|
|
- if check_ref_format(refname):
|
|
|
- keys.add(refname)
|
|
|
- keys.update(self.get_packed_refs())
|
|
|
- return keys
|
|
|
-
|
|
|
- def refpath(self, name):
|
|
|
- """Return the disk path of a ref.
|
|
|
-
|
|
|
- """
|
|
|
- if os.path.sep != "/":
|
|
|
- name = name.replace("/", os.path.sep)
|
|
|
- return os.path.join(self.path, name)
|
|
|
-
|
|
|
- def get_packed_refs(self):
|
|
|
- """Get contents of the packed-refs file.
|
|
|
-
|
|
|
- :return: Dictionary mapping ref names to SHA1s
|
|
|
-
|
|
|
- :note: Will return an empty dictionary when no packed-refs file is
|
|
|
- present.
|
|
|
- """
|
|
|
- # TODO: invalidate the cache on repacking
|
|
|
- if self._packed_refs is None:
|
|
|
- # set both to empty because we want _peeled_refs to be
|
|
|
- # None if and only if _packed_refs is also None.
|
|
|
- self._packed_refs = {}
|
|
|
- self._peeled_refs = {}
|
|
|
- path = os.path.join(self.path, 'packed-refs')
|
|
|
- try:
|
|
|
- f = GitFile(path, 'rb')
|
|
|
- except IOError, e:
|
|
|
- if e.errno == errno.ENOENT:
|
|
|
- return {}
|
|
|
- raise
|
|
|
- try:
|
|
|
- first_line = iter(f).next().rstrip()
|
|
|
- if (first_line.startswith("# pack-refs") and " peeled" in
|
|
|
- first_line):
|
|
|
- for sha, name, peeled in read_packed_refs_with_peeled(f):
|
|
|
- self._packed_refs[name] = sha
|
|
|
- if peeled:
|
|
|
- self._peeled_refs[name] = peeled
|
|
|
- else:
|
|
|
- f.seek(0)
|
|
|
- for sha, name in read_packed_refs(f):
|
|
|
- self._packed_refs[name] = sha
|
|
|
- finally:
|
|
|
- f.close()
|
|
|
- return self._packed_refs
|
|
|
-
|
|
|
- def get_peeled(self, name):
|
|
|
- """Return the cached peeled value of a ref, if available.
|
|
|
-
|
|
|
- :param name: Name of the ref to peel
|
|
|
- :return: The peeled value of the ref. If the ref is known not point to a
|
|
|
- tag, this will be the SHA the ref refers to. If the ref may point to
|
|
|
- a tag, but no cached information is available, None is returned.
|
|
|
- """
|
|
|
- self.get_packed_refs()
|
|
|
- if self._peeled_refs is None or name not in self._packed_refs:
|
|
|
- # No cache: no peeled refs were read, or this ref is loose
|
|
|
- return None
|
|
|
- if name in self._peeled_refs:
|
|
|
- return self._peeled_refs[name]
|
|
|
+ commit = raw_graft[0]
|
|
|
+ if len(raw_graft) == 2:
|
|
|
+ parents = raw_graft[1].split()
|
|
|
else:
|
|
|
- # Known not peelable
|
|
|
- return self[name]
|
|
|
-
|
|
|
- def read_loose_ref(self, name):
|
|
|
- """Read a reference file and return its contents.
|
|
|
-
|
|
|
- If the reference file a symbolic reference, only read the first line of
|
|
|
- the file. Otherwise, only read the first 40 bytes.
|
|
|
-
|
|
|
- :param name: the refname to read, relative to refpath
|
|
|
- :return: The contents of the ref file, or None if the file does not
|
|
|
- exist.
|
|
|
- :raises IOError: if any other error occurs
|
|
|
- """
|
|
|
- filename = self.refpath(name)
|
|
|
- try:
|
|
|
- f = GitFile(filename, 'rb')
|
|
|
- try:
|
|
|
- header = f.read(len(SYMREF))
|
|
|
- if header == SYMREF:
|
|
|
- # Read only the first line
|
|
|
- return header + iter(f).next().rstrip("\r\n")
|
|
|
- else:
|
|
|
- # Read only the first 40 bytes
|
|
|
- return header + f.read(40 - len(SYMREF))
|
|
|
- finally:
|
|
|
- f.close()
|
|
|
- except IOError, e:
|
|
|
- if e.errno == errno.ENOENT:
|
|
|
- return None
|
|
|
- raise
|
|
|
-
|
|
|
- def _remove_packed_ref(self, name):
|
|
|
- if self._packed_refs is None:
|
|
|
- return
|
|
|
- filename = os.path.join(self.path, 'packed-refs')
|
|
|
- # reread cached refs from disk, while holding the lock
|
|
|
- f = GitFile(filename, 'wb')
|
|
|
- try:
|
|
|
- self._packed_refs = None
|
|
|
- self.get_packed_refs()
|
|
|
-
|
|
|
- if name not in self._packed_refs:
|
|
|
- return
|
|
|
-
|
|
|
- del self._packed_refs[name]
|
|
|
- if name in self._peeled_refs:
|
|
|
- del self._peeled_refs[name]
|
|
|
- write_packed_refs(f, self._packed_refs, self._peeled_refs)
|
|
|
- f.close()
|
|
|
- finally:
|
|
|
- f.abort()
|
|
|
+ parents = []
|
|
|
|
|
|
- def set_symbolic_ref(self, name, other):
|
|
|
- """Make a ref point at another ref.
|
|
|
+ for sha in [commit] + parents:
|
|
|
+ check_hexsha(sha, 'Invalid graftpoint')
|
|
|
|
|
|
- :param name: Name of the ref to set
|
|
|
- :param other: Name of the ref to point at
|
|
|
- """
|
|
|
- self._check_refname(name)
|
|
|
- self._check_refname(other)
|
|
|
- filename = self.refpath(name)
|
|
|
- try:
|
|
|
- f = GitFile(filename, 'wb')
|
|
|
- try:
|
|
|
- f.write(SYMREF + other + '\n')
|
|
|
- except (IOError, OSError):
|
|
|
- f.abort()
|
|
|
- raise
|
|
|
- finally:
|
|
|
- f.close()
|
|
|
+ grafts[commit] = parents
|
|
|
+ return grafts
|
|
|
|
|
|
- def set_if_equals(self, name, old_ref, new_ref):
|
|
|
- """Set a refname to new_ref only if it currently equals old_ref.
|
|
|
|
|
|
- This method follows all symbolic references, and can be used to perform
|
|
|
- an atomic compare-and-swap operation.
|
|
|
+def serialize_graftpoints(graftpoints):
|
|
|
+ """Convert a dictionary of grafts into string
|
|
|
|
|
|
- :param name: The refname to set.
|
|
|
- :param old_ref: The old sha the refname must refer to, or None to set
|
|
|
- unconditionally.
|
|
|
- :param new_ref: The new sha the refname will refer to.
|
|
|
- :return: True if the set was successful, False otherwise.
|
|
|
- """
|
|
|
- self._check_refname(name)
|
|
|
- try:
|
|
|
- realname, _ = self._follow(name)
|
|
|
- except KeyError:
|
|
|
- realname = name
|
|
|
- filename = self.refpath(realname)
|
|
|
- ensure_dir_exists(os.path.dirname(filename))
|
|
|
- f = GitFile(filename, 'wb')
|
|
|
- try:
|
|
|
- if old_ref is not None:
|
|
|
- try:
|
|
|
- # read again while holding the lock
|
|
|
- orig_ref = self.read_loose_ref(realname)
|
|
|
- if orig_ref is None:
|
|
|
- orig_ref = self.get_packed_refs().get(realname, None)
|
|
|
- if orig_ref != old_ref:
|
|
|
- f.abort()
|
|
|
- return False
|
|
|
- except (OSError, IOError):
|
|
|
- f.abort()
|
|
|
- raise
|
|
|
- try:
|
|
|
- f.write(new_ref + "\n")
|
|
|
- except (OSError, IOError):
|
|
|
- f.abort()
|
|
|
- raise
|
|
|
- finally:
|
|
|
- f.close()
|
|
|
- return True
|
|
|
+ The graft dictionary is:
|
|
|
+ <commit sha1>: [<parent sha1>*]
|
|
|
|
|
|
- def add_if_new(self, name, ref):
|
|
|
- """Add a new reference only if it does not already exist.
|
|
|
+ Each line is formatted as:
|
|
|
+ <commit sha1> <parent sha1> [<parent sha1>]*
|
|
|
|
|
|
- This method follows symrefs, and only ensures that the last ref in the
|
|
|
- chain does not exist.
|
|
|
+ https://git.wiki.kernel.org/index.php/GraftPoint
|
|
|
|
|
|
- :param name: The refname to set.
|
|
|
- :param ref: The new sha the refname will refer to.
|
|
|
- :return: True if the add was successful, False otherwise.
|
|
|
- """
|
|
|
- try:
|
|
|
- realname, contents = self._follow(name)
|
|
|
- if contents is not None:
|
|
|
- return False
|
|
|
- except KeyError:
|
|
|
- realname = name
|
|
|
- self._check_refname(realname)
|
|
|
- filename = self.refpath(realname)
|
|
|
- ensure_dir_exists(os.path.dirname(filename))
|
|
|
- f = GitFile(filename, 'wb')
|
|
|
- try:
|
|
|
- if os.path.exists(filename) or name in self.get_packed_refs():
|
|
|
- f.abort()
|
|
|
- return False
|
|
|
- try:
|
|
|
- f.write(ref + "\n")
|
|
|
- except (OSError, IOError):
|
|
|
- f.abort()
|
|
|
- raise
|
|
|
- finally:
|
|
|
- f.close()
|
|
|
- return True
|
|
|
-
|
|
|
- def remove_if_equals(self, name, old_ref):
|
|
|
- """Remove a refname only if it currently equals old_ref.
|
|
|
-
|
|
|
- This method does not follow symbolic references. It can be used to
|
|
|
- perform an atomic compare-and-delete operation.
|
|
|
-
|
|
|
- :param name: The refname to delete.
|
|
|
- :param old_ref: The old sha the refname must refer to, or None to delete
|
|
|
- unconditionally.
|
|
|
- :return: True if the delete was successful, False otherwise.
|
|
|
- """
|
|
|
- self._check_refname(name)
|
|
|
- filename = self.refpath(name)
|
|
|
- ensure_dir_exists(os.path.dirname(filename))
|
|
|
- f = GitFile(filename, 'wb')
|
|
|
- try:
|
|
|
- if old_ref is not None:
|
|
|
- orig_ref = self.read_loose_ref(name)
|
|
|
- if orig_ref is None:
|
|
|
- orig_ref = self.get_packed_refs().get(name, None)
|
|
|
- if orig_ref != old_ref:
|
|
|
- return False
|
|
|
- # may only be packed
|
|
|
- try:
|
|
|
- os.remove(filename)
|
|
|
- except OSError, e:
|
|
|
- if e.errno != errno.ENOENT:
|
|
|
- raise
|
|
|
- self._remove_packed_ref(name)
|
|
|
- finally:
|
|
|
- # never write, we just wanted the lock
|
|
|
- f.abort()
|
|
|
- return True
|
|
|
-
|
|
|
-
|
|
|
-def _split_ref_line(line):
|
|
|
- """Split a single ref line into a tuple of SHA1 and name."""
|
|
|
- fields = line.rstrip("\n").split(" ")
|
|
|
- if len(fields) != 2:
|
|
|
- raise PackedRefsException("invalid ref line '%s'" % line)
|
|
|
- sha, name = fields
|
|
|
- try:
|
|
|
- hex_to_sha(sha)
|
|
|
- except (AssertionError, TypeError), e:
|
|
|
- raise PackedRefsException(e)
|
|
|
- if not check_ref_format(name):
|
|
|
- raise PackedRefsException("invalid ref name '%s'" % name)
|
|
|
- return (sha, name)
|
|
|
-
|
|
|
-
|
|
|
-def read_packed_refs(f):
|
|
|
- """Read a packed refs file.
|
|
|
-
|
|
|
- :param f: file-like object to read from
|
|
|
- :return: Iterator over tuples with SHA1s and ref names.
|
|
|
"""
|
|
|
- for l in f:
|
|
|
- if l[0] == "#":
|
|
|
- # Comment
|
|
|
- continue
|
|
|
- if l[0] == "^":
|
|
|
- raise PackedRefsException(
|
|
|
- "found peeled ref in packed-refs without peeled")
|
|
|
- yield _split_ref_line(l)
|
|
|
-
|
|
|
-
|
|
|
-def read_packed_refs_with_peeled(f):
|
|
|
- """Read a packed refs file including peeled refs.
|
|
|
-
|
|
|
- Assumes the "# pack-refs with: peeled" line was already read. Yields tuples
|
|
|
- with ref names, SHA1s, and peeled SHA1s (or None).
|
|
|
-
|
|
|
- :param f: file-like object to read from, seek'ed to the second line
|
|
|
- """
|
|
|
- last = None
|
|
|
- for l in f:
|
|
|
- if l[0] == "#":
|
|
|
- continue
|
|
|
- l = l.rstrip("\r\n")
|
|
|
- if l[0] == "^":
|
|
|
- if not last:
|
|
|
- raise PackedRefsException("unexpected peeled ref line")
|
|
|
- try:
|
|
|
- hex_to_sha(l[1:])
|
|
|
- except (AssertionError, TypeError), e:
|
|
|
- raise PackedRefsException(e)
|
|
|
- sha, name = _split_ref_line(last)
|
|
|
- last = None
|
|
|
- yield (sha, name, l[1:])
|
|
|
+ graft_lines = []
|
|
|
+ for commit, parents in graftpoints.iteritems():
|
|
|
+ if parents:
|
|
|
+ graft_lines.append('%s %s' % (commit, ' '.join(parents)))
|
|
|
else:
|
|
|
- if last:
|
|
|
- sha, name = _split_ref_line(last)
|
|
|
- yield (sha, name, None)
|
|
|
- last = l
|
|
|
- if last:
|
|
|
- sha, name = _split_ref_line(last)
|
|
|
- yield (sha, name, None)
|
|
|
-
|
|
|
-
|
|
|
-def write_packed_refs(f, packed_refs, peeled_refs=None):
|
|
|
- """Write a packed refs file.
|
|
|
-
|
|
|
- :param f: empty file-like object to write to
|
|
|
- :param packed_refs: dict of refname to sha of packed refs to write
|
|
|
- :param peeled_refs: dict of refname to peeled value of sha
|
|
|
- """
|
|
|
- if peeled_refs is None:
|
|
|
- peeled_refs = {}
|
|
|
- else:
|
|
|
- f.write('# pack-refs with: peeled\n')
|
|
|
- for refname in sorted(packed_refs.iterkeys()):
|
|
|
- f.write('%s %s\n' % (packed_refs[refname], refname))
|
|
|
- if refname in peeled_refs:
|
|
|
- f.write('^%s\n' % peeled_refs[refname])
|
|
|
+ graft_lines.append(commit)
|
|
|
+ return '\n'.join(graft_lines)
|
|
|
|
|
|
|
|
|
class BaseRepo(object):
|
|
@@ -813,6 +169,7 @@ class BaseRepo(object):
|
|
|
self.object_store = object_store
|
|
|
self.refs = refs
|
|
|
|
|
|
+ self._graftpoints = {}
|
|
|
self.hooks = {}
|
|
|
|
|
|
def _init_files(self, bare):
|
|
@@ -864,9 +221,10 @@ class BaseRepo(object):
|
|
|
:param determine_wants: Optional function to determine what refs to
|
|
|
fetch.
|
|
|
:param progress: Optional progress function
|
|
|
+ :return: The local refs
|
|
|
"""
|
|
|
if determine_wants is None:
|
|
|
- determine_wants = lambda heads: heads.values()
|
|
|
+ determine_wants = target.object_store.determine_wants_all
|
|
|
target.object_store.add_objects(
|
|
|
self.fetch_objects(determine_wants, target.get_graph_walker(),
|
|
|
progress))
|
|
@@ -910,7 +268,7 @@ class BaseRepo(object):
|
|
|
"""
|
|
|
if heads is None:
|
|
|
heads = self.refs.as_dict('refs/heads').values()
|
|
|
- return self.object_store.get_graph_walker(heads)
|
|
|
+ return ObjectStoreGraphWalker(heads, self.get_parents)
|
|
|
|
|
|
def ref(self, name):
|
|
|
"""Return the SHA1 a ref is pointing to.
|
|
@@ -919,6 +277,9 @@ class BaseRepo(object):
|
|
|
:raise KeyError: when the ref (or the one it points to) does not exist
|
|
|
:return: SHA1 it is pointing at
|
|
|
"""
|
|
|
+ warnings.warn(
|
|
|
+ "Repo.ref(name) is deprecated. Use Repo.refs[name] instead.",
|
|
|
+ category=DeprecationWarning, stacklevel=2)
|
|
|
return self.refs[name]
|
|
|
|
|
|
def get_refs(self):
|
|
@@ -958,13 +319,23 @@ class BaseRepo(object):
|
|
|
"""
|
|
|
return self.object_store[sha]
|
|
|
|
|
|
- def get_parents(self, sha):
|
|
|
+ def get_parents(self, sha, commit=None):
|
|
|
"""Retrieve the parents of a specific commit.
|
|
|
|
|
|
+ If the specific commit is a graftpoint, the graft parents
|
|
|
+ will be returned instead.
|
|
|
+
|
|
|
:param sha: SHA of the commit for which to retrieve the parents
|
|
|
+ :param commit: Optional commit matching the sha
|
|
|
:return: List of parents
|
|
|
"""
|
|
|
- return self.commit(sha).parents
|
|
|
+
|
|
|
+ try:
|
|
|
+ return self._graftpoints[sha]
|
|
|
+ except KeyError:
|
|
|
+ if commit is None:
|
|
|
+ commit = self[sha]
|
|
|
+ return commit.parents
|
|
|
|
|
|
def get_config(self):
|
|
|
"""Retrieve the config object.
|
|
@@ -981,6 +352,13 @@ class BaseRepo(object):
|
|
|
"""
|
|
|
raise NotImplementedError(self.get_description)
|
|
|
|
|
|
+ def set_description(self, description):
|
|
|
+ """Set the description for this repository.
|
|
|
+
|
|
|
+ :param description: Text to set as description for this repository.
|
|
|
+ """
|
|
|
+ raise NotImplementedError(self.set_description)
|
|
|
+
|
|
|
def get_config_stack(self):
|
|
|
"""Return a config stack for this repository.
|
|
|
|
|
@@ -1085,6 +463,9 @@ class BaseRepo(object):
|
|
|
include = [self.head()]
|
|
|
if isinstance(include, str):
|
|
|
include = [include]
|
|
|
+
|
|
|
+ kwargs['get_parents'] = lambda commit: self.get_parents(commit.id, commit)
|
|
|
+
|
|
|
return Walker(self.object_store, include, *args, **kwargs)
|
|
|
|
|
|
def revision_history(self, head):
|
|
@@ -1162,6 +543,27 @@ class BaseRepo(object):
|
|
|
config.get(("user", ), "name"),
|
|
|
config.get(("user", ), "email"))
|
|
|
|
|
|
+ def _add_graftpoints(self, updated_graftpoints):
|
|
|
+ """Add or modify graftpoints
|
|
|
+
|
|
|
+ :param updated_graftpoints: Dict of commit shas to list of parent shas
|
|
|
+ """
|
|
|
+
|
|
|
+ # Simple validation
|
|
|
+ for commit, parents in updated_graftpoints.iteritems():
|
|
|
+ for sha in [commit] + parents:
|
|
|
+ check_hexsha(sha, 'Invalid graftpoint')
|
|
|
+
|
|
|
+ self._graftpoints.update(updated_graftpoints)
|
|
|
+
|
|
|
+ def _remove_graftpoints(self, to_remove=[]):
|
|
|
+ """Remove graftpoints
|
|
|
+
|
|
|
+ :param to_remove: List of commit shas
|
|
|
+ """
|
|
|
+ for sha in to_remove:
|
|
|
+ del self._graftpoints[sha]
|
|
|
+
|
|
|
def do_commit(self, message=None, committer=None,
|
|
|
author=None, commit_timestamp=None,
|
|
|
commit_timezone=None, author_timestamp=None,
|
|
@@ -1205,9 +607,12 @@ class BaseRepo(object):
|
|
|
# FIXME: Read merge heads from .git/MERGE_HEADS
|
|
|
merge_heads = []
|
|
|
if committer is None:
|
|
|
+ # FIXME: Support GIT_COMMITTER_NAME/GIT_COMMITTER_EMAIL environment
|
|
|
+ # variables
|
|
|
committer = self._get_user_identity()
|
|
|
c.committer = committer
|
|
|
if commit_timestamp is None:
|
|
|
+ # FIXME: Support GIT_COMMITTER_DATE environment variable
|
|
|
commit_timestamp = time.time()
|
|
|
c.commit_time = int(commit_timestamp)
|
|
|
if commit_timezone is None:
|
|
@@ -1215,9 +620,12 @@ class BaseRepo(object):
|
|
|
commit_timezone = 0
|
|
|
c.commit_timezone = commit_timezone
|
|
|
if author is None:
|
|
|
+ # FIXME: Support GIT_AUTHOR_NAME/GIT_AUTHOR_EMAIL environment
|
|
|
+ # variables
|
|
|
author = committer
|
|
|
c.author = author
|
|
|
if author_timestamp is None:
|
|
|
+ # FIXME: Support GIT_AUTHOR_DATE environment variable
|
|
|
author_timestamp = commit_timestamp
|
|
|
c.author_time = int(author_timestamp)
|
|
|
if author_timezone is None:
|
|
@@ -1298,6 +706,10 @@ class Repo(BaseRepo):
|
|
|
refs = DiskRefsContainer(self.controldir())
|
|
|
BaseRepo.__init__(self, object_store, refs)
|
|
|
|
|
|
+ graft_file = self.get_named_file(os.path.join("info", "grafts"))
|
|
|
+ if graft_file:
|
|
|
+ self._graftpoints = parse_graftpoints(graft_file)
|
|
|
+
|
|
|
self.hooks['pre-commit'] = PreCommitShellHook(self.controldir())
|
|
|
self.hooks['commit-msg'] = CommitMsgShellHook(self.controldir())
|
|
|
self.hooks['post-commit'] = PostCommitShellHook(self.controldir())
|
|
@@ -1473,6 +885,19 @@ class Repo(BaseRepo):
|
|
|
def __repr__(self):
|
|
|
return "<Repo at %r>" % self.path
|
|
|
|
|
|
+ def set_description(self, description):
|
|
|
+ """Set the description for this repository.
|
|
|
+
|
|
|
+ :param description: Text to set as description for this repository.
|
|
|
+ """
|
|
|
+
|
|
|
+ path = os.path.join(self._controldir, 'description')
|
|
|
+ f = open(path, 'w')
|
|
|
+ try:
|
|
|
+ f.write(description)
|
|
|
+ finally:
|
|
|
+ f.close()
|
|
|
+
|
|
|
@classmethod
|
|
|
def _init_maybe_bare(cls, path, bare):
|
|
|
for d in BASE_DIRECTORIES:
|