git-version-sync 1.4.0__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.
File without changes
@@ -0,0 +1,4 @@
1
+ from .cli import main
2
+
3
+ if __name__ == "__main__":
4
+ main()
@@ -0,0 +1,36 @@
1
+ from git_version_sync.models import BumpRequest
2
+ from .parser import create_parser
3
+ from .core import do_check, do_bump, do_sync
4
+
5
+ def main():
6
+ parser = create_parser()
7
+ args = parser.parse_args()
8
+
9
+ try:
10
+ match args.command:
11
+ case 'bump':
12
+ bump_request = BumpRequest(
13
+ args.part,
14
+ tag_message=args.message,
15
+ force=args.force,
16
+ push=args.push,
17
+ release=args.release,
18
+ draft=args.draft,
19
+ )
20
+ print(do_bump(bump_request))
21
+
22
+ case 'sync':
23
+ print(do_sync(args.to_git, args.to_config))
24
+
25
+ case 'check':
26
+ print(do_check(args.fetch))
27
+
28
+ case _:
29
+ print(f"Invalid command {args.command}")
30
+
31
+ print("\n")
32
+ except Exception as e:
33
+ print(f"{e}")
34
+
35
+ if __name__ == "__main__":
36
+ main()
@@ -0,0 +1,3 @@
1
+ from .check import do_check
2
+ from .bump import do_bump
3
+ from .sync import do_sync
@@ -0,0 +1,107 @@
1
+ import subprocess, re
2
+ from packaging.version import Version
3
+
4
+ from .git import commit_config_change, push_to_remote, create_github_release
5
+ from .check import parse_highest_verion, get_local_tags, get_config_tag
6
+ from ..models import BumpRequest
7
+ from ..utils import get_config_path
8
+
9
+ def bump_git_tag(new_version: Version, message: str|None = None) -> None:
10
+ msg = message if message and message.strip() else f"bump version to v{new_version}"
11
+ command = [
12
+ 'git',
13
+ 'tag',
14
+ '-a', f'v{new_version}',
15
+ '-m', msg
16
+ ]
17
+
18
+ subprocess.run(
19
+ command,
20
+ capture_output=True,
21
+ text=True,
22
+ check=True
23
+ )
24
+
25
+ def bump_config_version(new_version: Version) -> None:
26
+ config_path = get_config_path()
27
+ content = config_path.read_text(encoding="utf-8")
28
+
29
+ pattern = r'^(version\s*=\s*["\']).*?(["\'])'
30
+ replacement = rf'\g<1>{new_version}\g<2>'
31
+
32
+ new_content, count = re.subn(pattern, replacement, content, flags=re.MULTILINE)
33
+
34
+ if count == 0:
35
+ raise RuntimeError(f"Version field not found in {config_path.stem}")
36
+
37
+ config_path.write_text(new_content, encoding="utf-8")
38
+
39
+ def bump_version(
40
+ request: BumpRequest,
41
+ new_version: Version,
42
+ ) -> None:
43
+ print("Updating pyproject.toml version...")
44
+ bump_config_version(new_version)
45
+
46
+ print("Committing new change...")
47
+ commit_config_change(new_version)
48
+
49
+ print(f"Creating git tag v{new_version}...")
50
+ bump_git_tag(new_version, request.tag_message)
51
+
52
+ if request.push:
53
+ print("Pushing commit and tag to remote...")
54
+ push_to_remote(new_version)
55
+
56
+ if request.release is not None:
57
+ print(f"Creating GitHub Release for v{new_version}...")
58
+ create_github_release(new_version, request.release, request.draft)
59
+
60
+ def get_new_major(version: Version) -> str:
61
+ return f"{version.major + 1}.0.0"
62
+
63
+ def get_new_minor(version: Version) -> str:
64
+ return f"{version.major}.{version.minor + 1}.0"
65
+
66
+ def get_new_patch(version: Version):
67
+ return f"{version.major}.{version.minor}.{version.micro + 1}"
68
+
69
+ def do_bump(request: BumpRequest):
70
+ config_tag = get_config_tag()
71
+ local_tags = get_local_tags()
72
+ highest_local_tag = parse_highest_verion(local_tags)
73
+
74
+ if config_tag != highest_local_tag and not request.force:
75
+ raise RuntimeError(
76
+ f"Version mismatch detected!\n"
77
+ f" Config: v{config_tag}\n"
78
+ f" Git : v{highest_local_tag}\n"
79
+ f"Please run `git-version-sync sync` first or fix the mismatch."
80
+ )
81
+
82
+ if highest_local_tag:
83
+ old_version = max(config_tag, highest_local_tag)
84
+ else:
85
+ old_version = config_tag
86
+
87
+ config_path = get_config_path()
88
+
89
+ if not config_path.exists():
90
+ raise RuntimeError(f"Config file not found: {config_path}")
91
+
92
+ match request.bump_type:
93
+ case "major":
94
+ new_version = get_new_major(old_version)
95
+ case "minor":
96
+ new_version = get_new_minor(old_version)
97
+ case "patch":
98
+ new_version = get_new_patch(old_version)
99
+
100
+ new_version = Version(new_version)
101
+
102
+ bump_version(
103
+ request,
104
+ new_version
105
+ )
106
+
107
+ return f"\nSuccess bump version to v{new_version}"
@@ -0,0 +1,101 @@
1
+ import subprocess, tomllib
2
+ from packaging.version import Version
3
+ from git_version_sync.utils import get_git_path, get_config_path
4
+
5
+
6
+ def get_local_tags() -> set[str]:
7
+ command = ["git", "tag", "--list"]
8
+ result = subprocess.run(
9
+ command,
10
+ capture_output=True,
11
+ text=True,
12
+ check=True,
13
+ )
14
+
15
+ tags = {tag for tag in result.stdout.strip().splitlines()}
16
+
17
+ return tags
18
+
19
+ def parse_highest_verion(tags: set[str]) -> Version|None:
20
+ valid_version = []
21
+ for tag in tags:
22
+ try:
23
+ clean_tag = tag.removeprefix("v")
24
+ valid_version.append(Version(clean_tag))
25
+ except Exception:
26
+ continue
27
+
28
+ return max(valid_version) if valid_version else None
29
+
30
+ def get_config_tag() -> Version:
31
+ config_path = get_config_path()
32
+
33
+ with config_path.open('rb') as f:
34
+ config = tomllib.load(f)
35
+ project_config = config.get('project', {})
36
+ config_tag = project_config.get("version", None)
37
+
38
+ if config_tag is None:
39
+ raise ValueError("No version found in project config.")
40
+
41
+ return Version(config_tag)
42
+
43
+ def get_remote_tags() -> set[str]:
44
+ command = ['git', 'ls-remote', '--tags', 'origin']
45
+ result = subprocess.run(
46
+ command,
47
+ capture_output=True,
48
+ text=True,
49
+ check=True
50
+ )
51
+
52
+ if result.returncode != 0:
53
+ return set()
54
+
55
+ remote_tags = set()
56
+ for line in result.stdout.strip().splitlines():
57
+ if not line:
58
+ continue
59
+
60
+ parts = line.split()
61
+ if len(parts) == 2:
62
+ ref = parts[1]
63
+ if ref.endswith("^{}"):
64
+ continue
65
+ tag_name = ref.removeprefix("refs/tags/")
66
+ remote_tags.add(tag_name)
67
+
68
+ return remote_tags
69
+
70
+ def do_check(fetch_true: bool = False) -> str:
71
+ config_tag = get_config_tag()
72
+ local_tags = get_local_tags()
73
+ remote_tags = get_remote_tags()
74
+
75
+ output = []
76
+
77
+ highest_local_version = parse_highest_verion(local_tags)
78
+ if highest_local_version is None:
79
+ return "No local tags found."
80
+
81
+ if highest_local_version == config_tag:
82
+ output.append(f"Version is synchronized with highest local tag (v{config_tag})")
83
+ else:
84
+ output.append(
85
+ f"Version mismatch\n"
86
+ f"Git Local: {highest_local_version}\n"
87
+ f"Config: {config_tag}"
88
+ )
89
+
90
+ if fetch_true:
91
+ missing_in_local = remote_tags - local_tags
92
+ if missing_in_local:
93
+ output.append(f"New tag(s) found from remote: ")
94
+ for tag in sorted(missing_in_local):
95
+ output.append(f" - {tag}")
96
+ output.append("")
97
+
98
+ return "\n".join(output)
99
+
100
+ if __name__ == "__main__":
101
+ print(do_check())
@@ -0,0 +1,66 @@
1
+ import subprocess, shutil
2
+
3
+ from packaging.version import Version
4
+ from git_version_sync.utils import get_config_path
5
+
6
+ def commit_config_change(new_version: Version) -> None:
7
+ subprocess.run([
8
+ 'git', 'add', str(get_config_path())],
9
+ capture_output=True,
10
+ text=True,
11
+ check=True
12
+ )
13
+
14
+ try:
15
+ commit_msg = f"chore({get_config_path().name}): bump version to v{new_version}"
16
+ subprocess.run(
17
+ ['git', 'commit', '-m', commit_msg],
18
+ capture_output=True,
19
+ text=True,
20
+ check=True
21
+ )
22
+ except subprocess.CalledProcessError as e:
23
+ output = (e.stdout or "") + (e.stderr or "")
24
+ if "nothing to commit" in output:
25
+ return
26
+ raise RuntimeError(f"Git commit failed: \n{e.stderr.strip()}") from e
27
+
28
+ def push_to_remote(new_version: Version) -> None:
29
+ try:
30
+ command = [
31
+ 'git', 'push',
32
+ 'origin', 'HEAD',
33
+ f'v{new_version}'
34
+ ]
35
+
36
+ subprocess.run(
37
+ command,
38
+ capture_output=True,
39
+ text=True,
40
+ check=True
41
+ )
42
+ except subprocess.CalledProcessError as e:
43
+ raise RuntimeError(f"Failed to push to remote: \n{e.stderr.strip()}") from e
44
+
45
+ def create_github_release(version: Version, message: str|None=None, draft: bool=False):
46
+ tag_name = f"v{version}"
47
+ command = ['gh', 'release', 'create', tag_name, '--generate-notes']
48
+
49
+ if not shutil.which('gh'):
50
+ raise RuntimeError("Github CLI ('gh') not installed yet, please install 'gh' first.")
51
+
52
+ if message and message.strip():
53
+ command.extend(['--notes', message])
54
+ if draft:
55
+ command.extend(['--draft'])
56
+
57
+ push_to_remote(version)
58
+ try:
59
+ subprocess.run(
60
+ command,
61
+ capture_output=True,
62
+ text=True,
63
+ check=True
64
+ )
65
+ except subprocess.CalledProcessError as e:
66
+ raise RuntimeError(f"Failed to create release tag: \n{e.stderr.strip()}") from e
@@ -0,0 +1,34 @@
1
+ from .bump import bump_config_version, bump_git_tag
2
+ from .check import get_config_tag, get_local_tags, parse_highest_verion
3
+
4
+ def do_sync(to_git: bool=False, to_config: bool=False) -> str:
5
+ config_tag = get_config_tag()
6
+ local_tags = get_local_tags()
7
+ highest_local_tag = parse_highest_verion(local_tags)
8
+
9
+ if config_tag == highest_local_tag:
10
+ return f"Already in sync at (v{config_tag})"
11
+
12
+ if to_git:
13
+ if highest_local_tag:
14
+ bump_config_version(highest_local_tag)
15
+ return f"Synced config version to match Git tag v{highest_local_tag}"
16
+ else:
17
+ raise RuntimeError("No git tag found on local.")
18
+
19
+ elif to_config and highest_local_tag:
20
+ bump_git_tag(config_tag, message=f"Sync git tag to v{config_tag}")
21
+ return f"Synced Git tag to match config version v{config_tag}"
22
+
23
+ else:
24
+ if highest_local_tag:
25
+ target_version = max(config_tag, highest_local_tag)
26
+ else:
27
+ target_version = config_tag
28
+
29
+ if highest_local_tag and highest_local_tag > config_tag:
30
+ bump_config_version(target_version)
31
+ else:
32
+ bump_git_tag(target_version)
33
+
34
+ return f"Synced workspace to highest version v{target_version}"
@@ -0,0 +1 @@
1
+ from .bump import BumpRequest
@@ -0,0 +1,13 @@
1
+ from dataclasses import dataclass
2
+ from typing import Literal
3
+
4
+ BumpType = Literal["major", "minor", "patch"]
5
+
6
+ @dataclass(frozen=True)
7
+ class BumpRequest:
8
+ bump_type : BumpType
9
+ tag_message : str|None
10
+ force : bool
11
+ push : bool
12
+ release : str|None
13
+ draft : bool
@@ -0,0 +1,83 @@
1
+ import argparse
2
+
3
+ def create_parser():
4
+ parser = argparse.ArgumentParser(
5
+ prog="git-version-sync",
6
+ description="Sync Git tags and pyproject.toml versions.",
7
+ )
8
+
9
+ subparsers = parser.add_subparsers(dest="command", required=True)
10
+
11
+ # Subcommand: check
12
+ check_parser = subparsers.add_parser(
13
+ "check",
14
+ help="Check and compare current version status between Git tags and pyproject.toml"
15
+ )
16
+ check_parser.add_argument(
17
+ "--fetch",
18
+ action="store_true",
19
+ help="Fetch remote tags before checking"
20
+ )
21
+
22
+ # Subcommand: sync
23
+ sync_parser = subparsers.add_parser(
24
+ "sync",
25
+ help="Sync version discrepancies between pyproject.toml and Git tags"
26
+ )
27
+ sync_group = sync_parser.add_mutually_exclusive_group()
28
+ sync_group.add_argument(
29
+ "--to-git",
30
+ action="store_true",
31
+ help="Force config version (pyproject.toml) to match the highest Git tag"
32
+ )
33
+ sync_group.add_argument(
34
+ "--to-config",
35
+ action="store_true",
36
+ help="Force Git tag to match the version in pyproject.toml"
37
+ )
38
+
39
+ # Subcommand: bump
40
+ bump_parser = subparsers.add_parser(
41
+ "bump",
42
+ help="Increment version in pyproject.toml and create a corresponding Git tag"
43
+ )
44
+ bump_parser.add_argument(
45
+ "part",
46
+ choices=["major", "minor", "patch"],
47
+ help="Version part to increment (major, minor, or patch)"
48
+ )
49
+ bump_parser.add_argument(
50
+ "-f",
51
+ "--force",
52
+ action="store_true",
53
+ help="Force bump even if version mismatch occurs"
54
+ )
55
+ bump_parser.add_argument(
56
+ "-p",
57
+ "--push",
58
+ action="store_true",
59
+ help="Automatically push commit and the new tag to remote"
60
+ )
61
+ bump_parser.add_argument(
62
+ "-m",
63
+ "--message",
64
+ type=str,
65
+ help="Custom annotation message for the created Git tag"
66
+ )
67
+ bump_parser.add_argument(
68
+ "-r",
69
+ "--release",
70
+ nargs="?",
71
+ const="",
72
+ default=None,
73
+ metavar="NOTES",
74
+ help="Create a GitHub release for the bumped version (requires 'gh' CLI)"
75
+ )
76
+ bump_parser.add_argument(
77
+ "-d",
78
+ "--draft",
79
+ action="store_true",
80
+ help="Save the GitHub release as a draft (requires --release)",
81
+ )
82
+
83
+ return parser
@@ -0,0 +1 @@
1
+ from .config import get_git_path, get_config_path
@@ -0,0 +1,20 @@
1
+ import subprocess
2
+ from pathlib import Path
3
+
4
+ def get_git_path() -> Path:
5
+ command = ["git", "rev-parse", "--show-toplevel"]
6
+ result = subprocess.run(
7
+ command,
8
+ capture_output=True,
9
+ text=True,
10
+ check=True
11
+ )
12
+
13
+ return Path(result.stdout.strip())
14
+
15
+ def get_config_path() -> Path:
16
+ config_path = get_git_path() / "pyproject.toml"
17
+ if config_path.exists():
18
+ return config_path
19
+ else:
20
+ raise RuntimeError(f"Config file not found: {config_path}")
@@ -0,0 +1,136 @@
1
+ Metadata-Version: 2.3
2
+ Name: git-version-sync
3
+ Version: 1.4.0
4
+ Summary: Tools to sync and update semantic versions
5
+ Keywords: git,versioning,semver,pyproject,cli
6
+ Author: Finsa-SC
7
+ Author-email: Finsa-SC <finsakusumaputra@gmail.com>
8
+ License: MIT
9
+ Requires-Dist: packaging>=26.3
10
+ Requires-Python: >=3.11
11
+ Project-URL: Homepage, https://github.com/Finsa-SC/git-version-sync
12
+ Project-URL: Repository, https://github.com/Finsa-SC/git-version-sync
13
+ Project-URL: Issues, https://github.com/Finsa-SC/git-version-sync/issues
14
+ Description-Content-Type: text/markdown
15
+
16
+ # git-version-sync
17
+
18
+ A command-line tool to synchronize semantic versions between Git tags and `pyproject.toml`.
19
+
20
+ ## Features
21
+
22
+ - **Check** version status and consistency between Git tags and `pyproject.toml`
23
+ - **Sync** version discrepancies with flexible sync directions
24
+ - **Bump** versions following semantic versioning (major, minor, patch)
25
+ - **Auto Push** option to push commits and tags directly to remote
26
+ - **Custom annotations** for Git tags
27
+ - **Force mode** for bypassing version mismatches
28
+
29
+ ## Installation
30
+
31
+ ### Requirements
32
+ - Python >= 3.11
33
+ - Git
34
+
35
+ ### Via PyPI (Recommended)
36
+
37
+ ```bash
38
+ pip install git-version-sync
39
+ ```
40
+
41
+ Or with pipx for isolated installation:
42
+
43
+ ```bash
44
+ pipx install git-version-sync
45
+ ```
46
+
47
+ ### From Source
48
+
49
+ ```bash
50
+ git clone <repository-url>
51
+ cd git-version-sync
52
+ pip install -e .
53
+ ```
54
+
55
+ ## Usage
56
+
57
+ ### Check Command
58
+
59
+ Verify version consistency between Git tags and `pyproject.toml`:
60
+
61
+ ```bash
62
+ git-version-sync check
63
+ ```
64
+
65
+ **Options:**
66
+ - `--fetch` - Fetch remote tags before checking
67
+
68
+ ### Sync Command
69
+
70
+ Synchronize versions when discrepancies are detected:
71
+
72
+ ```bash
73
+ git-version-sync sync
74
+ ```
75
+
76
+ **Options (mutually exclusive):**
77
+ - `--to-git` - Force `pyproject.toml` version to match the highest Git tag
78
+ - `--to-config` - Force Git tag to match `pyproject.toml` version
79
+
80
+ ### Bump Command
81
+
82
+ Increment the version in `pyproject.toml` and create a corresponding Git tag:
83
+
84
+ ```bash
85
+ git-version-sync bump {major|minor|patch}
86
+ ```
87
+
88
+ **Options:**
89
+ - `-f, --force` - Force bump even if version mismatch occurs
90
+ - `-p, --push` - Automatically push commit and tag to remote
91
+ - `-m, --message MESSAGE` - Custom annotation message for the Git tag
92
+
93
+ ## Examples
94
+
95
+ ### Check current version status
96
+ ```bash
97
+ $ git-version-sync check
98
+ Version is synchronized with highest local tag (v1.0.0)
99
+ ```
100
+
101
+ ### Bump patch version
102
+ ```bash
103
+ $ git-version-sync bump patch
104
+ Success bump version to v1.0.1
105
+ ```
106
+
107
+ ### Bump minor version with custom message and auto-push
108
+ ```bash
109
+ $ git-version-sync bump minor -m "Add new features" -p
110
+ Success bump version to v1.1.0
111
+ Pushing commit and tag to remote...
112
+ ```
113
+
114
+ ### Bump major version with force flag
115
+ ```bash
116
+ $ git-version-sync bump major -f
117
+ Success bump version to v2.0.0
118
+ ```
119
+
120
+ ### Sync to Git tags
121
+ ```bash
122
+ $ git-version-sync sync --to-git
123
+ Successfully synced version to match highest tag
124
+ ```
125
+
126
+ ## Dependencies
127
+
128
+ - `packaging>=26.3` - For version parsing and comparison
129
+
130
+ ## License
131
+
132
+ See LICENSE file for details.
133
+
134
+ ## Author
135
+
136
+ Finsa-SC
@@ -0,0 +1,17 @@
1
+ git_version_sync/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
+ git_version_sync/__main__.py,sha256=0g3iknXOS9gZUcpL_trgAcuCJnZZKjdsT_xt61WOVb4,60
3
+ git_version_sync/cli.py,sha256=-PQ_HZ6XahKdIhFQQmnLksS7z3o-G3fmBpJe8y6QC4Y,944
4
+ git_version_sync/core/__init__.py,sha256=5rc6piRQPZeqxBFN5V2lRdjdPGKREJtW1zh77msNuLc,79
5
+ git_version_sync/core/bump.py,sha256=NbbLsV4PcWmgLIzYg-1YIPRrZyYyrbXTLmSTmmtpI2E,3214
6
+ git_version_sync/core/check.py,sha256=8mNbE3g9fI3ZsOQUCW65s7sBa6SlCWCWJMM3ClN0CVM,2717
7
+ git_version_sync/core/git.py,sha256=7CPRgXiAoCp-G6AKdiwh5ChDFqrhnZZY0NXoOUogstI,2012
8
+ git_version_sync/core/sync.py,sha256=Yg_XA730DxI5PvaIpG4n05Zv9qOD9ZjbjVsSRhkQNQY,1250
9
+ git_version_sync/models/__init__.py,sha256=aW3l0SjQ5DuGh9gkrU7KO-kqp3notaCJO4humOKeEYk,29
10
+ git_version_sync/models/bump.py,sha256=ndCF9FL4baNc9h15GNTKoGAfSaBOqwqMllb_1-fCvcA,301
11
+ git_version_sync/parser.py,sha256=XDLVfZJJsuYFly1ucrpOgCds-q0OlQrmOptcg5wHwU0,2349
12
+ git_version_sync/utils/__init__.py,sha256=SlMounMaDolLS1rMQ4nIZlOaupbsu6GFql4IyU8DDqI,49
13
+ git_version_sync/utils/config.py,sha256=LtkXM_SJUtuKm2iZ5tF2-6poez2VIso-39FHiiF6tlI,502
14
+ git_version_sync-1.4.0.dist-info/WHEEL,sha256=7hzKWg-J8I3Buqyw5tBii5z_MmAVsDYXXijd_QAtNZ8,81
15
+ git_version_sync-1.4.0.dist-info/entry_points.txt,sha256=wR2mKbwOuvSbzPstfLGoA6-Qbm2amNdhUsYfYZJCqT0,64
16
+ git_version_sync-1.4.0.dist-info/METADATA,sha256=noL5_ZO-VDIsse_kykLym7y--xTJqPRcb8lSXiPtiIs,2985
17
+ git_version_sync-1.4.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: uv 0.12.18
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,3 @@
1
+ [console_scripts]
2
+ git-version-sync = git_version_sync.cli:main
3
+