#!/usr/bin/env python3 import argparse import logging import re import subprocess import sys from pathlib import Path def parse_args() -> argparse.Namespace: parser = argparse.ArgumentParser( description="Create a tag at HEAD by bumping the highest local X.Y.Z or vX.Y.Z tag. " "Preserves the latest tag's prefix; starts from v0.0.0 when no version tags exist.", formatter_class=argparse.ArgumentDefaultsHelpFormatter, ) group = parser.add_mutually_exclusive_group(required=True) for part in ("major", "minor", "patch"): group.add_argument(f"--{part}", dest="part", action="store_const", const=part, help=f"Bump the {part} version and reset lower components") parser.add_argument("--cwd", type=Path, default=Path.cwd(), help="Git repository directory") parser.add_argument("--push", action="store_true", help="Push only the new tag to origin") return parser.parse_args() def git(cwd: Path, arguments: list[str]) -> str: result = subprocess.run( args=["git", *arguments], cwd=cwd, check=True, capture_output=True, text=True, ) return result.stdout.strip() def next_tag(tags: list[str], part: str) -> str: versions = [] for tag in tags: match = re.fullmatch(r"(v?)(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)", tag) if match: versions.append((tuple(int(value) for value in match.groups()[1:]), match[1])) version, prefix = max(versions, default=((0, 0, 0), "v")) components = list(version) index = ("major", "minor", "patch").index(part) components[index] += 1 components[index + 1:] = [0] * (2 - index) return f"{prefix}{'.'.join(str(value) for value in components)}" def main() -> int: logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s: %(message)s") arguments = parse_args() try: tags = git(cwd=arguments.cwd, arguments=["tag", "--list"]).splitlines() tag = next_tag(tags=tags, part=arguments.part) git(cwd=arguments.cwd, arguments=["tag", "--", tag, "HEAD"]) logging.info(f"Created {tag} at HEAD") if arguments.push: git(cwd=arguments.cwd, arguments=["push", "origin", f"refs/tags/{tag}:refs/tags/{tag}"]) logging.info(f"Pushed {tag} to origin") except subprocess.CalledProcessError as error: logging.error(f"Git failed: {(error.stderr or str(error)).strip()}") return 1 except OSError as error: logging.error(f"{error}") return 1 return 0 if __name__ == "__main__": sys.exit(main())