commit-and-tag-version 0.0.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- commit_and_tag_version/__init__.py +68 -0
- commit_and_tag_version/checkpoint.py +18 -0
- commit_and_tag_version/cli.py +76 -0
- commit_and_tag_version/commit_parser.py +77 -0
- commit_and_tag_version/config.py +109 -0
- commit_and_tag_version/defaults.py +68 -0
- commit_and_tag_version/format_commit_message.py +2 -0
- commit_and_tag_version/git.py +115 -0
- commit_and_tag_version/lifecycles/__init__.py +0 -0
- commit_and_tag_version/lifecycles/bump.py +179 -0
- commit_and_tag_version/lifecycles/changelog.py +149 -0
- commit_and_tag_version/lifecycles/commit.py +43 -0
- commit_and_tag_version/lifecycles/tag.py +36 -0
- commit_and_tag_version/models.py +71 -0
- commit_and_tag_version/run_lifecycle_script.py +30 -0
- commit_and_tag_version/updaters/__init__.py +80 -0
- commit_and_tag_version/updaters/base.py +8 -0
- commit_and_tag_version/updaters/csproj.py +19 -0
- commit_and_tag_version/updaters/gradle.py +19 -0
- commit_and_tag_version/updaters/json_updater.py +44 -0
- commit_and_tag_version/updaters/maven.py +36 -0
- commit_and_tag_version/updaters/openapi.py +19 -0
- commit_and_tag_version/updaters/plain_text.py +9 -0
- commit_and_tag_version/updaters/python_updater.py +25 -0
- commit_and_tag_version/updaters/yaml_updater.py +19 -0
- commit_and_tag_version/write_file.py +7 -0
- commit_and_tag_version-0.0.1.dist-info/METADATA +349 -0
- commit_and_tag_version-0.0.1.dist-info/RECORD +31 -0
- commit_and_tag_version-0.0.1.dist-info/WHEEL +4 -0
- commit_and_tag_version-0.0.1.dist-info/entry_points.txt +2 -0
- commit_and_tag_version-0.0.1.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
"""Versioning using semver and CHANGELOG generation powered by Conventional Commits."""
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
from commit_and_tag_version.checkpoint import print_error
|
|
6
|
+
from commit_and_tag_version.git import get_latest_semver_tag
|
|
7
|
+
from commit_and_tag_version.lifecycles.bump import bump
|
|
8
|
+
from commit_and_tag_version.lifecycles.changelog import (
|
|
9
|
+
START_OF_LAST_RELEASE_PATTERN,
|
|
10
|
+
changelog,
|
|
11
|
+
)
|
|
12
|
+
from commit_and_tag_version.lifecycles.commit import commit
|
|
13
|
+
from commit_and_tag_version.lifecycles.tag import tag
|
|
14
|
+
from commit_and_tag_version.models import Config
|
|
15
|
+
from commit_and_tag_version.updaters import resolve_updater_object
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def commit_and_tag_version(config: Config) -> None:
|
|
19
|
+
if config.header and START_OF_LAST_RELEASE_PATTERN.search(config.header):
|
|
20
|
+
raise ValueError(
|
|
21
|
+
f"custom changelog header must not match {START_OF_LAST_RELEASE_PATTERN.pattern}"
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
pkg = None
|
|
25
|
+
version = None
|
|
26
|
+
|
|
27
|
+
for package_file in config.package_files:
|
|
28
|
+
updater_obj = resolve_updater_object(package_file)
|
|
29
|
+
if not updater_obj:
|
|
30
|
+
continue
|
|
31
|
+
pkg_path = Path.cwd() / updater_obj["filename"]
|
|
32
|
+
try:
|
|
33
|
+
contents = pkg_path.read_text(encoding="utf-8")
|
|
34
|
+
updater = updater_obj["updater"]
|
|
35
|
+
pkg = {
|
|
36
|
+
"version": updater.read_version(contents),
|
|
37
|
+
"private": (
|
|
38
|
+
updater.is_private(contents) if hasattr(updater, "is_private") else False
|
|
39
|
+
),
|
|
40
|
+
}
|
|
41
|
+
version = pkg["version"]
|
|
42
|
+
break
|
|
43
|
+
except (FileNotFoundError, KeyError, ValueError):
|
|
44
|
+
continue
|
|
45
|
+
|
|
46
|
+
all_skipped = (
|
|
47
|
+
config.skip.bump and config.skip.changelog and config.skip.commit and config.skip.tag
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
if version is None:
|
|
51
|
+
if all_skipped:
|
|
52
|
+
return
|
|
53
|
+
if config.git_tag_fallback:
|
|
54
|
+
version = get_latest_semver_tag(config.tag_prefix)
|
|
55
|
+
else:
|
|
56
|
+
raise RuntimeError("no package file found")
|
|
57
|
+
|
|
58
|
+
if all_skipped:
|
|
59
|
+
return
|
|
60
|
+
|
|
61
|
+
try:
|
|
62
|
+
new_version = bump(config, version) if not config.skip.bump else version
|
|
63
|
+
changelog(config, new_version)
|
|
64
|
+
commit(config, new_version)
|
|
65
|
+
tag(new_version, pkg["private"] if pkg else False, config)
|
|
66
|
+
except Exception as err:
|
|
67
|
+
print_error(str(err), silent=config.silent)
|
|
68
|
+
raise
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def checkpoint(msg: str, args: list[str], silent: bool = False, dry_run: bool = False) -> None:
|
|
5
|
+
if silent:
|
|
6
|
+
return
|
|
7
|
+
figure = "\u2713" if not dry_run else "\u2299"
|
|
8
|
+
formatted_args = [f"\033[1m{a}\033[0m" for a in args]
|
|
9
|
+
formatted_msg = msg
|
|
10
|
+
for arg in formatted_args:
|
|
11
|
+
formatted_msg = formatted_msg.replace("%s", arg, 1)
|
|
12
|
+
print(f" {figure} {formatted_msg}", file=sys.stderr)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def print_error(msg: str, silent: bool = False) -> None:
|
|
16
|
+
if silent:
|
|
17
|
+
return
|
|
18
|
+
print(f"\033[31m{msg}\033[0m", file=sys.stderr)
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
|
|
3
|
+
import click
|
|
4
|
+
|
|
5
|
+
from commit_and_tag_version import commit_and_tag_version
|
|
6
|
+
from commit_and_tag_version.config import load_config
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@click.command(name="commit-and-tag-version")
|
|
10
|
+
@click.option(
|
|
11
|
+
"--release-as", "-r", type=str, default=None, help="Specify release type or exact version"
|
|
12
|
+
)
|
|
13
|
+
@click.option(
|
|
14
|
+
"--prerelease",
|
|
15
|
+
"-p",
|
|
16
|
+
type=str,
|
|
17
|
+
default=None,
|
|
18
|
+
is_flag=False,
|
|
19
|
+
flag_value="",
|
|
20
|
+
help="Make a prerelease with optional tag id",
|
|
21
|
+
)
|
|
22
|
+
@click.option(
|
|
23
|
+
"--first-release", "-f", is_flag=True, default=False, help="Is this the first release?"
|
|
24
|
+
)
|
|
25
|
+
@click.option("--sign", "-s", is_flag=True, default=False, help="GPG sign commits and tags")
|
|
26
|
+
@click.option("--signoff", is_flag=True, default=False, help="Add Signed-off-by trailer")
|
|
27
|
+
@click.option("--no-verify", "-n", is_flag=True, default=False, help="Bypass git hooks")
|
|
28
|
+
@click.option("--commit-all", "-a", is_flag=True, default=False, help="Commit all staged changes")
|
|
29
|
+
@click.option("--dry-run", is_flag=True, default=False, help="Simulate without making changes")
|
|
30
|
+
@click.option("--silent", is_flag=True, default=False, help="Suppress output")
|
|
31
|
+
@click.option("--tag-prefix", "-t", type=str, default=None, help="Tag prefix (default: v)")
|
|
32
|
+
@click.option("--tag-force", is_flag=True, default=False, help="Replace existing tag")
|
|
33
|
+
@click.option("--config", "-c", type=click.Path(), default=None, help="Custom config file path")
|
|
34
|
+
@click.option("--infile", "-i", type=str, default=None, help="Changelog file path")
|
|
35
|
+
@click.option("--release-count", type=int, default=None, help="Number of releases in changelog")
|
|
36
|
+
@click.option("--header", type=str, default=None, help="Custom changelog header")
|
|
37
|
+
@click.option(
|
|
38
|
+
"--release-commit-message-format", type=str, default=None, help="Commit message format"
|
|
39
|
+
)
|
|
40
|
+
@click.option("--commit-url-format", type=str, default=None, help="Commit URL format template")
|
|
41
|
+
@click.option("--compare-url-format", type=str, default=None, help="Compare URL format template")
|
|
42
|
+
@click.option("--issue-url-format", type=str, default=None, help="Issue URL format template")
|
|
43
|
+
@click.option(
|
|
44
|
+
"--git-tag-fallback/--no-git-tag-fallback",
|
|
45
|
+
default=None,
|
|
46
|
+
help="Fallback to git tags for version",
|
|
47
|
+
)
|
|
48
|
+
@click.option("--path", type=str, default=None, help="Only populate commits under this path")
|
|
49
|
+
@click.option("--skip-bump", is_flag=True, default=False, help="Skip version bump")
|
|
50
|
+
@click.option("--skip-changelog", is_flag=True, default=False, help="Skip changelog generation")
|
|
51
|
+
@click.option("--skip-commit", is_flag=True, default=False, help="Skip git commit")
|
|
52
|
+
@click.option("--skip-tag", is_flag=True, default=False, help="Skip git tag")
|
|
53
|
+
@click.version_option()
|
|
54
|
+
def main(**kwargs):
|
|
55
|
+
"""Versioning using semver and CHANGELOG generation powered by Conventional Commits."""
|
|
56
|
+
cli_args = {k: v for k, v in kwargs.items() if v is not None}
|
|
57
|
+
|
|
58
|
+
skip_args = {}
|
|
59
|
+
for skip_flag in ["skip_bump", "skip_changelog", "skip_commit", "skip_tag"]:
|
|
60
|
+
val = cli_args.pop(skip_flag, None)
|
|
61
|
+
if val:
|
|
62
|
+
field = skip_flag.replace("skip_", "")
|
|
63
|
+
skip_args[field] = True
|
|
64
|
+
|
|
65
|
+
try:
|
|
66
|
+
config = load_config(cli_args)
|
|
67
|
+
|
|
68
|
+
if skip_args:
|
|
69
|
+
for k, v in skip_args.items():
|
|
70
|
+
setattr(config.skip, k, v)
|
|
71
|
+
|
|
72
|
+
commit_and_tag_version(config)
|
|
73
|
+
except Exception as err:
|
|
74
|
+
if not kwargs.get("silent"):
|
|
75
|
+
click.echo(f"Error: {err}", err=True)
|
|
76
|
+
sys.exit(1)
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import re
|
|
2
|
+
|
|
3
|
+
import semver as semver_mod
|
|
4
|
+
|
|
5
|
+
from commit_and_tag_version.models import ParsedCommit
|
|
6
|
+
|
|
7
|
+
COMMIT_PATTERN = re.compile(
|
|
8
|
+
r"^(?P<type>\w+)(?:\((?P<scope>[^)]+)\))?(?P<breaking>!)?:\s*(?P<subject>.+)"
|
|
9
|
+
)
|
|
10
|
+
BREAKING_CHANGE_PATTERN = re.compile(r"^BREAKING[\- ]CHANGE:\s*(?P<note>.+)", re.MULTILINE)
|
|
11
|
+
ISSUE_PATTERN = re.compile(r"#(\d+)")
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def parse_commit(raw: str) -> ParsedCommit | None:
|
|
15
|
+
lines = raw.strip().split("\n")
|
|
16
|
+
if not lines:
|
|
17
|
+
return None
|
|
18
|
+
|
|
19
|
+
header = lines[0]
|
|
20
|
+
match = COMMIT_PATTERN.match(header)
|
|
21
|
+
if not match:
|
|
22
|
+
return None
|
|
23
|
+
|
|
24
|
+
body_lines = []
|
|
25
|
+
if len(lines) > 2:
|
|
26
|
+
body_lines = lines[2:]
|
|
27
|
+
body = "\n".join(body_lines) if body_lines else None
|
|
28
|
+
full_text = raw.strip()
|
|
29
|
+
|
|
30
|
+
breaking = match.group("breaking") == "!"
|
|
31
|
+
breaking_note = None
|
|
32
|
+
|
|
33
|
+
breaking_match = BREAKING_CHANGE_PATTERN.search(full_text)
|
|
34
|
+
if breaking_match:
|
|
35
|
+
breaking = True
|
|
36
|
+
breaking_note = breaking_match.group("note").strip()
|
|
37
|
+
|
|
38
|
+
references = []
|
|
39
|
+
for issue_match in ISSUE_PATTERN.finditer(full_text):
|
|
40
|
+
ref = f"#{issue_match.group(1)}"
|
|
41
|
+
if ref not in references:
|
|
42
|
+
references.append(ref)
|
|
43
|
+
|
|
44
|
+
return ParsedCommit(
|
|
45
|
+
type=match.group("type"),
|
|
46
|
+
scope=match.group("scope"),
|
|
47
|
+
subject=match.group("subject").strip(),
|
|
48
|
+
body=body,
|
|
49
|
+
breaking=breaking,
|
|
50
|
+
breaking_note=breaking_note,
|
|
51
|
+
references=references,
|
|
52
|
+
raw=raw.strip(),
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def recommend_bump(
|
|
57
|
+
commits: list[ParsedCommit | None],
|
|
58
|
+
current_version: str = "1.0.0",
|
|
59
|
+
) -> str:
|
|
60
|
+
has_breaking = False
|
|
61
|
+
has_feat = False
|
|
62
|
+
|
|
63
|
+
for commit in commits:
|
|
64
|
+
if commit is None:
|
|
65
|
+
continue
|
|
66
|
+
if commit.breaking:
|
|
67
|
+
has_breaking = True
|
|
68
|
+
if commit.type == "feat":
|
|
69
|
+
has_feat = True
|
|
70
|
+
|
|
71
|
+
pre_major = semver_mod.Version.parse(current_version) < semver_mod.Version.parse("1.0.0")
|
|
72
|
+
|
|
73
|
+
if has_breaking:
|
|
74
|
+
return "minor" if pre_major else "major"
|
|
75
|
+
if has_feat:
|
|
76
|
+
return "minor"
|
|
77
|
+
return "patch"
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import json
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
|
|
4
|
+
from commit_and_tag_version.defaults import get_default_config
|
|
5
|
+
from commit_and_tag_version.models import Config
|
|
6
|
+
|
|
7
|
+
try:
|
|
8
|
+
import tomllib
|
|
9
|
+
except ModuleNotFoundError:
|
|
10
|
+
try:
|
|
11
|
+
import tomli as tomllib # type: ignore[no-redef]
|
|
12
|
+
except ModuleNotFoundError:
|
|
13
|
+
tomllib = None # type: ignore[assignment]
|
|
14
|
+
|
|
15
|
+
CAMEL_TO_SNAKE: dict[str, str] = {
|
|
16
|
+
"tagPrefix": "tag_prefix",
|
|
17
|
+
"releaseAs": "release_as",
|
|
18
|
+
"noVerify": "no_verify",
|
|
19
|
+
"commitAll": "commit_all",
|
|
20
|
+
"firstRelease": "first_release",
|
|
21
|
+
"dryRun": "dry_run",
|
|
22
|
+
"tagForce": "tag_force",
|
|
23
|
+
"releaseCount": "release_count",
|
|
24
|
+
"releaseCommitMessageFormat": "release_commit_message_format",
|
|
25
|
+
"commitUrlFormat": "commit_url_format",
|
|
26
|
+
"compareUrlFormat": "compare_url_format",
|
|
27
|
+
"issueUrlFormat": "issue_url_format",
|
|
28
|
+
"gitTagFallback": "git_tag_fallback",
|
|
29
|
+
"packageFiles": "package_files",
|
|
30
|
+
"bumpFiles": "bump_files",
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _normalize_keys(raw: dict) -> dict:
|
|
35
|
+
normalized: dict = {}
|
|
36
|
+
for key, value in raw.items():
|
|
37
|
+
snake_key = CAMEL_TO_SNAKE.get(key, key)
|
|
38
|
+
normalized[snake_key] = value
|
|
39
|
+
return normalized
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _load_json_file(path: Path) -> dict:
|
|
43
|
+
return json.loads(path.read_text(encoding="utf-8"))
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _load_pyproject_toml(path: Path) -> dict:
|
|
47
|
+
if tomllib is None:
|
|
48
|
+
return {}
|
|
49
|
+
with open(path, "rb") as f:
|
|
50
|
+
data = tomllib.load(f)
|
|
51
|
+
return data.get("tool", {}).get("commit-and-tag-version", {})
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _discover_file_config(config_path: str | None = None) -> dict:
|
|
55
|
+
if config_path:
|
|
56
|
+
path = Path(config_path)
|
|
57
|
+
if path.exists():
|
|
58
|
+
return _normalize_keys(_load_json_file(path))
|
|
59
|
+
return {}
|
|
60
|
+
|
|
61
|
+
cwd = Path.cwd()
|
|
62
|
+
|
|
63
|
+
versionrc = cwd / ".versionrc"
|
|
64
|
+
if versionrc.exists():
|
|
65
|
+
return _normalize_keys(_load_json_file(versionrc))
|
|
66
|
+
|
|
67
|
+
versionrc_json = cwd / ".versionrc.json"
|
|
68
|
+
if versionrc_json.exists():
|
|
69
|
+
return _normalize_keys(_load_json_file(versionrc_json))
|
|
70
|
+
|
|
71
|
+
pyproject = cwd / "pyproject.toml"
|
|
72
|
+
if pyproject.exists():
|
|
73
|
+
raw = _load_pyproject_toml(pyproject)
|
|
74
|
+
if raw:
|
|
75
|
+
return _normalize_keys(raw)
|
|
76
|
+
|
|
77
|
+
return {}
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def _apply_to_config(config: Config, overrides: dict) -> None:
|
|
81
|
+
skip_raw = overrides.pop("skip", None)
|
|
82
|
+
scripts_raw = overrides.pop("scripts", None)
|
|
83
|
+
|
|
84
|
+
for key, value in overrides.items():
|
|
85
|
+
if hasattr(config, key):
|
|
86
|
+
setattr(config, key, value)
|
|
87
|
+
|
|
88
|
+
if skip_raw and isinstance(skip_raw, dict):
|
|
89
|
+
for k, v in skip_raw.items():
|
|
90
|
+
if hasattr(config.skip, k):
|
|
91
|
+
setattr(config.skip, k, v)
|
|
92
|
+
|
|
93
|
+
if scripts_raw and isinstance(scripts_raw, dict):
|
|
94
|
+
for k, v in scripts_raw.items():
|
|
95
|
+
if hasattr(config.scripts, k):
|
|
96
|
+
setattr(config.scripts, k, v)
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def load_config(cli_args: dict) -> Config:
|
|
100
|
+
config = get_default_config()
|
|
101
|
+
|
|
102
|
+
config_path = cli_args.pop("config", None)
|
|
103
|
+
file_overrides = _discover_file_config(config_path)
|
|
104
|
+
_apply_to_config(config, file_overrides)
|
|
105
|
+
|
|
106
|
+
cli_overrides = {k: v for k, v in cli_args.items() if v is not None}
|
|
107
|
+
_apply_to_config(config, cli_overrides)
|
|
108
|
+
|
|
109
|
+
return config
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
from commit_and_tag_version.models import Config, ScriptsConfig, SkipConfig
|
|
2
|
+
|
|
3
|
+
DEFAULT_PACKAGE_FILES: list[str] = ["package.json", "bower.json", "manifest.json"]
|
|
4
|
+
|
|
5
|
+
DEFAULT_BUMP_FILES: list[str] = [
|
|
6
|
+
"package.json",
|
|
7
|
+
"bower.json",
|
|
8
|
+
"manifest.json",
|
|
9
|
+
"package-lock.json",
|
|
10
|
+
"npm-shrinkwrap.json",
|
|
11
|
+
]
|
|
12
|
+
|
|
13
|
+
DEFAULT_INFILE = "CHANGELOG.md"
|
|
14
|
+
DEFAULT_TAG_PREFIX = "v"
|
|
15
|
+
DEFAULT_RELEASE_COUNT = 1
|
|
16
|
+
DEFAULT_PRESET = "conventionalcommits"
|
|
17
|
+
|
|
18
|
+
DEFAULT_RELEASE_COMMIT_MESSAGE_FORMAT = "chore(release): {{currentTag}}"
|
|
19
|
+
|
|
20
|
+
DEFAULT_HEADER = (
|
|
21
|
+
"# Changelog\n\nAll notable changes to this project will be documented in this file. "
|
|
22
|
+
"See [commit-and-tag-version](https://github.com/absolute-version/commit-and-tag-version) "
|
|
23
|
+
"for commit guidelines.\n"
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
DEFAULT_COMMIT_TYPES: list[dict] = [
|
|
27
|
+
{"type": "feat", "section": "Features"},
|
|
28
|
+
{"type": "fix", "section": "Bug Fixes"},
|
|
29
|
+
{"type": "chore", "section": "Chores", "hidden": True},
|
|
30
|
+
{"type": "docs", "section": "Documentation", "hidden": True},
|
|
31
|
+
{"type": "style", "section": "Styles", "hidden": True},
|
|
32
|
+
{"type": "refactor", "section": "Refactoring", "hidden": True},
|
|
33
|
+
{"type": "perf", "section": "Performance", "hidden": True},
|
|
34
|
+
{"type": "test", "section": "Tests", "hidden": True},
|
|
35
|
+
{"type": "build", "section": "Build System", "hidden": True},
|
|
36
|
+
{"type": "ci", "section": "CI", "hidden": True},
|
|
37
|
+
]
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def get_default_config() -> Config:
|
|
41
|
+
return Config(
|
|
42
|
+
package_files=list(DEFAULT_PACKAGE_FILES),
|
|
43
|
+
bump_files=list(DEFAULT_BUMP_FILES),
|
|
44
|
+
infile=DEFAULT_INFILE,
|
|
45
|
+
tag_prefix=DEFAULT_TAG_PREFIX,
|
|
46
|
+
preset=DEFAULT_PRESET,
|
|
47
|
+
release_as=None,
|
|
48
|
+
prerelease=None,
|
|
49
|
+
sign=False,
|
|
50
|
+
signoff=False,
|
|
51
|
+
no_verify=False,
|
|
52
|
+
commit_all=False,
|
|
53
|
+
first_release=False,
|
|
54
|
+
dry_run=False,
|
|
55
|
+
silent=False,
|
|
56
|
+
tag_force=False,
|
|
57
|
+
release_count=DEFAULT_RELEASE_COUNT,
|
|
58
|
+
skip=SkipConfig(),
|
|
59
|
+
scripts=ScriptsConfig(),
|
|
60
|
+
header=DEFAULT_HEADER,
|
|
61
|
+
commit_url_format=None,
|
|
62
|
+
compare_url_format=None,
|
|
63
|
+
issue_url_format=None,
|
|
64
|
+
release_commit_message_format=DEFAULT_RELEASE_COMMIT_MESSAGE_FORMAT,
|
|
65
|
+
git_tag_fallback=True,
|
|
66
|
+
path=None,
|
|
67
|
+
types=None,
|
|
68
|
+
)
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import re
|
|
2
|
+
import subprocess
|
|
3
|
+
|
|
4
|
+
import semver
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def _run_git(args: list[str], dry_run: bool = False) -> str:
|
|
8
|
+
if dry_run:
|
|
9
|
+
return ""
|
|
10
|
+
result = subprocess.run(
|
|
11
|
+
["git", *args],
|
|
12
|
+
capture_output=True,
|
|
13
|
+
text=True,
|
|
14
|
+
check=True,
|
|
15
|
+
)
|
|
16
|
+
return result.stdout
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def git_add(files: list[str], dry_run: bool = False) -> None:
|
|
20
|
+
_run_git(["add", *files], dry_run=dry_run)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def git_commit(
|
|
24
|
+
message: str,
|
|
25
|
+
sign: bool = False,
|
|
26
|
+
signoff: bool = False,
|
|
27
|
+
no_verify: bool = False,
|
|
28
|
+
dry_run: bool = False,
|
|
29
|
+
files: list[str] | None = None,
|
|
30
|
+
) -> None:
|
|
31
|
+
cmd = ["commit"]
|
|
32
|
+
if sign:
|
|
33
|
+
cmd.append("--gpg-sign")
|
|
34
|
+
if signoff:
|
|
35
|
+
cmd.append("--signoff")
|
|
36
|
+
if no_verify:
|
|
37
|
+
cmd.append("--no-verify")
|
|
38
|
+
cmd.extend(["-m", message])
|
|
39
|
+
if files:
|
|
40
|
+
cmd.extend(files)
|
|
41
|
+
_run_git(cmd, dry_run=dry_run)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def git_tag(
|
|
45
|
+
name: str,
|
|
46
|
+
message: str,
|
|
47
|
+
sign: bool = False,
|
|
48
|
+
force: bool = False,
|
|
49
|
+
dry_run: bool = False,
|
|
50
|
+
) -> None:
|
|
51
|
+
cmd = ["tag"]
|
|
52
|
+
if sign:
|
|
53
|
+
cmd.append("-s")
|
|
54
|
+
else:
|
|
55
|
+
cmd.append("-a")
|
|
56
|
+
if force:
|
|
57
|
+
cmd.append("-f")
|
|
58
|
+
cmd.extend([name, "-m", message])
|
|
59
|
+
_run_git(cmd, dry_run=dry_run)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def get_current_branch() -> str:
|
|
63
|
+
return _run_git(["rev-parse", "--abbrev-ref", "HEAD"]).strip()
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def get_semver_tags(tag_prefix: str, prerelease: str | None = None) -> list[str]:
|
|
67
|
+
output = _run_git(["tag", "-l", f"{tag_prefix}*"])
|
|
68
|
+
if not output.strip():
|
|
69
|
+
return []
|
|
70
|
+
|
|
71
|
+
tags = []
|
|
72
|
+
prefix_pattern = re.compile(f"^{re.escape(tag_prefix)}")
|
|
73
|
+
for line in output.strip().split("\n"):
|
|
74
|
+
stripped = prefix_pattern.sub("", line.strip())
|
|
75
|
+
if semver.Version.is_valid(stripped):
|
|
76
|
+
tags.append(stripped)
|
|
77
|
+
|
|
78
|
+
if prerelease is not None:
|
|
79
|
+
filtered = []
|
|
80
|
+
for tag in tags:
|
|
81
|
+
v = semver.Version.parse(tag)
|
|
82
|
+
if v.prerelease is None:
|
|
83
|
+
filtered.append(tag)
|
|
84
|
+
elif v.prerelease.split(".")[0] == prerelease:
|
|
85
|
+
filtered.append(tag)
|
|
86
|
+
tags = filtered
|
|
87
|
+
|
|
88
|
+
tags.sort(key=lambda t: semver.Version.parse(t), reverse=True)
|
|
89
|
+
return tags
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def get_latest_semver_tag(tag_prefix: str, prerelease: str | None = None) -> str:
|
|
93
|
+
tags = get_semver_tags(tag_prefix, prerelease=prerelease)
|
|
94
|
+
if not tags:
|
|
95
|
+
return "1.0.0"
|
|
96
|
+
return tags[0]
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def git_log_raw(
|
|
100
|
+
from_tag: str | None = None,
|
|
101
|
+
to: str = "HEAD",
|
|
102
|
+
path: str | None = None,
|
|
103
|
+
) -> list[str]:
|
|
104
|
+
if from_tag:
|
|
105
|
+
range_spec = f"{from_tag}..{to}"
|
|
106
|
+
else:
|
|
107
|
+
range_spec = to
|
|
108
|
+
cmd = ["log", range_spec, "--format=%B%x00"]
|
|
109
|
+
if path:
|
|
110
|
+
cmd.extend(["--", path])
|
|
111
|
+
output = _run_git(cmd)
|
|
112
|
+
if not output.strip():
|
|
113
|
+
return []
|
|
114
|
+
messages = [m.strip() for m in output.split("\x00") if m.strip()]
|
|
115
|
+
return messages
|
|
File without changes
|