2018-09-02 21:48:12 +02:00
|
|
|
# This file is part of the pycalver project
|
2019-02-22 10:56:47 +01:00
|
|
|
# https://gitlab.com/mbarkhau/pycalver
|
2018-09-02 21:48:12 +02:00
|
|
|
#
|
2019-02-22 10:56:47 +01:00
|
|
|
# Copyright (c) 2019 Manuel Barkhau (mbarkhau@gmail.com) - MIT License
|
2018-09-02 21:48:12 +02:00
|
|
|
# SPDX-License-Identifier: MIT
|
|
|
|
|
#
|
|
|
|
|
# pycalver/vcs.py (this file) is based on code from the
|
|
|
|
|
# bumpversion project: https://github.com/peritus/bumpversion
|
2018-11-06 21:45:33 +01:00
|
|
|
# Copyright (c) 2013-2014 Filip Noetzel - MIT License
|
2018-11-15 22:16:16 +01:00
|
|
|
"""Minimal Git and Mercirial API.
|
|
|
|
|
|
|
|
|
|
If terminology for similar concepts differs between git and
|
|
|
|
|
mercurial, then the git terms are used. For example "fetch"
|
|
|
|
|
(git) instead of "pull" (hg) .
|
|
|
|
|
"""
|
2018-09-02 21:48:12 +02:00
|
|
|
|
|
|
|
|
import os
|
2020-05-25 07:46:30 +00:00
|
|
|
import typing as typ
|
2018-09-02 21:48:12 +02:00
|
|
|
import logging
|
|
|
|
|
import tempfile
|
|
|
|
|
import subprocess as sp
|
|
|
|
|
|
2020-07-19 13:18:42 +00:00
|
|
|
logger = logging.getLogger("pycalver.vcs")
|
2018-09-02 21:48:12 +02:00
|
|
|
|
2018-12-09 18:00:22 +01:00
|
|
|
|
2018-11-11 15:07:46 +01:00
|
|
|
VCS_SUBCOMMANDS_BY_NAME = {
|
|
|
|
|
'git': {
|
2018-12-21 19:22:10 +01:00
|
|
|
'is_usable' : "git rev-parse --git-dir",
|
|
|
|
|
'fetch' : "git fetch",
|
2019-02-16 10:40:18 +01:00
|
|
|
'ls_tags' : "git tag --list",
|
2018-12-21 19:22:10 +01:00
|
|
|
'status' : "git status --porcelain",
|
|
|
|
|
'add_path' : "git add --update {path}",
|
|
|
|
|
'commit' : "git commit --file {path}",
|
|
|
|
|
'tag' : "git tag --annotate {tag} --message {tag}",
|
2020-05-08 19:51:33 +00:00
|
|
|
'push_tag' : "git push origin --follow-tags {tag}",
|
2018-12-21 19:22:10 +01:00
|
|
|
'show_remotes': "git config --get remote.origin.url",
|
2018-11-11 15:07:46 +01:00
|
|
|
},
|
|
|
|
|
'hg': {
|
2018-12-21 19:22:10 +01:00
|
|
|
'is_usable' : "hg root",
|
|
|
|
|
'fetch' : "hg pull",
|
|
|
|
|
'ls_tags' : "hg tags",
|
2019-03-24 18:46:39 +01:00
|
|
|
'status' : "hg status -umard",
|
2018-12-21 19:22:10 +01:00
|
|
|
'add_path' : "hg add {path}",
|
2018-12-21 19:51:58 +01:00
|
|
|
'commit' : "hg commit --logfile {path}",
|
2018-12-21 19:22:10 +01:00
|
|
|
'tag' : "hg tag {tag} --message {tag}",
|
|
|
|
|
'push_tag' : "hg push {tag}",
|
|
|
|
|
'show_remotes': "hg paths",
|
2018-11-11 15:07:46 +01:00
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
2019-01-07 17:30:02 +01:00
|
|
|
Env = typ.Dict[str, str]
|
|
|
|
|
|
|
|
|
|
|
2020-07-19 13:18:42 +00:00
|
|
|
class VCSAPI:
|
|
|
|
|
"""Absraction for git and mercurial."""
|
2018-11-11 15:07:46 +01:00
|
|
|
|
|
|
|
|
def __init__(self, name: str, subcommands: typ.Dict[str, str] = None):
|
|
|
|
|
self.name = name
|
|
|
|
|
if subcommands is None:
|
|
|
|
|
self.subcommands = VCS_SUBCOMMANDS_BY_NAME[name]
|
|
|
|
|
else:
|
|
|
|
|
self.subcommands = subcommands
|
|
|
|
|
|
2019-01-07 17:30:02 +01:00
|
|
|
def __call__(self, cmd_name: str, env: Env = None, **kwargs: str) -> str:
|
2018-11-15 22:16:16 +01:00
|
|
|
"""Invoke subcommand and return output."""
|
2018-12-22 00:31:59 +01:00
|
|
|
cmd_tmpl = self.subcommands[cmd_name]
|
|
|
|
|
cmd_str = cmd_tmpl.format(**kwargs)
|
2018-12-22 00:37:27 +01:00
|
|
|
if cmd_name in ("commit", "tag", "push_tag"):
|
2020-07-19 13:18:42 +00:00
|
|
|
logger.info(cmd_str)
|
2018-12-22 00:37:27 +01:00
|
|
|
else:
|
2020-07-19 13:18:42 +00:00
|
|
|
logger.debug(cmd_str)
|
2019-01-07 17:30:02 +01:00
|
|
|
output_data: bytes = sp.check_output(cmd_str.split(), env=env, stderr=sp.STDOUT)
|
2018-11-15 22:16:16 +01:00
|
|
|
|
|
|
|
|
# TODO (mb 2018-11-15): Detect encoding of output?
|
|
|
|
|
_encoding = "utf-8"
|
|
|
|
|
return output_data.decode(_encoding)
|
2018-11-11 15:07:46 +01:00
|
|
|
|
|
|
|
|
@property
|
|
|
|
|
def is_usable(self) -> bool:
|
2018-11-15 22:16:16 +01:00
|
|
|
"""Detect availability of subcommand."""
|
2018-12-21 19:22:10 +01:00
|
|
|
if not os.path.exists(f".{self.name}"):
|
|
|
|
|
return False
|
|
|
|
|
|
2018-11-11 15:07:46 +01:00
|
|
|
cmd = self.subcommands['is_usable'].split()
|
2018-09-02 21:48:12 +02:00
|
|
|
|
|
|
|
|
try:
|
2018-11-11 15:07:46 +01:00
|
|
|
retcode = sp.call(cmd, stderr=sp.PIPE, stdout=sp.PIPE)
|
|
|
|
|
return retcode == 0
|
2018-09-02 21:48:12 +02:00
|
|
|
except OSError as e:
|
|
|
|
|
if e.errno == 2:
|
2018-11-11 15:07:46 +01:00
|
|
|
# git/mercurial is not installed.
|
2018-09-02 21:48:12 +02:00
|
|
|
return False
|
|
|
|
|
raise
|
|
|
|
|
|
2018-12-21 19:22:10 +01:00
|
|
|
@property
|
|
|
|
|
def has_remote(self) -> bool:
|
|
|
|
|
try:
|
|
|
|
|
output = self('show_remotes')
|
|
|
|
|
if output.strip() == "":
|
|
|
|
|
return False
|
|
|
|
|
return True
|
|
|
|
|
except Exception:
|
|
|
|
|
return False
|
|
|
|
|
|
2018-11-11 15:15:14 +01:00
|
|
|
def fetch(self) -> None:
|
2018-11-15 22:16:16 +01:00
|
|
|
"""Fetch updates from remote origin."""
|
2018-12-21 19:22:10 +01:00
|
|
|
if self.has_remote:
|
|
|
|
|
self('fetch')
|
2018-11-11 15:15:14 +01:00
|
|
|
|
2019-03-24 18:46:39 +01:00
|
|
|
def status(self, required_files: typ.Set[str]) -> typ.List[str]:
|
2018-11-15 22:16:16 +01:00
|
|
|
"""Get status lines."""
|
2018-11-11 15:07:46 +01:00
|
|
|
status_output = self('status')
|
2019-03-24 19:04:14 +01:00
|
|
|
status_items = [line.split(" ", 1) for line in status_output.splitlines()]
|
2019-03-24 18:46:39 +01:00
|
|
|
|
2018-09-03 00:14:10 +02:00
|
|
|
return [
|
2019-03-24 18:46:39 +01:00
|
|
|
filepath.strip()
|
|
|
|
|
for status, filepath in status_items
|
|
|
|
|
if filepath.strip() in required_files or status != "??"
|
2018-09-02 21:48:12 +02:00
|
|
|
]
|
|
|
|
|
|
2018-11-11 15:15:14 +01:00
|
|
|
def ls_tags(self) -> typ.List[str]:
|
2018-11-15 22:16:16 +01:00
|
|
|
"""List vcs tags on all branches."""
|
2018-11-11 15:15:14 +01:00
|
|
|
ls_tag_lines = self('ls_tags').splitlines()
|
2020-07-19 13:18:42 +00:00
|
|
|
logger.debug(f"ls_tags output {ls_tag_lines}")
|
2019-02-16 10:46:10 +01:00
|
|
|
return [line.strip().split(" ", 1)[0] for line in ls_tag_lines]
|
2018-09-03 22:23:51 +02:00
|
|
|
|
2018-11-15 22:16:16 +01:00
|
|
|
def add(self, path: str) -> None:
|
|
|
|
|
"""Add updates to be included in next commit."""
|
2018-12-21 19:51:58 +01:00
|
|
|
try:
|
|
|
|
|
self('add_path', path=path)
|
|
|
|
|
except sp.CalledProcessError as ex:
|
|
|
|
|
if "already tracked!" in str(ex):
|
|
|
|
|
# mercurial
|
|
|
|
|
return
|
|
|
|
|
else:
|
|
|
|
|
raise
|
2018-09-03 00:14:10 +02:00
|
|
|
|
2018-11-11 15:07:46 +01:00
|
|
|
def commit(self, message: str) -> None:
|
2018-11-15 22:16:16 +01:00
|
|
|
"""Commit added files."""
|
2018-11-11 15:07:46 +01:00
|
|
|
message_data = message.encode("utf-8")
|
2018-09-02 21:48:12 +02:00
|
|
|
|
2018-11-11 15:07:46 +01:00
|
|
|
tmp_file = tempfile.NamedTemporaryFile("wb", delete=False)
|
2018-11-11 15:44:43 +01:00
|
|
|
assert " " not in tmp_file.name
|
2018-09-02 21:48:12 +02:00
|
|
|
|
2019-03-24 18:46:39 +01:00
|
|
|
fh: typ.IO[bytes]
|
|
|
|
|
|
2018-11-11 15:07:46 +01:00
|
|
|
with tmp_file as fh:
|
|
|
|
|
fh.write(message_data)
|
2018-09-02 21:48:12 +02:00
|
|
|
|
2019-01-07 17:30:02 +01:00
|
|
|
env: Env = os.environ.copy()
|
2018-11-11 15:07:46 +01:00
|
|
|
env['HGENCODING'] = "utf-8"
|
|
|
|
|
self('commit', env=env, path=tmp_file.name)
|
|
|
|
|
os.unlink(tmp_file.name)
|
2018-09-02 21:48:12 +02:00
|
|
|
|
2018-11-15 22:16:16 +01:00
|
|
|
def tag(self, tag_name: str) -> None:
|
|
|
|
|
"""Create an annotated tag."""
|
2018-11-11 15:47:39 +01:00
|
|
|
self('tag', tag=tag_name)
|
2018-09-03 00:14:10 +02:00
|
|
|
|
2018-11-15 22:16:16 +01:00
|
|
|
def push(self, tag_name: str) -> None:
|
|
|
|
|
"""Push changes to origin."""
|
2018-12-21 19:22:10 +01:00
|
|
|
if self.has_remote:
|
|
|
|
|
self('push_tag', tag=tag_name)
|
2018-09-02 21:48:12 +02:00
|
|
|
|
2018-11-11 15:07:46 +01:00
|
|
|
def __repr__(self) -> str:
|
2018-11-15 22:16:16 +01:00
|
|
|
"""Generate string representation."""
|
2020-07-19 13:18:42 +00:00
|
|
|
return f"VCSAPI(name='{self.name}')"
|
2018-09-02 21:48:12 +02:00
|
|
|
|
|
|
|
|
|
2020-07-19 13:18:42 +00:00
|
|
|
def get_vcs_api() -> VCSAPI:
|
2018-11-15 22:16:16 +01:00
|
|
|
"""Detect the appropriate VCS for a repository.
|
|
|
|
|
|
|
|
|
|
raises OSError if the directory doesn't use a supported VCS.
|
|
|
|
|
"""
|
2018-11-11 15:07:46 +01:00
|
|
|
for vcs_name in VCS_SUBCOMMANDS_BY_NAME.keys():
|
2020-07-19 13:18:42 +00:00
|
|
|
vcs_api = VCSAPI(name=vcs_name)
|
|
|
|
|
if vcs_api.is_usable:
|
|
|
|
|
return vcs_api
|
2018-09-02 21:48:12 +02:00
|
|
|
|
2018-11-11 15:07:46 +01:00
|
|
|
raise OSError("No such directory .git/ or .hg/ ")
|