|
|
|
|
|
|
|
"""Some ref-based objects. |
|
|
|
Note the distinction between the :class:`HEAD` and :class:`Head` classes. |
|
""" |
|
|
|
__all__ = ["HEAD", "Head"] |
|
|
|
from git.config import GitConfigParser, SectionConstraint |
|
from git.exc import GitCommandError |
|
from git.util import join_path |
|
|
|
from .reference import Reference |
|
from .symbolic import SymbolicReference |
|
|
|
|
|
|
|
from typing import Any, Sequence, TYPE_CHECKING, Union |
|
|
|
from git.types import Commit_ish, PathLike |
|
|
|
if TYPE_CHECKING: |
|
from git.objects import Commit |
|
from git.refs import RemoteReference |
|
from git.repo import Repo |
|
|
|
|
|
|
|
|
|
def strip_quotes(string: str) -> str: |
|
if string.startswith('"') and string.endswith('"'): |
|
return string[1:-1] |
|
return string |
|
|
|
|
|
class HEAD(SymbolicReference): |
|
"""Special case of a :class:`~git.refs.symbolic.SymbolicReference` representing the |
|
repository's HEAD reference.""" |
|
|
|
_HEAD_NAME = "HEAD" |
|
_ORIG_HEAD_NAME = "ORIG_HEAD" |
|
|
|
__slots__ = () |
|
|
|
|
|
commit: "Commit" |
|
|
|
def __init__(self, repo: "Repo", path: PathLike = _HEAD_NAME) -> None: |
|
if path != self._HEAD_NAME: |
|
raise ValueError("HEAD instance must point to %r, got %r" % (self._HEAD_NAME, path)) |
|
super().__init__(repo, path) |
|
|
|
def orig_head(self) -> SymbolicReference: |
|
""" |
|
:return: |
|
:class:`~git.refs.symbolic.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 reset( |
|
self, |
|
commit: Union[Commit_ish, SymbolicReference, str] = "HEAD", |
|
index: bool = True, |
|
working_tree: bool = False, |
|
paths: Union[PathLike, Sequence[PathLike], None] = None, |
|
**kwargs: Any, |
|
) -> "HEAD": |
|
"""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: |
|
:class:`~git.objects.commit.Commit`, :class:`~git.refs.reference.Reference`, |
|
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 :manpage:`git-reset(1)`. |
|
|
|
:return: |
|
self |
|
""" |
|
mode: Union[str, None] |
|
mode = "--soft" |
|
if index: |
|
mode = "--mixed" |
|
|
|
|
|
|
|
if paths: |
|
mode = None |
|
|
|
|
|
|
|
if working_tree: |
|
mode = "--hard" |
|
if not index: |
|
raise ValueError("Cannot reset the working tree if the index is not reset as well") |
|
|
|
|
|
|
|
try: |
|
self.repo.git.reset(mode, commit, "--", paths, **kwargs) |
|
except GitCommandError as e: |
|
|
|
|
|
if e.status != 1: |
|
raise |
|
|
|
|
|
return self |
|
|
|
|
|
class Head(Reference): |
|
"""A Head is a named reference to a :class:`~git.objects.commit.Commit`. Every Head |
|
instance contains a name and a :class:`~git.objects.commit.Commit` object. |
|
|
|
Examples:: |
|
|
|
>>> repo = Repo("/path/to/repo") |
|
>>> head = repo.heads[0] |
|
|
|
>>> head.name |
|
'master' |
|
|
|
>>> head.commit |
|
<git.Commit "1c09f116cbc2cb4100fb6935bb162daa4723f455"> |
|
|
|
>>> head.commit.hexsha |
|
'1c09f116cbc2cb4100fb6935bb162daa4723f455' |
|
""" |
|
|
|
_common_path_default = "refs/heads" |
|
k_config_remote = "remote" |
|
k_config_remote_ref = "merge" |
|
|
|
@classmethod |
|
def delete(cls, repo: "Repo", *heads: "Union[Head, str]", force: bool = False, **kwargs: Any) -> None: |
|
"""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``. |
|
""" |
|
flag = "-d" |
|
if force: |
|
flag = "-D" |
|
repo.git.branch(flag, *heads) |
|
|
|
def set_tracking_branch(self, remote_reference: Union["RemoteReference", None]) -> "Head": |
|
"""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 |
|
""" |
|
from .remote import RemoteReference |
|
|
|
if remote_reference is not None and not isinstance(remote_reference, RemoteReference): |
|
raise ValueError("Incorrect parameter type: %r" % remote_reference) |
|
|
|
|
|
with self.config_writer() as 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() |
|
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), |
|
) |
|
|
|
return self |
|
|
|
def tracking_branch(self) -> Union["RemoteReference", None]: |
|
""" |
|
:return: |
|
The remote reference we are tracking, or ``None`` if we are not a tracking |
|
branch. |
|
""" |
|
from .remote import RemoteReference |
|
|
|
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(strip_quotes(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) |
|
|
|
|
|
|
|
return None |
|
|
|
def rename(self, new_path: PathLike, force: bool = False) -> "Head": |
|
"""Rename self to a new path. |
|
|
|
:param new_path: |
|
Either a simple name or a path, e.g. ``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: bool = False, **kwargs: Any) -> Union["HEAD", "Head"]: |
|
"""Check out 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``, :exc:`~git.exc.GitCommandError` will be raised in that |
|
situation. |
|
|
|
:param kwargs: |
|
Additional keyword arguments to be passed to git checkout, e.g. |
|
``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. |
|
If there is no active branch, as the HEAD is now detached, the HEAD |
|
reference will be returned instead. |
|
|
|
: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. |
|
""" |
|
kwargs["f"] = force |
|
if kwargs["f"] is False: |
|
kwargs.pop("f") |
|
|
|
self.repo.git.checkout(self, **kwargs) |
|
if self.repo.head.is_detached: |
|
return self.repo.head |
|
else: |
|
return self.repo.active_branch |
|
|
|
|
|
def _config_parser(self, read_only: bool) -> SectionConstraint[GitConfigParser]: |
|
if read_only: |
|
parser = self.repo.config_reader() |
|
else: |
|
parser = self.repo.config_writer() |
|
|
|
|
|
return SectionConstraint(parser, 'branch "%s"' % self.name) |
|
|
|
def config_reader(self) -> SectionConstraint[GitConfigParser]: |
|
""" |
|
:return: |
|
A configuration parser instance constrained to only read this instance's |
|
values. |
|
""" |
|
return self._config_parser(read_only=True) |
|
|
|
def config_writer(self) -> SectionConstraint[GitConfigParser]: |
|
""" |
|
:return: |
|
A configuration writer instance with read-and write access to options of |
|
this head. |
|
""" |
|
return self._config_parser(read_only=False) |
|
|
|
|
|
|