2017-10-31 19:43:55 +01:00
|
|
|
#!/usr/bin/env python
|
|
|
|
#
|
|
|
|
# NEED POSIX (i.e. *Cygwin* on Windows).
|
|
|
|
"""
|
|
|
|
Script to bump, commit and tag new versions.
|
|
|
|
|
|
|
|
USAGE:
|
|
|
|
bumpver
|
|
|
|
bumpver [-n] [-f] [-c] [-a] [-t <message>] <new-ver>
|
|
|
|
|
|
|
|
Without <new-ver> prints version extracted from current file.
|
|
|
|
Don't add a 'v' prefix!
|
|
|
|
|
|
|
|
OPTIONS:
|
|
|
|
-a, --amend Amend current commit for setting the "chore(ver): ..." msg.
|
|
|
|
-f, --force Bump (and optionally) commit/tag if version exists/is same.
|
|
|
|
-n, --dry-run Do not write files - just pretend.
|
|
|
|
-c, --commit Commit afterwardswith a commit-message describing version bump.
|
|
|
|
-t, --tag=<msg> Adds a signed tag with the given message (commit implied).
|
|
|
|
|
|
|
|
|
2018-06-13 02:58:31 +02:00
|
|
|
- Pre-releases: when working on some version
|
2017-10-31 19:43:55 +01:00
|
|
|
X.YbN # Beta release
|
|
|
|
X.YrcN or X.YcN # Release Candidate
|
|
|
|
X.Y # Final release
|
|
|
|
- Post-release:
|
|
|
|
X.YaN.postM # Post-release of an alpha release
|
|
|
|
X.YrcN.postM # Post-release of a release candidate
|
|
|
|
- Dev-release:
|
|
|
|
X.YaN.devM # Developmental release of an alpha release
|
|
|
|
X.Y.postN.devM # Developmental release of a post-release
|
|
|
|
|
|
|
|
EXAMPLE:
|
|
|
|
bumpver -t 'Mostly model changes' 1.6.2b0
|
|
|
|
|
|
|
|
"""
|
|
|
|
|
2023-08-27 16:11:54 +02:00
|
|
|
import functools as fnt
|
2017-10-31 19:43:55 +01:00
|
|
|
import os.path as osp
|
|
|
|
import re
|
2023-08-27 16:11:54 +02:00
|
|
|
import sys
|
|
|
|
from datetime import datetime
|
2017-10-31 19:43:55 +01:00
|
|
|
|
|
|
|
import docopt
|
|
|
|
|
|
|
|
my_dir = osp.dirname(__file__)
|
|
|
|
|
2020-10-06 04:04:22 +02:00
|
|
|
VFILE = osp.join(my_dir, "..", "pypiserver", "__init__.py")
|
2023-08-27 16:11:54 +02:00
|
|
|
VFILE_regex_version = re.compile(r'version *= *__version__ *= *"([^"]+)"')
|
|
|
|
VFILE_regex_datetime = re.compile(r'__updated__ *= *"([^"]+)"')
|
|
|
|
VFILE_regex_date = re.compile(r'__updated__ *= *"([^"\s]+)\s')
|
2017-10-31 19:43:55 +01:00
|
|
|
|
2023-08-15 11:16:30 +02:00
|
|
|
RFILE = osp.join(my_dir, "..", "README.md")
|
2017-10-31 19:43:55 +01:00
|
|
|
|
2020-10-06 04:04:22 +02:00
|
|
|
PYTEST_ARGS = [osp.join("tests", "test_docs.py")]
|
2017-10-31 19:43:55 +01:00
|
|
|
|
|
|
|
|
|
|
|
class CmdException(Exception):
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
2023-08-27 16:11:54 +02:00
|
|
|
def get_current_date_info() -> (str, str):
|
|
|
|
now = datetime.now()
|
|
|
|
new_datetime = now.strftime("%Y-%m-%d %H:%M:%S%z")
|
|
|
|
new_date = now.strftime("%Y-%m-%d")
|
|
|
|
return (new_datetime, new_date)
|
|
|
|
|
|
|
|
|
2017-10-31 19:43:55 +01:00
|
|
|
@fnt.lru_cache()
|
|
|
|
def read_txtfile(fpath):
|
2020-10-06 04:04:22 +02:00
|
|
|
with open(fpath, "rt", encoding="utf-8") as fp:
|
2017-10-31 19:43:55 +01:00
|
|
|
return fp.read()
|
|
|
|
|
|
|
|
|
|
|
|
def extract_file_regexes(fpath, regexes):
|
|
|
|
"""
|
|
|
|
:param regexes:
|
|
|
|
A sequence of regexes to "search", having a single capturing-group.
|
|
|
|
:return:
|
|
|
|
One groups per regex, or raise if any regex did not match.
|
|
|
|
"""
|
|
|
|
txt = read_txtfile(fpath)
|
|
|
|
matches = [regex.search(txt) for regex in regexes]
|
|
|
|
|
|
|
|
if not all(matches):
|
2020-10-06 04:04:22 +02:00
|
|
|
raise CmdException(
|
|
|
|
"Failed extracting current versions with: %s"
|
|
|
|
"\n matches: %s" % (regexes, matches)
|
|
|
|
)
|
2017-10-31 19:43:55 +01:00
|
|
|
|
|
|
|
return [m.group(1) for m in matches]
|
|
|
|
|
|
|
|
|
|
|
|
def replace_substrings(files, subst_pairs):
|
|
|
|
for fpath in files:
|
|
|
|
txt = read_txtfile(fpath)
|
|
|
|
|
|
|
|
replacements = []
|
|
|
|
for old, new in subst_pairs:
|
|
|
|
replacements.append((old, new, txt.count(old)))
|
|
|
|
txt = txt.replace(old, new)
|
|
|
|
|
|
|
|
yield (txt, fpath, replacements)
|
|
|
|
|
|
|
|
|
|
|
|
def format_syscmd(cmd):
|
|
|
|
if isinstance(cmd, (list, tuple)):
|
2020-10-06 04:04:22 +02:00
|
|
|
cmd = " ".join('"%s"' % s if " " in s else s for s in cmd)
|
2017-10-31 19:43:55 +01:00
|
|
|
else:
|
|
|
|
assert isinstance(cmd, str), cmd
|
|
|
|
|
|
|
|
return cmd
|
|
|
|
|
|
|
|
|
|
|
|
def strip_ver2_commonprefix(ver1, ver2):
|
|
|
|
cprefix = osp.commonprefix([ver1, ver2])
|
|
|
|
if cprefix:
|
2020-10-06 04:04:22 +02:00
|
|
|
striplen = cprefix.rfind(".")
|
2017-10-31 19:43:55 +01:00
|
|
|
if striplen > 0:
|
|
|
|
striplen += 1
|
|
|
|
else:
|
|
|
|
striplen = len(cprefix)
|
|
|
|
ver2 = ver2[striplen:]
|
|
|
|
|
|
|
|
return ver2
|
|
|
|
|
|
|
|
|
|
|
|
def run_testcases():
|
|
|
|
import pytest
|
|
|
|
|
|
|
|
retcode = pytest.main(PYTEST_ARGS)
|
|
|
|
if retcode:
|
|
|
|
raise CmdException(
|
2020-10-06 04:04:22 +02:00
|
|
|
"Doc TCs failed(%s), probably version-bumping has failed!" % retcode
|
|
|
|
)
|
2017-10-31 19:43:55 +01:00
|
|
|
|
|
|
|
|
|
|
|
def exec_cmd(cmd):
|
|
|
|
import subprocess as sbp
|
|
|
|
|
|
|
|
err = sbp.call(cmd, stderr=sbp.STDOUT)
|
|
|
|
if err:
|
|
|
|
raise CmdException("Failed(%i) on: %s" % (err, format_syscmd(cmd)))
|
|
|
|
|
|
|
|
|
|
|
|
def do_commit(new_ver, old_ver, dry_run, amend, ver_files):
|
|
|
|
import pathlib
|
|
|
|
|
2020-10-06 04:04:22 +02:00
|
|
|
cmt_msg = "chore(ver): bump %s-->%s" % (old_ver, new_ver)
|
2017-10-31 19:43:55 +01:00
|
|
|
|
|
|
|
ver_files = [pathlib.Path(f).as_posix() for f in ver_files]
|
2020-10-06 04:04:22 +02:00
|
|
|
git_add = ["git", "add"] + ver_files
|
|
|
|
git_cmt = ["git", "commit", "-m", cmt_msg]
|
2017-10-31 19:43:55 +01:00
|
|
|
if amend:
|
2020-10-06 04:04:22 +02:00
|
|
|
git_cmt.append("--amend")
|
2017-10-31 19:43:55 +01:00
|
|
|
commands = [git_add, git_cmt]
|
|
|
|
|
|
|
|
for cmd in commands:
|
|
|
|
cmd_str = format_syscmd(cmd)
|
|
|
|
if dry_run:
|
|
|
|
yield "DRYRUN: %s" % cmd_str
|
|
|
|
else:
|
|
|
|
yield "EXEC: %s" % cmd_str
|
|
|
|
exec_cmd(cmd)
|
|
|
|
|
|
|
|
|
|
|
|
def do_tag(tag, tag_msg, dry_run, force):
|
2020-10-06 04:04:22 +02:00
|
|
|
cmd = ["git", "tag", tag, "-s", "-m", tag_msg]
|
2017-10-31 19:43:55 +01:00
|
|
|
if force:
|
2020-10-06 04:04:22 +02:00
|
|
|
cmd.append("--force")
|
2017-10-31 19:43:55 +01:00
|
|
|
cmd_str = format_syscmd(cmd)
|
|
|
|
if dry_run:
|
|
|
|
yield "DRYRUN: %s" % cmd_str
|
|
|
|
else:
|
|
|
|
yield "EXEC: %s" % cmd_str
|
|
|
|
exec_cmd(cmd)
|
|
|
|
|
|
|
|
|
2020-10-06 04:04:22 +02:00
|
|
|
def bumpver(
|
|
|
|
new_ver, dry_run=False, force=False, amend=False, tag_name_or_commit=None
|
|
|
|
):
|
2017-10-31 19:43:55 +01:00
|
|
|
"""
|
|
|
|
:param tag_name_or_commit:
|
|
|
|
if true, do `git commit`, if string, also `git tag` with that as msg.
|
|
|
|
"""
|
|
|
|
if amend:
|
|
|
|
## Restore previous version before extracting it.
|
2020-10-06 04:04:22 +02:00
|
|
|
cmd = "git checkout HEAD~ --".split()
|
2017-10-31 19:43:55 +01:00
|
|
|
cmd.append(VFILE)
|
|
|
|
cmd.append(RFILE)
|
|
|
|
exec_cmd(cmd)
|
|
|
|
|
2023-08-27 16:11:54 +02:00
|
|
|
regexes = [VFILE_regex_version, VFILE_regex_datetime, VFILE_regex_date]
|
|
|
|
old_ver, old_datetime, old_date = extract_file_regexes(VFILE, regexes)
|
2017-10-31 19:43:55 +01:00
|
|
|
|
|
|
|
if not new_ver:
|
|
|
|
yield old_ver
|
2023-08-27 16:11:54 +02:00
|
|
|
yield old_datetime
|
2017-10-31 19:43:55 +01:00
|
|
|
yield old_date
|
|
|
|
else:
|
|
|
|
if new_ver == old_ver:
|
|
|
|
msg = "Version '%s'already bumped"
|
|
|
|
if force:
|
|
|
|
msg += ", but --force effected."
|
|
|
|
yield msg % new_ver
|
|
|
|
else:
|
|
|
|
msg += "!\n Use of --force recommended."
|
|
|
|
raise CmdException(msg % new_ver)
|
|
|
|
|
2023-08-27 16:11:54 +02:00
|
|
|
new_datetime, new_date = get_current_date_info()
|
2017-10-31 19:43:55 +01:00
|
|
|
ver_files = [osp.normpath(f) for f in [VFILE, RFILE]]
|
2023-08-27 16:11:54 +02:00
|
|
|
subst_pairs = [
|
|
|
|
(old_ver, new_ver),
|
|
|
|
(old_datetime, new_datetime),
|
|
|
|
(old_date, new_date),
|
|
|
|
]
|
2017-10-31 19:43:55 +01:00
|
|
|
|
|
|
|
for repl in replace_substrings(ver_files, subst_pairs):
|
|
|
|
new_txt, fpath, replacements = repl
|
|
|
|
|
|
|
|
if not dry_run:
|
2020-10-06 04:04:22 +02:00
|
|
|
with open(fpath, "wt", encoding="utf-8") as fp:
|
2017-10-31 19:43:55 +01:00
|
|
|
fp.write(new_txt)
|
|
|
|
|
2020-10-06 04:04:22 +02:00
|
|
|
yield "%s: " % fpath
|
2017-10-31 19:43:55 +01:00
|
|
|
for old, new, nrepl in replacements:
|
2020-10-06 04:04:22 +02:00
|
|
|
yield " %i x (%24s --> %s)" % (nrepl, old, new)
|
2017-10-31 19:43:55 +01:00
|
|
|
|
|
|
|
yield "...now launching DocTCs..."
|
|
|
|
run_testcases()
|
|
|
|
|
|
|
|
if tag_name_or_commit is not None:
|
|
|
|
yield from do_commit(new_ver, old_ver, dry_run, amend, ver_files)
|
|
|
|
|
|
|
|
if isinstance(tag_name_or_commit, str):
|
2020-10-06 04:04:22 +02:00
|
|
|
tag = "v%s" % new_ver
|
2017-10-31 19:43:55 +01:00
|
|
|
yield from do_tag(tag, tag_name_or_commit, dry_run, force)
|
|
|
|
|
|
|
|
|
|
|
|
def main(*args):
|
|
|
|
opts = docopt.docopt(__doc__, argv=args)
|
|
|
|
|
2020-10-06 04:04:22 +02:00
|
|
|
new_ver = opts["<new-ver>"]
|
2017-10-31 19:43:55 +01:00
|
|
|
|
2020-10-06 04:04:22 +02:00
|
|
|
assert not new_ver or new_ver[0] != "v", (
|
|
|
|
"Version '%s' must NOT start with `v`!" % new_ver
|
|
|
|
)
|
2017-10-31 19:43:55 +01:00
|
|
|
|
2020-10-06 04:04:22 +02:00
|
|
|
commit = opts["--commit"]
|
|
|
|
tag = opts["--tag"]
|
2017-10-31 19:43:55 +01:00
|
|
|
if tag:
|
|
|
|
tag_name_or_commit = tag
|
|
|
|
elif commit:
|
|
|
|
tag_name_or_commit = True
|
|
|
|
else:
|
|
|
|
tag_name_or_commit = None
|
|
|
|
|
|
|
|
try:
|
2020-10-06 04:04:22 +02:00
|
|
|
for i in bumpver(
|
|
|
|
new_ver,
|
|
|
|
opts["--dry-run"],
|
|
|
|
opts["--force"],
|
|
|
|
opts["--amend"],
|
|
|
|
tag_name_or_commit,
|
|
|
|
):
|
2017-10-31 19:43:55 +01:00
|
|
|
print(i)
|
|
|
|
except CmdException as ex:
|
|
|
|
sys.exit(str(ex))
|
|
|
|
except Exception as ex:
|
2023-08-27 16:11:54 +02:00
|
|
|
print("Unexpected error happened.")
|
2017-10-31 19:43:55 +01:00
|
|
|
raise ex
|
|
|
|
|
|
|
|
|
2020-10-06 04:04:22 +02:00
|
|
|
if __name__ == "__main__":
|
2017-10-31 19:43:55 +01:00
|
|
|
main(*sys.argv[1:])
|