mirror of
https://github.com/ItsDrike/dotfiles.git
synced 2025-09-13 10:40:04 +00:00
Compare commits
No commits in common. "6d46153097a2712658ba5479e4a9478567fd4110" and "aeef67ba58f17c70c1a4407ce21a80f39a9a1fed" have entirely different histories.
6d46153097
...
aeef67ba58
2 changed files with 0 additions and 145 deletions
|
@ -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 --"
|
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"
|
loog = "log --format=fuller --show-signature --all --color --decorate --graph"
|
||||||
|
|
||||||
release = "!~/.config/git/release.py"
|
|
||||||
|
|
||||||
make-patch = "diff --no-prefix --relative"
|
make-patch = "diff --no-prefix --relative"
|
||||||
|
|
||||||
set-upstream = "!git branch --set-upstream-to=origin/`git symbolic-ref --short HEAD`"
|
set-upstream = "!git branch --set-upstream-to=origin/`git symbolic-ref --short HEAD`"
|
||||||
|
|
|
@ -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 <tag> 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>")
|
|
||||||
|
|
||||||
new_tag_str = sys.argv[1]
|
|
||||||
try_make_tag(new_tag_str)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
Loading…
Add table
Add a link
Reference in a new issue