#!/usr/bin/env python3 # src: # https://git.sr.ht/~sircmpwn/dotfiles/blob/f9e7e88a72c2b87e5ab823205cb3a3aa8f3babd0/bin/semver # usage: semver [patch|minor|major|] # desc: will tag a new version either based on last tag or # # patch(robertgzr) # allow tagging a new non-annotated release import getopt import io import os import subprocess import sys import tempfile import time dry_run = False simple_tag = True force = False changelog = False push = False def exec(args, dry_run=False, strict=True): print(f"+ {args}") if not dry_run: p = subprocess.run(args, stdout=subprocess.PIPE) if strict and p.returncode != 0: print(f"ERROR: {' '.join(args)} returned nonzero exit code") sys.exit(1) return p.stdout.decode().strip() def usage(): print(f"usage: {os.path.basename(sys.argv[0])} OPTIONS COMMANDS") print(""" bump the previous tag and create a new one OPTIONS: \t-h\tshow this help \t-a\tannotate tag \t-f\toverwrite existing tag \t-c\twrite CHANGELOG \t-p\tpush changes \t-n\tdry run COMMANDS: \tpatch \tminor \tmajor """) sys.exit(2) try: opts, argv = getopt.getopt(sys.argv[1:], 'afcpnh') except getopt.GetoptError as err: print(err) usage() for o, val in opts: if o == "-a": simple_tag = not simple_tag elif o == "-f": force = not force elif o == "-c": changelog = not changelog elif o == "-p": push = not push elif o == "-n": dry_run = not dry_run elif o == "-h": usage() else: assert False, "unhandled option" if any(item in ["-h", "--help", "help"] for item in argv): usage() if len(argv) < 1 or len(argv) > 1: usage() if exec(["git", "branch", "--show-current"]) != "master": print("WARNING: not on master branch.") exec(["git", "pull", "--dry-run", "--rebase"]) describe = exec(["git", "describe", "--abbrev=0", "--tags"], strict=False) # TODO also ask to re-append the trailing portion.... e.g. -rev1 prepend = None if describe: if describe[0] == 'v': describe = describe[1:] prepend = 'v' old_version = describe.split("-")[0].split(".") if len(old_version) == 2: [major, minor] = old_version [major, minor] = map(int, [major, minor]) patch = 0 elif len(old_version) == 3: [major, minor, patch] = old_version [major, minor, patch] = map(int, [major, minor, patch]) else: print( f"WARNING: git describe returned invalid semver version: \"{describe}\"" ) sys.exit(1) old_version_revision = exec(["git", "log", "--pretty=%H", "-1", describe]) head_revision = exec(["git", "log", "--pretty=%H", "-1", "HEAD"]) if old_version_revision == head_revision: print(f"WARNING: HEAD already tagged as {describe}") if not force: sys.exit(0) if (not simple_tag) or changelog: previous = describe and describe + ".." if prepend: previous = prepend + previous shortlog = exec(["git", "shortlog", "--no-merges", f"{previous}HEAD"]) if not shortlog: print(f"WARNING: no shortlog") sys.exit(1) else: print(f"{shortlog}") new_version = None if argv[0] == "patch": patch += 1 elif argv[0] == "minor": minor += 1 patch = 0 elif argv[0] == "major": major += 1 minor = patch = 0 else: new_version = argv[0] if not new_version: if len(old_version) == 2 and patch == 0: new_version = f"{major}.{minor}" else: new_version = f"{major}.{minor}.{patch}" if prepend and (new_version[0] != 'v'): new_version = prepend + new_version if os.path.exists("contrib/_incr_version"): exec(["contrib/_incr_version", describe, new_version]) args = ["git", "tag"] if force: args.extend(["-f"]) if simple_tag: args.extend(["-m", new_version, new_version]) exec(args, dry_run) else: with tempfile.NamedTemporaryFile() as f: f.write(new_version.encode()) f.write(f"\n\n".encode()) f.write(shortlog.encode()) f.flush() args.extend(["-F", f.name, "-a", new_version]) exec(args, dry_run) if changelog: changelog = io.StringIO() changelog.write( f"\n{new_version} ({time.strftime('%Y-%m-%d', time.localtime())})\n\n") changelog.write(shortlog) if dry_run: print("+ [writing changelog]") print(changelog.getvalue()) else: with open("CHANGELOG", "w+") as f: changelog.write(f.read()) f.seek(0) f.write(changelog) f.flush() if push: exec(["git", "push", "origin", "HEAD", new_version], dry_run)