diff --git a/home/.config/git/config b/home/.config/git/config index 307901f..9bd30cf 100644 --- a/home/.config/git/config +++ b/home/.config/git/config @@ -62,8 +62,6 @@ lg = "log --all --color --graph --pretty=format:'%Cred%h%Creset -%C(yellow)%d%Creset %s %Cgreen(%cr) %C(bold blue)<%an>%Creset' --abbrev-commit --" loog = "log --format=fuller --show-signature --all --color --decorate --graph" - release = "!~/.config/git/release.py" - make-patch = "diff --no-prefix --relative" set-upstream = "!git branch --set-upstream-to=origin/`git symbolic-ref --short HEAD`" diff --git a/home/.config/git/release.py b/home/.config/git/release.py deleted file mode 100755 index 837fcd8..0000000 --- a/home/.config/git/release.py +++ /dev/null @@ -1,143 +0,0 @@ -#!/usr/bin/env python3 -""" -This is a "safe release tag" script to avoid accidental mistakes when creating version release tags. - -It will: - -1. Checks that you are on `main`, `master`, `release` or `stable` branch. -2. Validate that your local version matches the remote. -3. Ensures that the provided tag is a valid semantic version bump from the latest release tag. -4. Runs git tag -m "" -as if valid. - -Do note that this does NOT support versions outside of simple X.Y.Z (e.g. a version like 1.2.5rc-1 will not be picked up). -If you're using such versions, this script will block the release and you will need to create the tag for it manually. -That said, due to the nature of these versions, even if there's an alpha/beta/rc/post/dev/... version tag, this will still -correctly identify whether the next stable release is following the previous, so you can use this even if your last version -tag was one of these special forms, just not when creating new tags for them. -""" - -from __future__ import annotations - -from dataclasses import dataclass -import re -import subprocess -import sys -from typing import ClassVar, final, override - - -RELEASE_BRANCHES = {"main", "master", "release", "stable"} - - -@final -@dataclass(frozen=True, order=True) -class SemverTag: - VERSION_TAG_RE: ClassVar[re.Pattern[str]] = re.compile(r"^(v)?(\d+)\.(\d+)\.(\d+)$") - - has_v: bool - major: int - minor: int - patch: int - - @classmethod - def from_str(cls, s: str) -> SemverTag: - """Parse a version tag string into a SemverTag instance.""" - m = cls.VERSION_TAG_RE.fullmatch(s) - if not m: - raise ValueError(f"Invalid version tag string: {s}") - - v, major, minor, patch = m.groups() - return cls(v == "v", int(major), int(minor), int(patch)) - - def is_next_after(self, other: SemverTag) -> bool: - """Return True if this tag is the immediate next major, minor, or patch bump after another tag.""" - return ( - self == SemverTag(other.has_v, other.major + 1, 0, 0) - or self == SemverTag(other.has_v, other.major, other.minor + 1, 0) - or self == SemverTag(other.has_v, other.major, other.minor, other.patch + 1) - ) - - @override - def __str__(self) -> str: - """Return tag in standard 'vX.Y.Z' format; the v can be omitted.""" - return f"{self.has_v and 'v' or ''}{self.major}.{self.minor}.{self.patch}" - - -def run_git(*args: str) -> str: - """Run a git command and return stdout, raising on any error.""" - result = subprocess.run(["git", *args], capture_output=True, text=True, check=True) - return result.stdout.strip() - - -def ensure_branch() -> None: - """Exit if not on a valid release branch.""" - branch = run_git("rev-parse", "--abbrev-ref", "HEAD") - if branch not in RELEASE_BRANCHES: - sys.exit( - f"You must be on a valid release branch (one of: {', '.join(RELEASE_BRANCHES)}), your branch: {branch}" - ) - - -def ensure_remote_match() -> None: - """Exit if the local version doesn't match the remote version.""" - _ = run_git("fetch") - branch = run_git("rev-parse", "--abbrev-ref", "HEAD") - tracking = run_git("rev-parse", "--abbrev-ref", f"{branch}@{{upstream}}") - - local_commit = run_git("rev-parse", branch) - remote_commit = run_git("rev-parse", tracking) - - if local_commit != remote_commit: - sys.exit(f"Local branch {branch} is not in sync with remote branch {tracking}") - - -def get_latest_tag() -> SemverTag: - """Return the latest git version tag.""" - # Get all tags sorted by creation name - tags = run_git("tag").splitlines() - version_tags: list[SemverTag] = [] - - for tag in tags: - try: - version_tag = SemverTag.from_str(tag) - except ValueError: - continue - version_tags.append(version_tag) - - if not version_tags: - # Arguably, it might make sense to instead return None and let that pass - # since it's entirely valid to make a new release tag, however, it could - # also indicate a problem with the script, so it's safer to just fail and - # let the user make the first release tag manually. - sys.exit("No version tags found in repository.") - - # Sort by semantic version, newest first - return max(version_tags) - - -def try_make_tag(tag: str) -> None: - ensure_branch() - ensure_remote_match() - - try: - new_tag = SemverTag.from_str(tag) - except ValueError as exc: - sys.exit(f"Invalid tag: {exc!s}") - - latest_tag = get_latest_tag() - - if not new_tag.is_next_after(latest_tag): - sys.exit(f"Given tag isn't a valid version bump after {latest_tag}") - - _ = run_git("tag", "-m", "", "-as", str(new_tag)) - - -def main() -> None: - if len(sys.argv) != 2: - sys.exit(f"Usage: {sys.argv[0]} ") - - new_tag_str = sys.argv[1] - try_make_tag(new_tag_str) - - -if __name__ == "__main__": - main()