bumpver/src/pycalver/vcs.py

168 lines
5.1 KiB
Python
Raw Normal View History

2018-09-02 21:48:12 +02:00
# This file is part of the pycalver project
# https://github.com/mbarkhau/pycalver
#
2018-11-06 21:45:33 +01:00
# Copyright (c) 2018 Manuel Barkhau (@mbarkhau) - 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
import logging
import tempfile
2018-09-03 22:23:51 +02:00
import typing as typ
2018-09-02 21:48:12 +02:00
import subprocess as sp
log = logging.getLogger("pycalver.vcs")
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",
'ls_tags' : "git tag --list v*",
'status' : "git status --porcelain",
'add_path' : "git add --update {path}",
'commit' : "git commit --file {path}",
'tag' : "git tag --annotate {tag} --message {tag}",
'push_tag' : "git push origin {tag}",
'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",
'status' : "hg status -mard",
'add_path' : "hg add {path}",
'commit' : "hg commit --logfile",
'tag' : "hg tag {tag} --message {tag}",
'push_tag' : "hg push {tag}",
'show_remotes': "hg paths",
2018-11-11 15:07:46 +01:00
},
}
class VCS:
2018-11-15 22:16:16 +01:00
"""VCS 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
2018-11-15 22:16:16 +01:00
def __call__(self, cmd_name: str, env=None, **kwargs: str) -> str:
"""Invoke subcommand and return output."""
cmd_str = self.subcommands[cmd_name]
cmd_parts = cmd_str.format(**kwargs).split()
output_data = sp.check_output(cmd_parts, env=env)
# 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
2018-11-11 15:07:46 +01:00
def status(self) -> 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')
2018-09-03 00:14:10 +02:00
return [
2018-11-15 22:16:16 +01:00
line[2:].strip()
2018-09-02 21:48:12 +02:00
for line in status_output.splitlines()
2018-11-15 22:16:16 +01:00
if not line.strip().startswith("??")
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()
log.debug(f"ls_tags output {ls_tag_lines}")
2018-11-15 22:16:16 +01:00
return [line.strip() for line in ls_tag_lines if line.strip().startswith("v")]
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-11-11 15:07:46 +01:00
log.info(f"{self.name} add {path}")
2018-11-11 15:41:45 +01:00
self('add_path', path=path)
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
log.info(f"{self.name} commit -m '{message}'")
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
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
2018-11-11 15:07:46 +01:00
env = os.environ.copy()
# TODO (mb 2018-09-04): check that this works on py27,
# might need to be bytes there, idk.
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."""
2018-11-11 15:07:46 +01:00
return f"VCS(name='{self.name}')"
2018-09-02 21:48:12 +02:00
2018-11-11 15:07:46 +01:00
def get_vcs() -> VCS:
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():
vcs = VCS(name=vcs_name)
if vcs.is_usable:
2018-09-03 00:14:10 +02:00
return vcs
2018-09-02 21:48:12 +02:00
2018-11-11 15:07:46 +01:00
raise OSError("No such directory .git/ or .hg/ ")