tgit 0.34.1__tar.gz → 0.34.2__tar.gz
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.
- {tgit-0.34.1 → tgit-0.34.2}/CHANGELOG.md +12 -0
- {tgit-0.34.1 → tgit-0.34.2}/PKG-INFO +1 -1
- {tgit-0.34.1 → tgit-0.34.2}/pyproject.toml +1 -1
- {tgit-0.34.1 → tgit-0.34.2}/tests/unit/test_version.py +236 -0
- {tgit-0.34.1 → tgit-0.34.2}/tgit/version.py +93 -6
- {tgit-0.34.1 → tgit-0.34.2}/uv.lock +1 -1
- tgit-0.34.1/.claude/settings.local.json +0 -17
- {tgit-0.34.1 → tgit-0.34.2}/.github/workflows/build.yml +0 -0
- {tgit-0.34.1 → tgit-0.34.2}/.github/workflows/ci.yml +0 -0
- {tgit-0.34.1 → tgit-0.34.2}/.github/workflows/claude-code-review.yml +0 -0
- {tgit-0.34.1 → tgit-0.34.2}/.github/workflows/claude.yml +0 -0
- {tgit-0.34.1 → tgit-0.34.2}/.gitignore +0 -0
- {tgit-0.34.1 → tgit-0.34.2}/.python-version +0 -0
- {tgit-0.34.1 → tgit-0.34.2}/.tgit/settings.json +0 -0
- {tgit-0.34.1 → tgit-0.34.2}/.vscode/extensions.json +0 -0
- {tgit-0.34.1 → tgit-0.34.2}/.vscode/launch.json +0 -0
- {tgit-0.34.1 → tgit-0.34.2}/AGENTS.md +0 -0
- {tgit-0.34.1 → tgit-0.34.2}/CLAUDE.md +0 -0
- {tgit-0.34.1 → tgit-0.34.2}/LICENSE +0 -0
- {tgit-0.34.1 → tgit-0.34.2}/README.md +0 -0
- {tgit-0.34.1 → tgit-0.34.2}/pyrightconfig.json +0 -0
- {tgit-0.34.1 → tgit-0.34.2}/scripts/publish.sh +0 -0
- {tgit-0.34.1 → tgit-0.34.2}/scripts/test.sh +0 -0
- {tgit-0.34.1 → tgit-0.34.2}/tests/README.md +0 -0
- {tgit-0.34.1 → tgit-0.34.2}/tests/__init__.py +0 -0
- {tgit-0.34.1 → tgit-0.34.2}/tests/conftest.py +0 -0
- {tgit-0.34.1 → tgit-0.34.2}/tests/integration/__init__.py +0 -0
- {tgit-0.34.1 → tgit-0.34.2}/tests/integration/test_version_integration.py +0 -0
- {tgit-0.34.1 → tgit-0.34.2}/tests/unit/__init__.py +0 -0
- {tgit-0.34.1 → tgit-0.34.2}/tests/unit/test_add.py +0 -0
- {tgit-0.34.1 → tgit-0.34.2}/tests/unit/test_changelog.py +0 -0
- {tgit-0.34.1 → tgit-0.34.2}/tests/unit/test_changelog_coverage.py +0 -0
- {tgit-0.34.1 → tgit-0.34.2}/tests/unit/test_cli.py +0 -0
- {tgit-0.34.1 → tgit-0.34.2}/tests/unit/test_commit.py +0 -0
- {tgit-0.34.1 → tgit-0.34.2}/tests/unit/test_commit_coverage.py +0 -0
- {tgit-0.34.1 → tgit-0.34.2}/tests/unit/test_interactive_settings.py +0 -0
- {tgit-0.34.1 → tgit-0.34.2}/tests/unit/test_settings.py +0 -0
- {tgit-0.34.1 → tgit-0.34.2}/tests/unit/test_types.py +0 -0
- {tgit-0.34.1 → tgit-0.34.2}/tests/unit/test_utils.py +0 -0
- {tgit-0.34.1 → tgit-0.34.2}/tests/unit/test_utils_coverage.py +0 -0
- {tgit-0.34.1 → tgit-0.34.2}/tests/unit/test_version_coverage.py +0 -0
- {tgit-0.34.1 → tgit-0.34.2}/tgit/__init__.py +0 -0
- {tgit-0.34.1 → tgit-0.34.2}/tgit/add.py +0 -0
- {tgit-0.34.1 → tgit-0.34.2}/tgit/changelog.py +0 -0
- {tgit-0.34.1 → tgit-0.34.2}/tgit/cli.py +0 -0
- {tgit-0.34.1 → tgit-0.34.2}/tgit/commit.py +0 -0
- {tgit-0.34.1 → tgit-0.34.2}/tgit/constants.py +0 -0
- {tgit-0.34.1 → tgit-0.34.2}/tgit/interactive_settings.py +0 -0
- {tgit-0.34.1 → tgit-0.34.2}/tgit/prompts/commit.txt +0 -0
- {tgit-0.34.1 → tgit-0.34.2}/tgit/py.typed +0 -0
- {tgit-0.34.1 → tgit-0.34.2}/tgit/settings.py +0 -0
- {tgit-0.34.1 → tgit-0.34.2}/tgit/shared.py +0 -0
- {tgit-0.34.1 → tgit-0.34.2}/tgit/types.py +0 -0
- {tgit-0.34.1 → tgit-0.34.2}/tgit/utils/__init__.py +0 -0
|
@@ -1,3 +1,15 @@
|
|
|
1
|
+
## v0.34.2
|
|
2
|
+
|
|
3
|
+
[v0.34.1...v0.34.2](https://github.com/Jannchie/tgit/compare/v0.34.1...v0.34.2)
|
|
4
|
+
|
|
5
|
+
### :adhesive_bandage: Fixes
|
|
6
|
+
|
|
7
|
+
- **version**: replace utf-8 errors handling in version file reads - By [Jannchie](mailto:jannchie@gmail.com) in [27dd1f8](https://github.com/Jannchie/tgit/commit/27dd1f8)
|
|
8
|
+
|
|
9
|
+
### :test_tube: Tests
|
|
10
|
+
|
|
11
|
+
- **cargo**: add unit tests for cargo version updates - By [Jianqi Pan](mailto:jannchie@gmail.com) in [bce5042](https://github.com/Jannchie/tgit/commit/bce5042)
|
|
12
|
+
|
|
1
13
|
## v0.34.1
|
|
2
14
|
|
|
3
15
|
[v0.34.0...v0.34.1](https://github.com/Jannchie/tgit/compare/v0.34.0...v0.34.1)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: tgit
|
|
3
|
-
Version: 0.34.
|
|
3
|
+
Version: 0.34.2
|
|
4
4
|
Summary: AI-powered Git CLI for commits, changelogs, and semantic versioning.
|
|
5
5
|
Project-URL: Homepage, https://github.com/Jannchie/tgit
|
|
6
6
|
Project-URL: Repository, https://github.com/Jannchie/tgit
|
|
@@ -10,6 +10,8 @@ from tgit.version import (
|
|
|
10
10
|
VersionArgs,
|
|
11
11
|
VersionChoice,
|
|
12
12
|
_apply_version_choice,
|
|
13
|
+
_find_cargo_lock_for,
|
|
14
|
+
_get_cargo_package_name,
|
|
13
15
|
_get_default_bump_from_commits,
|
|
14
16
|
_handle_explicit_version_args,
|
|
15
17
|
_handle_interactive_version_selection,
|
|
@@ -37,6 +39,7 @@ from tgit.version import (
|
|
|
37
39
|
get_version_from_version_txt,
|
|
38
40
|
handle_version,
|
|
39
41
|
show_file_diff,
|
|
42
|
+
update_cargo_lock_version,
|
|
40
43
|
update_cargo_toml_version,
|
|
41
44
|
update_file,
|
|
42
45
|
update_version_files,
|
|
@@ -941,6 +944,239 @@ members = ["other-crate"]
|
|
|
941
944
|
update_cargo_toml_version(non_existent_file, "1.0.0", 0, show_diff=False)
|
|
942
945
|
|
|
943
946
|
|
|
947
|
+
class TestGetCargoPackageName:
|
|
948
|
+
"""Test cases for _get_cargo_package_name."""
|
|
949
|
+
|
|
950
|
+
def test_basic(self, tmp_path):
|
|
951
|
+
cargo_toml = tmp_path / "Cargo.toml"
|
|
952
|
+
cargo_toml.write_text('[package]\nname = "arthash"\nversion = "0.2.0"\n')
|
|
953
|
+
assert _get_cargo_package_name(cargo_toml) == "arthash"
|
|
954
|
+
|
|
955
|
+
def test_missing_file(self, tmp_path):
|
|
956
|
+
assert _get_cargo_package_name(tmp_path / "Cargo.toml") is None
|
|
957
|
+
|
|
958
|
+
def test_missing_package_section(self, tmp_path):
|
|
959
|
+
cargo_toml = tmp_path / "Cargo.toml"
|
|
960
|
+
cargo_toml.write_text('[workspace]\nmembers = ["a"]\n')
|
|
961
|
+
assert _get_cargo_package_name(cargo_toml) is None
|
|
962
|
+
|
|
963
|
+
def test_missing_name_key(self, tmp_path):
|
|
964
|
+
cargo_toml = tmp_path / "Cargo.toml"
|
|
965
|
+
cargo_toml.write_text('[package]\nversion = "0.1.0"\n')
|
|
966
|
+
assert _get_cargo_package_name(cargo_toml) is None
|
|
967
|
+
|
|
968
|
+
def test_empty_name(self, tmp_path):
|
|
969
|
+
cargo_toml = tmp_path / "Cargo.toml"
|
|
970
|
+
cargo_toml.write_text('[package]\nname = ""\nversion = "0.1.0"\n')
|
|
971
|
+
assert _get_cargo_package_name(cargo_toml) is None
|
|
972
|
+
|
|
973
|
+
def test_invalid_toml(self, tmp_path):
|
|
974
|
+
cargo_toml = tmp_path / "Cargo.toml"
|
|
975
|
+
cargo_toml.write_text("this is not valid toml [[[")
|
|
976
|
+
assert _get_cargo_package_name(cargo_toml) is None
|
|
977
|
+
|
|
978
|
+
|
|
979
|
+
class TestFindCargoLockFor:
|
|
980
|
+
"""Test cases for _find_cargo_lock_for."""
|
|
981
|
+
|
|
982
|
+
def test_same_directory(self, tmp_path):
|
|
983
|
+
"""Single-crate layout: Cargo.toml and Cargo.lock side-by-side."""
|
|
984
|
+
(tmp_path / "Cargo.toml").write_text('[package]\nname = "x"\nversion = "0.1.0"\n')
|
|
985
|
+
lockfile = tmp_path / "Cargo.lock"
|
|
986
|
+
lockfile.write_text("# lock\n")
|
|
987
|
+
assert _find_cargo_lock_for(tmp_path) == lockfile
|
|
988
|
+
|
|
989
|
+
def test_parent_directory(self, tmp_path):
|
|
990
|
+
"""Workspace layout: lockfile lives at workspace root."""
|
|
991
|
+
(tmp_path / ".git").mkdir()
|
|
992
|
+
lockfile = tmp_path / "Cargo.lock"
|
|
993
|
+
lockfile.write_text("# lock\n")
|
|
994
|
+
member = tmp_path / "crates" / "alpha"
|
|
995
|
+
member.mkdir(parents=True)
|
|
996
|
+
(member / "Cargo.toml").write_text('[package]\nname = "alpha"\nversion = "0.1.0"\n')
|
|
997
|
+
assert _find_cargo_lock_for(member) == lockfile
|
|
998
|
+
|
|
999
|
+
def test_no_lockfile_returns_none(self, tmp_path):
|
|
1000
|
+
(tmp_path / ".git").mkdir()
|
|
1001
|
+
crate = tmp_path / "crate"
|
|
1002
|
+
crate.mkdir()
|
|
1003
|
+
(crate / "Cargo.toml").write_text('[package]\nname = "x"\nversion = "0.1.0"\n')
|
|
1004
|
+
assert _find_cargo_lock_for(crate) is None
|
|
1005
|
+
|
|
1006
|
+
def test_stops_at_git_root(self, tmp_path):
|
|
1007
|
+
"""Must not escape the current repo, even if an outer Cargo.lock exists."""
|
|
1008
|
+
outer_lock = tmp_path / "Cargo.lock"
|
|
1009
|
+
outer_lock.write_text("# outer\n")
|
|
1010
|
+
repo = tmp_path / "inner_repo"
|
|
1011
|
+
repo.mkdir()
|
|
1012
|
+
(repo / ".git").mkdir()
|
|
1013
|
+
crate = repo / "crate"
|
|
1014
|
+
crate.mkdir()
|
|
1015
|
+
(crate / "Cargo.toml").write_text('[package]\nname = "x"\nversion = "0.1.0"\n')
|
|
1016
|
+
# Should NOT find the outer Cargo.lock — that's in a different repo.
|
|
1017
|
+
assert _find_cargo_lock_for(crate) is None
|
|
1018
|
+
|
|
1019
|
+
|
|
1020
|
+
class TestUpdateCargoLockVersion:
|
|
1021
|
+
"""Test cases for update_cargo_lock_version."""
|
|
1022
|
+
|
|
1023
|
+
@staticmethod
|
|
1024
|
+
def _make_lockfile(tmp_path: Path, content: str) -> Path:
|
|
1025
|
+
lockfile = tmp_path / "Cargo.lock"
|
|
1026
|
+
lockfile.write_text(content, encoding="utf-8")
|
|
1027
|
+
return lockfile
|
|
1028
|
+
|
|
1029
|
+
def test_local_crate_version_updated(self, tmp_path):
|
|
1030
|
+
"""Workspace member (no source field) gets its version rewritten."""
|
|
1031
|
+
content = (
|
|
1032
|
+
"# This file is automatically @generated by Cargo.\n"
|
|
1033
|
+
"version = 4\n\n"
|
|
1034
|
+
"[[package]]\n"
|
|
1035
|
+
'name = "arthash"\n'
|
|
1036
|
+
'version = "0.2.0"\n'
|
|
1037
|
+
"dependencies = [\n"
|
|
1038
|
+
' "matrixmultiply",\n'
|
|
1039
|
+
"]\n"
|
|
1040
|
+
)
|
|
1041
|
+
lockfile = self._make_lockfile(tmp_path, content)
|
|
1042
|
+
update_cargo_lock_version("arthash", "0.3.0", lockfile, 0, show_diff=False)
|
|
1043
|
+
new_content = lockfile.read_text()
|
|
1044
|
+
assert 'version = "0.3.0"' in new_content
|
|
1045
|
+
assert 'version = "0.2.0"' not in new_content
|
|
1046
|
+
|
|
1047
|
+
def test_registry_dep_with_same_name_not_touched(self, tmp_path):
|
|
1048
|
+
"""A registry dep named identically to the local crate must NOT be hit.
|
|
1049
|
+
|
|
1050
|
+
Registry entries have `source = "registry+..."` between `name` and
|
|
1051
|
+
`version`, so they fail the "name immediately followed by version"
|
|
1052
|
+
anchor. Local crates fit the anchor exactly.
|
|
1053
|
+
"""
|
|
1054
|
+
content = (
|
|
1055
|
+
"[[package]]\n"
|
|
1056
|
+
'name = "arthash"\n'
|
|
1057
|
+
'source = "registry+https://github.com/rust-lang/crates.io-index"\n'
|
|
1058
|
+
'version = "9.9.9"\n'
|
|
1059
|
+
'checksum = "deadbeef"\n\n'
|
|
1060
|
+
"[[package]]\n"
|
|
1061
|
+
'name = "arthash"\n'
|
|
1062
|
+
'version = "0.2.0"\n'
|
|
1063
|
+
"dependencies = [\n"
|
|
1064
|
+
' "matrixmultiply",\n'
|
|
1065
|
+
"]\n"
|
|
1066
|
+
)
|
|
1067
|
+
lockfile = self._make_lockfile(tmp_path, content)
|
|
1068
|
+
update_cargo_lock_version("arthash", "0.3.0", lockfile, 0, show_diff=False)
|
|
1069
|
+
new_content = lockfile.read_text()
|
|
1070
|
+
# registry entry untouched
|
|
1071
|
+
assert 'version = "9.9.9"' in new_content
|
|
1072
|
+
# local entry bumped
|
|
1073
|
+
assert 'version = "0.3.0"' in new_content
|
|
1074
|
+
# only one occurrence of the new version (didn't accidentally double-write)
|
|
1075
|
+
assert new_content.count('version = "0.3.0"') == 1
|
|
1076
|
+
|
|
1077
|
+
def test_crate_not_in_lockfile_is_noop(self, tmp_path):
|
|
1078
|
+
"""If the crate isn't recorded, nothing changes."""
|
|
1079
|
+
content = (
|
|
1080
|
+
"[[package]]\n"
|
|
1081
|
+
'name = "other"\n'
|
|
1082
|
+
'version = "1.0.0"\n'
|
|
1083
|
+
)
|
|
1084
|
+
lockfile = self._make_lockfile(tmp_path, content)
|
|
1085
|
+
update_cargo_lock_version("arthash", "0.3.0", lockfile, 0, show_diff=False)
|
|
1086
|
+
assert lockfile.read_text() == content
|
|
1087
|
+
|
|
1088
|
+
def test_missing_lockfile_is_noop(self, tmp_path):
|
|
1089
|
+
"""Pure non-Rust projects don't have a Cargo.lock — must not error."""
|
|
1090
|
+
lockfile = tmp_path / "Cargo.lock"
|
|
1091
|
+
# Should not raise.
|
|
1092
|
+
update_cargo_lock_version("arthash", "0.3.0", lockfile, 0, show_diff=False)
|
|
1093
|
+
assert not lockfile.exists()
|
|
1094
|
+
|
|
1095
|
+
def test_already_in_sync_is_noop(self, tmp_path):
|
|
1096
|
+
"""Idempotent: same version in -> file unchanged."""
|
|
1097
|
+
content = (
|
|
1098
|
+
"[[package]]\n"
|
|
1099
|
+
'name = "arthash"\n'
|
|
1100
|
+
'version = "0.3.0"\n'
|
|
1101
|
+
)
|
|
1102
|
+
lockfile = self._make_lockfile(tmp_path, content)
|
|
1103
|
+
before = lockfile.stat().st_mtime_ns
|
|
1104
|
+
update_cargo_lock_version("arthash", "0.3.0", lockfile, 0, show_diff=False)
|
|
1105
|
+
# Content identical; we explicitly skip the write in that case.
|
|
1106
|
+
assert lockfile.read_text() == content
|
|
1107
|
+
# And the file was not rewritten (mtime preserved). Some filesystems
|
|
1108
|
+
# have coarse mtime resolution, so this guard is a tightening only —
|
|
1109
|
+
# the content check above is the real assertion.
|
|
1110
|
+
assert lockfile.stat().st_mtime_ns == before
|
|
1111
|
+
|
|
1112
|
+
|
|
1113
|
+
class TestUpdateVersionInFileCargoIntegration:
|
|
1114
|
+
"""update_version_in_file should sync Cargo.lock when it sees Cargo.toml."""
|
|
1115
|
+
|
|
1116
|
+
def test_cargo_toml_branch_syncs_sibling_lockfile(self, tmp_path):
|
|
1117
|
+
cargo_toml = tmp_path / "Cargo.toml"
|
|
1118
|
+
cargo_toml.write_text(
|
|
1119
|
+
"[package]\n"
|
|
1120
|
+
'name = "arthash"\n'
|
|
1121
|
+
'version = "0.2.0"\n'
|
|
1122
|
+
"\n"
|
|
1123
|
+
"[dependencies]\n"
|
|
1124
|
+
'matrixmultiply = "0.3"\n',
|
|
1125
|
+
encoding="utf-8",
|
|
1126
|
+
)
|
|
1127
|
+
lockfile = tmp_path / "Cargo.lock"
|
|
1128
|
+
lockfile.write_text(
|
|
1129
|
+
"[[package]]\n"
|
|
1130
|
+
'name = "arthash"\n'
|
|
1131
|
+
'version = "0.2.0"\n'
|
|
1132
|
+
"dependencies = [\n"
|
|
1133
|
+
' "matrixmultiply",\n'
|
|
1134
|
+
"]\n",
|
|
1135
|
+
encoding="utf-8",
|
|
1136
|
+
)
|
|
1137
|
+
update_version_in_file(0, "0.3.0", "Cargo.toml", cargo_toml, show_diff=False)
|
|
1138
|
+
# Both files should now report 0.3.0.
|
|
1139
|
+
assert 'version = "0.3.0"' in cargo_toml.read_text()
|
|
1140
|
+
assert 'version = "0.3.0"' in lockfile.read_text()
|
|
1141
|
+
|
|
1142
|
+
def test_cargo_toml_branch_syncs_workspace_root_lockfile(self, tmp_path):
|
|
1143
|
+
"""Lockfile in workspace root, Cargo.toml in a member directory."""
|
|
1144
|
+
(tmp_path / ".git").mkdir()
|
|
1145
|
+
lockfile = tmp_path / "Cargo.lock"
|
|
1146
|
+
lockfile.write_text(
|
|
1147
|
+
"[[package]]\n"
|
|
1148
|
+
'name = "alpha"\n'
|
|
1149
|
+
'version = "0.2.0"\n',
|
|
1150
|
+
encoding="utf-8",
|
|
1151
|
+
)
|
|
1152
|
+
member_dir = tmp_path / "crates" / "alpha"
|
|
1153
|
+
member_dir.mkdir(parents=True)
|
|
1154
|
+
cargo_toml = member_dir / "Cargo.toml"
|
|
1155
|
+
cargo_toml.write_text(
|
|
1156
|
+
"[package]\n"
|
|
1157
|
+
'name = "alpha"\n'
|
|
1158
|
+
'version = "0.2.0"\n',
|
|
1159
|
+
encoding="utf-8",
|
|
1160
|
+
)
|
|
1161
|
+
update_version_in_file(0, "0.3.0", "Cargo.toml", cargo_toml, show_diff=False)
|
|
1162
|
+
assert 'version = "0.3.0"' in cargo_toml.read_text()
|
|
1163
|
+
assert 'version = "0.3.0"' in lockfile.read_text()
|
|
1164
|
+
|
|
1165
|
+
def test_cargo_toml_branch_no_lockfile_present(self, tmp_path):
|
|
1166
|
+
"""When there's no lockfile at all, the manifest update still proceeds."""
|
|
1167
|
+
(tmp_path / ".git").mkdir()
|
|
1168
|
+
cargo_toml = tmp_path / "Cargo.toml"
|
|
1169
|
+
cargo_toml.write_text(
|
|
1170
|
+
"[package]\n"
|
|
1171
|
+
'name = "alpha"\n'
|
|
1172
|
+
'version = "0.2.0"\n',
|
|
1173
|
+
encoding="utf-8",
|
|
1174
|
+
)
|
|
1175
|
+
# Should not raise.
|
|
1176
|
+
update_version_in_file(0, "0.3.0", "Cargo.toml", cargo_toml, show_diff=False)
|
|
1177
|
+
assert 'version = "0.3.0"' in cargo_toml.read_text()
|
|
1178
|
+
|
|
1179
|
+
|
|
944
1180
|
class TestParseGitignore:
|
|
945
1181
|
"""Test cases for _parse_gitignore function."""
|
|
946
1182
|
|
|
@@ -354,7 +354,7 @@ def get_version_from_chart_yaml(path: Path) -> Version | None:
|
|
|
354
354
|
def get_version_from_setup_py(path: Path) -> Version | None:
|
|
355
355
|
setup_py_path = path / "setup.py"
|
|
356
356
|
if setup_py_path.exists():
|
|
357
|
-
with setup_py_path.open(encoding="utf-8") as f:
|
|
357
|
+
with setup_py_path.open(encoding="utf-8", errors="replace") as f:
|
|
358
358
|
setup_data = f.read()
|
|
359
359
|
if res := re.search(r"version=['\"]([^'\"]+)['\"]", setup_data):
|
|
360
360
|
try:
|
|
@@ -413,7 +413,7 @@ def get_version_from_cargo_toml(directory_path: Path) -> Version | None:
|
|
|
413
413
|
def get_version_from_version_file(path: Path) -> Version | None:
|
|
414
414
|
version_path = path / "VERSION"
|
|
415
415
|
if version_path.exists():
|
|
416
|
-
with version_path.open(encoding="utf-8") as f:
|
|
416
|
+
with version_path.open(encoding="utf-8", errors="replace") as f:
|
|
417
417
|
version = f.read().strip()
|
|
418
418
|
try:
|
|
419
419
|
return Version.from_str(version)
|
|
@@ -425,7 +425,7 @@ def get_version_from_version_file(path: Path) -> Version | None:
|
|
|
425
425
|
def get_version_from_version_txt(path: Path) -> Version | None:
|
|
426
426
|
version_txt_path = path / "VERSION.txt"
|
|
427
427
|
if version_txt_path.exists():
|
|
428
|
-
with version_txt_path.open(encoding="utf-8") as f:
|
|
428
|
+
with version_txt_path.open(encoding="utf-8", errors="replace") as f:
|
|
429
429
|
version = f.read().strip()
|
|
430
430
|
try:
|
|
431
431
|
return Version.from_str(version)
|
|
@@ -437,7 +437,7 @@ def get_version_from_version_txt(path: Path) -> Version | None:
|
|
|
437
437
|
def get_version_from_build_gradle_kts(path: Path) -> Version | None:
|
|
438
438
|
build_gradle_kts_path = path / "build.gradle.kts"
|
|
439
439
|
if build_gradle_kts_path.exists():
|
|
440
|
-
with build_gradle_kts_path.open(encoding="utf-8") as f:
|
|
440
|
+
with build_gradle_kts_path.open(encoding="utf-8", errors="replace") as f:
|
|
441
441
|
content = f.read()
|
|
442
442
|
if res := re.search(r'version\s*=\s*"([^"]+)"', content):
|
|
443
443
|
try:
|
|
@@ -875,7 +875,18 @@ def update_version_in_file(verbose: int, next_version_str: str, file: str, file_
|
|
|
875
875
|
elif file == "setup.py":
|
|
876
876
|
update_file(str(file_path), r"version=['\"].*?['\"]", f"version='{next_version_str}'", verbose, show_diff=show_diff)
|
|
877
877
|
elif file == "Cargo.toml":
|
|
878
|
+
crate_name = _get_cargo_package_name(file_path)
|
|
878
879
|
update_cargo_toml_version(str(file_path), next_version_str, verbose, show_diff=show_diff)
|
|
880
|
+
# Cargo.lock pins every workspace member's version. Without
|
|
881
|
+
# syncing it here, `cargo publish` and `cargo build --locked`
|
|
882
|
+
# refuse to run against a manifest that disagrees with the
|
|
883
|
+
# lockfile — the most common reason a release tag gets bumped
|
|
884
|
+
# but the publish CI step fails on "working directory contains
|
|
885
|
+
# changes". Idempotent / no-op for pure non-Rust projects.
|
|
886
|
+
if crate_name:
|
|
887
|
+
lockfile = _find_cargo_lock_for(file_path.parent)
|
|
888
|
+
if lockfile is not None:
|
|
889
|
+
update_cargo_lock_version(crate_name, next_version_str, lockfile, verbose, show_diff=show_diff)
|
|
879
890
|
elif file in ("VERSION", "VERSION.txt"):
|
|
880
891
|
update_file(str(file_path), None, next_version_str, verbose, show_diff=show_diff)
|
|
881
892
|
elif file in ("__about__.py", "__init__.py"):
|
|
@@ -918,7 +929,7 @@ def update_file(filename: str, search_pattern: str | None, replace_text: str, ve
|
|
|
918
929
|
return
|
|
919
930
|
if verbose > 0:
|
|
920
931
|
console.print(f"Updating {file_path}")
|
|
921
|
-
with file_path.open(encoding="utf-8") as f:
|
|
932
|
+
with file_path.open(encoding="utf-8", errors="replace") as f:
|
|
922
933
|
content = f.read()
|
|
923
934
|
new_content = re.sub(search_pattern, replace_text, content) if search_pattern else replace_text
|
|
924
935
|
if show_diff:
|
|
@@ -935,7 +946,7 @@ def update_cargo_toml_version(filename: str, next_version_str: str, verbose: int
|
|
|
935
946
|
if verbose > 0:
|
|
936
947
|
console.print(f"Updating {file_path}")
|
|
937
948
|
|
|
938
|
-
with file_path.open(encoding="utf-8") as f:
|
|
949
|
+
with file_path.open(encoding="utf-8", errors="replace") as f:
|
|
939
950
|
content = f.read()
|
|
940
951
|
|
|
941
952
|
# Use regex to match version in [package] section only
|
|
@@ -958,6 +969,82 @@ def update_cargo_toml_version(filename: str, next_version_str: str, verbose: int
|
|
|
958
969
|
f.write(new_content)
|
|
959
970
|
|
|
960
971
|
|
|
972
|
+
def _get_cargo_package_name(cargo_toml_path: Path) -> str | None:
|
|
973
|
+
"""Read [package].name from a Cargo.toml. Returns None if missing/invalid.
|
|
974
|
+
|
|
975
|
+
Used to anchor lockfile rewrites — we need the crate's own name to
|
|
976
|
+
find its entry in Cargo.lock.
|
|
977
|
+
"""
|
|
978
|
+
if not cargo_toml_path.is_file():
|
|
979
|
+
return None
|
|
980
|
+
try:
|
|
981
|
+
with cargo_toml_path.open("rb") as f:
|
|
982
|
+
data = tomllib.load(f)
|
|
983
|
+
except (OSError, tomllib.TOMLDecodeError):
|
|
984
|
+
return None
|
|
985
|
+
package = data.get("package")
|
|
986
|
+
if not isinstance(package, dict):
|
|
987
|
+
return None
|
|
988
|
+
name = package.get("name")
|
|
989
|
+
return name if isinstance(name, str) and name else None
|
|
990
|
+
|
|
991
|
+
|
|
992
|
+
def _find_cargo_lock_for(cargo_toml_dir: Path) -> Path | None:
|
|
993
|
+
"""Walk up from a Cargo.toml's directory looking for a Cargo.lock.
|
|
994
|
+
|
|
995
|
+
Covers both layouts:
|
|
996
|
+
* single crate with its own Cargo.lock (lockfile in same dir)
|
|
997
|
+
* cargo workspace where members share a root-level Cargo.lock
|
|
998
|
+
|
|
999
|
+
Stops at the directory containing .git (we never escape the current
|
|
1000
|
+
repo) or at the filesystem root.
|
|
1001
|
+
"""
|
|
1002
|
+
current = cargo_toml_dir.resolve()
|
|
1003
|
+
while True:
|
|
1004
|
+
candidate = current / "Cargo.lock"
|
|
1005
|
+
if candidate.is_file():
|
|
1006
|
+
return candidate
|
|
1007
|
+
if (current / ".git").exists():
|
|
1008
|
+
return None
|
|
1009
|
+
if current.parent == current:
|
|
1010
|
+
return None
|
|
1011
|
+
current = current.parent
|
|
1012
|
+
|
|
1013
|
+
|
|
1014
|
+
def update_cargo_lock_version(
|
|
1015
|
+
crate_name: str,
|
|
1016
|
+
next_version_str: str,
|
|
1017
|
+
lockfile_path: Path,
|
|
1018
|
+
verbose: int,
|
|
1019
|
+
*,
|
|
1020
|
+
show_diff: bool = True,
|
|
1021
|
+
) -> None:
|
|
1022
|
+
"""Sync a local crate's version in Cargo.lock.
|
|
1023
|
+
|
|
1024
|
+
Cargo.lock records every dep, but registry deps carry a
|
|
1025
|
+
`source = "registry+..."` line between `name` and `version`.
|
|
1026
|
+
Workspace members / path deps do NOT — their `version` is on the
|
|
1027
|
+
line immediately after `name`. We anchor on that two-line shape so
|
|
1028
|
+
registry deps with the same name as a local crate can't be hit by
|
|
1029
|
+
mistake. No-op if the crate isn't in this lockfile or is already
|
|
1030
|
+
in sync.
|
|
1031
|
+
"""
|
|
1032
|
+
if not lockfile_path.is_file():
|
|
1033
|
+
return
|
|
1034
|
+
if verbose > 0:
|
|
1035
|
+
console.print(f"Updating {lockfile_path}")
|
|
1036
|
+
content = lockfile_path.read_text(encoding="utf-8")
|
|
1037
|
+
pattern = re.compile(
|
|
1038
|
+
rf'(\[\[package\]\]\nname = "{re.escape(crate_name)}"\nversion = ")[^"]+(")',
|
|
1039
|
+
)
|
|
1040
|
+
new_content, n = pattern.subn(rf"\g<1>{next_version_str}\g<2>", content, count=1)
|
|
1041
|
+
if n == 0 or new_content == content:
|
|
1042
|
+
return
|
|
1043
|
+
if show_diff:
|
|
1044
|
+
show_file_diff(content, new_content, str(lockfile_path))
|
|
1045
|
+
lockfile_path.write_text(new_content, encoding="utf-8")
|
|
1046
|
+
|
|
1047
|
+
|
|
961
1048
|
def show_file_diff(old_content: str, new_content: str, filename: str) -> None:
|
|
962
1049
|
old_lines = old_content.splitlines()
|
|
963
1050
|
new_lines = new_content.splitlines()
|
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"permissions": {
|
|
3
|
-
"allow": [
|
|
4
|
-
"Bash(ruff check:*)",
|
|
5
|
-
"Bash(mkdir:*)",
|
|
6
|
-
"Bash(rg:*)",
|
|
7
|
-
"Bash(uv sync:*)",
|
|
8
|
-
"Bash(python:*)",
|
|
9
|
-
"Bash(pytest:*)",
|
|
10
|
-
"Bash(uv run pytest:*)",
|
|
11
|
-
"Bash(uv run ruff check:*)",
|
|
12
|
-
"Bash(ls:*)",
|
|
13
|
-
"Bash(./scripts/test.sh:*)"
|
|
14
|
-
],
|
|
15
|
-
"deny": []
|
|
16
|
-
}
|
|
17
|
-
}
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|