import os import warnings from datetime import date from datetime import datetime from os.path import isfile from os.path import join from os.path import samefile from .config import Configuration from .scm_workdir import Workdir from .utils import do_ex from .utils import require_command from .utils import trace from .version import meta DEFAULT_DESCRIBE = "git describe --dirty --tags --long --match *[0-9]*" class GitWorkdir(Workdir): """experimental, may change at any time""" COMMAND = "git" @classmethod def from_potential_worktree(cls, wd): require_command(cls.COMMAND) wd = os.path.abspath(wd) real_wd, _, ret = do_ex("git rev-parse --show-prefix", wd) real_wd = real_wd[:-1] # remove the trailing pathsep if ret: return if not real_wd: real_wd = wd else: assert wd.replace("\\", "/").endswith(real_wd) # In windows wd contains ``\`` which should be replaced by ``/`` # for this assertion to work. Length of string isn't changed by replace # ``\\`` is just and escape for `\` real_wd = wd[: -len(real_wd)] trace("real root", real_wd) if not samefile(real_wd, wd): return return cls(real_wd) def is_dirty(self): out, _, _ = self.do_ex("git status --porcelain --untracked-files=no") return bool(out) def get_branch(self): branch, err, ret = self.do_ex("git rev-parse --abbrev-ref HEAD") if ret: trace("branch err", branch, err, ret) branch, err, ret = self.do_ex("git symbolic-ref --short HEAD") if ret: trace("branch err (symbolic-ref)", branch, err, ret) branch = None return branch def get_head_date(self): timestamp, err, ret = self.do_ex("git log -n 1 HEAD --format=%cI") if ret: trace("timestamp err", timestamp, err, ret) return # TODO, when dropping python3.6 use fromiso date_part = timestamp.split("T")[0] if "%c" in date_part: trace("git too old -> timestamp is ", timestamp) return None return datetime.strptime(date_part, r"%Y-%m-%d").date() def is_shallow(self): return isfile(join(self.path, ".git/shallow")) def fetch_shallow(self): self.do_ex("git fetch --unshallow") def node(self): node, _, ret = self.do_ex("git rev-parse --verify --quiet HEAD") if not ret: return node[:7] def count_all_nodes(self): revs, _, _ = self.do_ex("git rev-list HEAD") return revs.count("\n") + 1 def default_describe(self): return self.do_ex(DEFAULT_DESCRIBE) def warn_on_shallow(wd): """experimental, may change at any time""" if wd.is_shallow(): warnings.warn(f'"{wd.path}" is shallow and may cause errors') def fetch_on_shallow(wd): """experimental, may change at any time""" if wd.is_shallow(): warnings.warn(f'"{wd.path}" was shallow, git fetch was used to rectify') wd.fetch_shallow() def fail_on_shallow(wd): """experimental, may change at any time""" if wd.is_shallow(): raise ValueError( f'{wd.path} is shallow, please correct with "git fetch --unshallow"' ) def get_working_directory(config): """ Return the working directory (``GitWorkdir``). """ if config.parent: return GitWorkdir.from_potential_worktree(config.parent) if config.search_parent_directories: return search_parent(config.absolute_root) return GitWorkdir.from_potential_worktree(config.absolute_root) def parse(root, describe_command=None, pre_parse=warn_on_shallow, config=None): """ :param pre_parse: experimental pre_parse action, may change at any time """ if not config: config = Configuration(root=root) wd = get_working_directory(config) if wd: return _git_parse_inner( config, wd, describe_command=describe_command, pre_parse=pre_parse ) def _git_parse_inner(config, wd, pre_parse=None, describe_command=None): if pre_parse: pre_parse(wd) if config.git_describe_command is not None: describe_command = config.git_describe_command if describe_command is not None: out, _, ret = wd.do_ex(describe_command) else: out, _, ret = wd.default_describe() if ret == 0: tag, distance, node, dirty = _git_parse_describe(out) if distance == 0 and not dirty: distance = None else: # If 'git git_describe_command' failed, try to get the information otherwise. tag = "0.0" node = wd.node() if node is None: distance = 0 else: distance = wd.count_all_nodes() node = "g" + node dirty = wd.is_dirty() branch = wd.get_branch() node_date = wd.get_head_date() or date.today() return meta( tag, branch=branch, node=node, node_date=node_date, distance=distance, dirty=dirty, config=config, ) def _git_parse_describe(describe_output): # 'describe_output' looks e.g. like 'v1.5.0-0-g4060507' or # 'v1.15.1rc1-37-g9bd1298-dirty'. if describe_output.endswith("-dirty"): dirty = True describe_output = describe_output[:-6] else: dirty = False tag, number, node = describe_output.rsplit("-", 2) number = int(number) return tag, number, node, dirty def search_parent(dirname): """ Walk up the path to find the `.git` directory. :param dirname: Directory from which to start searching. """ # Code based on: # https://github.com/gitpython-developers/GitPython/blob/main/git/repo/base.py curpath = os.path.abspath(dirname) while curpath: try: wd = GitWorkdir.from_potential_worktree(curpath) except Exception: wd = None if wd is not None: return wd curpath, tail = os.path.split(curpath) if not tail: return None