tgit 0.34.1__tar.gz → 0.35.0__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.35.0}/CHANGELOG.md +24 -0
- {tgit-0.34.1 → tgit-0.35.0}/PKG-INFO +1 -1
- {tgit-0.34.1 → tgit-0.35.0}/pyproject.toml +1 -1
- {tgit-0.34.1 → tgit-0.35.0}/tests/integration/test_version_integration.py +104 -0
- {tgit-0.34.1 → tgit-0.35.0}/tests/unit/test_version.py +445 -0
- {tgit-0.34.1 → tgit-0.35.0}/tests/unit/test_version_coverage.py +2 -4
- {tgit-0.34.1 → tgit-0.35.0}/tgit/prompts/commit.txt +22 -6
- {tgit-0.34.1 → tgit-0.35.0}/tgit/version.py +311 -24
- {tgit-0.34.1 → tgit-0.35.0}/uv.lock +221 -244
- tgit-0.34.1/.claude/settings.local.json +0 -17
- {tgit-0.34.1 → tgit-0.35.0}/.github/workflows/build.yml +0 -0
- {tgit-0.34.1 → tgit-0.35.0}/.github/workflows/ci.yml +0 -0
- {tgit-0.34.1 → tgit-0.35.0}/.github/workflows/claude-code-review.yml +0 -0
- {tgit-0.34.1 → tgit-0.35.0}/.github/workflows/claude.yml +0 -0
- {tgit-0.34.1 → tgit-0.35.0}/.gitignore +0 -0
- {tgit-0.34.1 → tgit-0.35.0}/.python-version +0 -0
- {tgit-0.34.1 → tgit-0.35.0}/.tgit/settings.json +0 -0
- {tgit-0.34.1 → tgit-0.35.0}/.vscode/extensions.json +0 -0
- {tgit-0.34.1 → tgit-0.35.0}/.vscode/launch.json +0 -0
- {tgit-0.34.1 → tgit-0.35.0}/AGENTS.md +0 -0
- {tgit-0.34.1 → tgit-0.35.0}/CLAUDE.md +0 -0
- {tgit-0.34.1 → tgit-0.35.0}/LICENSE +0 -0
- {tgit-0.34.1 → tgit-0.35.0}/README.md +0 -0
- {tgit-0.34.1 → tgit-0.35.0}/pyrightconfig.json +0 -0
- {tgit-0.34.1 → tgit-0.35.0}/scripts/publish.sh +0 -0
- {tgit-0.34.1 → tgit-0.35.0}/scripts/test.sh +0 -0
- {tgit-0.34.1 → tgit-0.35.0}/tests/README.md +0 -0
- {tgit-0.34.1 → tgit-0.35.0}/tests/__init__.py +0 -0
- {tgit-0.34.1 → tgit-0.35.0}/tests/conftest.py +0 -0
- {tgit-0.34.1 → tgit-0.35.0}/tests/integration/__init__.py +0 -0
- {tgit-0.34.1 → tgit-0.35.0}/tests/unit/__init__.py +0 -0
- {tgit-0.34.1 → tgit-0.35.0}/tests/unit/test_add.py +0 -0
- {tgit-0.34.1 → tgit-0.35.0}/tests/unit/test_changelog.py +0 -0
- {tgit-0.34.1 → tgit-0.35.0}/tests/unit/test_changelog_coverage.py +0 -0
- {tgit-0.34.1 → tgit-0.35.0}/tests/unit/test_cli.py +0 -0
- {tgit-0.34.1 → tgit-0.35.0}/tests/unit/test_commit.py +0 -0
- {tgit-0.34.1 → tgit-0.35.0}/tests/unit/test_commit_coverage.py +0 -0
- {tgit-0.34.1 → tgit-0.35.0}/tests/unit/test_interactive_settings.py +0 -0
- {tgit-0.34.1 → tgit-0.35.0}/tests/unit/test_settings.py +0 -0
- {tgit-0.34.1 → tgit-0.35.0}/tests/unit/test_types.py +0 -0
- {tgit-0.34.1 → tgit-0.35.0}/tests/unit/test_utils.py +0 -0
- {tgit-0.34.1 → tgit-0.35.0}/tests/unit/test_utils_coverage.py +0 -0
- {tgit-0.34.1 → tgit-0.35.0}/tgit/__init__.py +0 -0
- {tgit-0.34.1 → tgit-0.35.0}/tgit/add.py +0 -0
- {tgit-0.34.1 → tgit-0.35.0}/tgit/changelog.py +0 -0
- {tgit-0.34.1 → tgit-0.35.0}/tgit/cli.py +0 -0
- {tgit-0.34.1 → tgit-0.35.0}/tgit/commit.py +0 -0
- {tgit-0.34.1 → tgit-0.35.0}/tgit/constants.py +0 -0
- {tgit-0.34.1 → tgit-0.35.0}/tgit/interactive_settings.py +0 -0
- {tgit-0.34.1 → tgit-0.35.0}/tgit/py.typed +0 -0
- {tgit-0.34.1 → tgit-0.35.0}/tgit/settings.py +0 -0
- {tgit-0.34.1 → tgit-0.35.0}/tgit/shared.py +0 -0
- {tgit-0.34.1 → tgit-0.35.0}/tgit/types.py +0 -0
- {tgit-0.34.1 → tgit-0.35.0}/tgit/utils/__init__.py +0 -0
|
@@ -1,3 +1,27 @@
|
|
|
1
|
+
## v0.35.0
|
|
2
|
+
|
|
3
|
+
[v0.34.2...v0.35.0](https://github.com/Jannchie/tgit/compare/v0.34.2...v0.35.0)
|
|
4
|
+
|
|
5
|
+
### :sparkles: Features
|
|
6
|
+
|
|
7
|
+
- **version**: add support for syncing Cargo.lock with Cargo.toml - By [Jannchie](mailto:jannchie@gmail.com) in [7ca29ae](https://github.com/Jannchie/tgit/commit/7ca29ae)
|
|
8
|
+
|
|
9
|
+
### :wrench: Chores
|
|
10
|
+
|
|
11
|
+
- update risk detection guidelines - By [Jannchie](mailto:jannchie@gmail.com) in [b5e0762](https://github.com/Jannchie/tgit/commit/b5e0762)
|
|
12
|
+
|
|
13
|
+
## v0.34.2
|
|
14
|
+
|
|
15
|
+
[v0.34.1...v0.34.2](https://github.com/Jannchie/tgit/compare/v0.34.1...v0.34.2)
|
|
16
|
+
|
|
17
|
+
### :adhesive_bandage: Fixes
|
|
18
|
+
|
|
19
|
+
- **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)
|
|
20
|
+
|
|
21
|
+
### :test_tube: Tests
|
|
22
|
+
|
|
23
|
+
- **cargo**: add unit tests for cargo version updates - By [Jianqi Pan](mailto:jannchie@gmail.com) in [bce5042](https://github.com/Jannchie/tgit/commit/bce5042)
|
|
24
|
+
|
|
1
25
|
## v0.34.1
|
|
2
26
|
|
|
3
27
|
[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.
|
|
3
|
+
Version: 0.35.0
|
|
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
|
|
@@ -305,6 +305,110 @@ description = "Test package"
|
|
|
305
305
|
node_modules_content = json.loads(node_modules_package_json.read_text())
|
|
306
306
|
assert node_modules_content["version"] == "1.0.0"
|
|
307
307
|
|
|
308
|
+
def _build_args(self, tmp_path, *, recursive: bool) -> VersionArgs:
|
|
309
|
+
return VersionArgs(
|
|
310
|
+
version="",
|
|
311
|
+
verbose=0,
|
|
312
|
+
no_commit=True,
|
|
313
|
+
no_tag=True,
|
|
314
|
+
no_push=True,
|
|
315
|
+
patch=False,
|
|
316
|
+
minor=False,
|
|
317
|
+
major=False,
|
|
318
|
+
prepatch="",
|
|
319
|
+
preminor="",
|
|
320
|
+
premajor="",
|
|
321
|
+
recursive=recursive,
|
|
322
|
+
custom="",
|
|
323
|
+
path=str(tmp_path),
|
|
324
|
+
)
|
|
325
|
+
|
|
326
|
+
def test_update_version_files_syncs_cargo_lock(self, tmp_path):
|
|
327
|
+
"""End-to-end: bumping a crate's Cargo.toml also rewrites its
|
|
328
|
+
Cargo.lock entry. The lockfile lives next to the manifest."""
|
|
329
|
+
(tmp_path / "Cargo.toml").write_text(
|
|
330
|
+
'[package]\nname = "demo"\nversion = "0.1.0"\n',
|
|
331
|
+
encoding="utf-8",
|
|
332
|
+
)
|
|
333
|
+
(tmp_path / "Cargo.lock").write_text(
|
|
334
|
+
'[[package]]\nname = "demo"\nversion = "0.1.0"\n',
|
|
335
|
+
encoding="utf-8",
|
|
336
|
+
)
|
|
337
|
+
update_version_files(
|
|
338
|
+
self._build_args(tmp_path, recursive=False),
|
|
339
|
+
Version(major=0, minor=2, patch=0),
|
|
340
|
+
verbose=0,
|
|
341
|
+
recursive=False,
|
|
342
|
+
)
|
|
343
|
+
assert 'version = "0.2.0"' in (tmp_path / "Cargo.toml").read_text()
|
|
344
|
+
assert 'version = "0.2.0"' in (tmp_path / "Cargo.lock").read_text()
|
|
345
|
+
|
|
346
|
+
def test_update_version_files_workspace_lockfile_dedups(self, tmp_path):
|
|
347
|
+
"""Cargo workspace with two members and one shared lockfile —
|
|
348
|
+
both [[package]] entries get rewritten, but the lockfile is
|
|
349
|
+
not duplicated."""
|
|
350
|
+
(tmp_path / ".git").mkdir()
|
|
351
|
+
(tmp_path / "Cargo.toml").write_text(
|
|
352
|
+
'[workspace]\nmembers = ["crates/alpha", "crates/beta"]\n',
|
|
353
|
+
encoding="utf-8",
|
|
354
|
+
)
|
|
355
|
+
(tmp_path / "Cargo.lock").write_text(
|
|
356
|
+
'[[package]]\nname = "alpha"\nversion = "0.1.0"\n\n'
|
|
357
|
+
'[[package]]\nname = "beta"\nversion = "0.1.0"\n',
|
|
358
|
+
encoding="utf-8",
|
|
359
|
+
)
|
|
360
|
+
alpha = tmp_path / "crates" / "alpha"
|
|
361
|
+
alpha.mkdir(parents=True)
|
|
362
|
+
(alpha / "Cargo.toml").write_text(
|
|
363
|
+
'[package]\nname = "alpha"\nversion = "0.1.0"\n',
|
|
364
|
+
encoding="utf-8",
|
|
365
|
+
)
|
|
366
|
+
beta = tmp_path / "crates" / "beta"
|
|
367
|
+
beta.mkdir(parents=True)
|
|
368
|
+
(beta / "Cargo.toml").write_text(
|
|
369
|
+
'[package]\nname = "beta"\nversion = "0.1.0"\n',
|
|
370
|
+
encoding="utf-8",
|
|
371
|
+
)
|
|
372
|
+
update_version_files(
|
|
373
|
+
self._build_args(tmp_path, recursive=True),
|
|
374
|
+
Version(major=0, minor=2, patch=0),
|
|
375
|
+
verbose=0,
|
|
376
|
+
recursive=True,
|
|
377
|
+
)
|
|
378
|
+
lockfile_content = (tmp_path / "Cargo.lock").read_text()
|
|
379
|
+
assert lockfile_content.count('version = "0.2.0"') == 2
|
|
380
|
+
assert 'version = "0.1.0"' not in lockfile_content
|
|
381
|
+
|
|
382
|
+
def test_update_version_files_pnpm_root_without_version(self, tmp_path):
|
|
383
|
+
"""pnpm workspace root without `version`: bump must insert
|
|
384
|
+
one in the root package.json and update children too."""
|
|
385
|
+
(tmp_path / "package.json").write_text(
|
|
386
|
+
'{\n "name": "monorepo",\n "private": true\n}\n',
|
|
387
|
+
encoding="utf-8",
|
|
388
|
+
)
|
|
389
|
+
(tmp_path / "pnpm-workspace.yaml").write_text(
|
|
390
|
+
'packages:\n - "packages/*"\n',
|
|
391
|
+
encoding="utf-8",
|
|
392
|
+
)
|
|
393
|
+
alpha = tmp_path / "packages" / "alpha"
|
|
394
|
+
alpha.mkdir(parents=True)
|
|
395
|
+
(alpha / "package.json").write_text(
|
|
396
|
+
'{"name": "@x/alpha", "version": "0.1.0"}\n',
|
|
397
|
+
encoding="utf-8",
|
|
398
|
+
)
|
|
399
|
+
|
|
400
|
+
update_version_files(
|
|
401
|
+
self._build_args(tmp_path, recursive=True),
|
|
402
|
+
Version(major=0, minor=2, patch=0),
|
|
403
|
+
verbose=0,
|
|
404
|
+
recursive=True,
|
|
405
|
+
)
|
|
406
|
+
root_data = json.loads((tmp_path / "package.json").read_text())
|
|
407
|
+
assert root_data["version"] == "0.2.0"
|
|
408
|
+
assert root_data["name"] == "monorepo"
|
|
409
|
+
child_data = json.loads((alpha / "package.json").read_text())
|
|
410
|
+
assert child_data["version"] == "0.2.0"
|
|
411
|
+
|
|
308
412
|
@patch("tgit.version.get_next_version")
|
|
309
413
|
@patch("tgit.version.get_current_version")
|
|
310
414
|
@patch("tgit.version.update_version_files")
|
|
@@ -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,
|
|
@@ -17,8 +19,11 @@ from tgit.version import (
|
|
|
17
19
|
_parse_gitignore,
|
|
18
20
|
_prompt_for_version_choice,
|
|
19
21
|
_should_ignore_path,
|
|
22
|
+
_version_from_workspace_children,
|
|
20
23
|
bump_version,
|
|
24
|
+
cargo_lockfiles_for_manifests,
|
|
21
25
|
execute_git_commands,
|
|
26
|
+
find_workspace_package_jsons,
|
|
22
27
|
format_diff_lines,
|
|
23
28
|
get_current_version,
|
|
24
29
|
get_custom_version,
|
|
@@ -37,8 +42,11 @@ from tgit.version import (
|
|
|
37
42
|
get_version_from_version_txt,
|
|
38
43
|
handle_version,
|
|
39
44
|
show_file_diff,
|
|
45
|
+
sync_cargo_lockfiles,
|
|
46
|
+
update_cargo_lock_version,
|
|
40
47
|
update_cargo_toml_version,
|
|
41
48
|
update_file,
|
|
49
|
+
update_package_json_version,
|
|
42
50
|
update_version_files,
|
|
43
51
|
update_version_in_file,
|
|
44
52
|
version,
|
|
@@ -941,6 +949,443 @@ members = ["other-crate"]
|
|
|
941
949
|
update_cargo_toml_version(non_existent_file, "1.0.0", 0, show_diff=False)
|
|
942
950
|
|
|
943
951
|
|
|
952
|
+
class TestGetCargoPackageName:
|
|
953
|
+
"""Test cases for _get_cargo_package_name."""
|
|
954
|
+
|
|
955
|
+
def test_basic(self, tmp_path):
|
|
956
|
+
cargo_toml = tmp_path / "Cargo.toml"
|
|
957
|
+
cargo_toml.write_text('[package]\nname = "arthash"\nversion = "0.2.0"\n')
|
|
958
|
+
assert _get_cargo_package_name(cargo_toml) == "arthash"
|
|
959
|
+
|
|
960
|
+
def test_missing_file(self, tmp_path):
|
|
961
|
+
assert _get_cargo_package_name(tmp_path / "Cargo.toml") is None
|
|
962
|
+
|
|
963
|
+
def test_missing_package_section(self, tmp_path):
|
|
964
|
+
cargo_toml = tmp_path / "Cargo.toml"
|
|
965
|
+
cargo_toml.write_text('[workspace]\nmembers = ["a"]\n')
|
|
966
|
+
assert _get_cargo_package_name(cargo_toml) is None
|
|
967
|
+
|
|
968
|
+
def test_missing_name_key(self, tmp_path):
|
|
969
|
+
cargo_toml = tmp_path / "Cargo.toml"
|
|
970
|
+
cargo_toml.write_text('[package]\nversion = "0.1.0"\n')
|
|
971
|
+
assert _get_cargo_package_name(cargo_toml) is None
|
|
972
|
+
|
|
973
|
+
def test_empty_name(self, tmp_path):
|
|
974
|
+
cargo_toml = tmp_path / "Cargo.toml"
|
|
975
|
+
cargo_toml.write_text('[package]\nname = ""\nversion = "0.1.0"\n')
|
|
976
|
+
assert _get_cargo_package_name(cargo_toml) is None
|
|
977
|
+
|
|
978
|
+
def test_invalid_toml(self, tmp_path):
|
|
979
|
+
cargo_toml = tmp_path / "Cargo.toml"
|
|
980
|
+
cargo_toml.write_text("this is not valid toml [[[")
|
|
981
|
+
assert _get_cargo_package_name(cargo_toml) is None
|
|
982
|
+
|
|
983
|
+
|
|
984
|
+
class TestFindCargoLockFor:
|
|
985
|
+
"""Test cases for _find_cargo_lock_for."""
|
|
986
|
+
|
|
987
|
+
def test_same_directory(self, tmp_path):
|
|
988
|
+
"""Single-crate layout: Cargo.toml and Cargo.lock side-by-side."""
|
|
989
|
+
(tmp_path / "Cargo.toml").write_text('[package]\nname = "x"\nversion = "0.1.0"\n')
|
|
990
|
+
lockfile = tmp_path / "Cargo.lock"
|
|
991
|
+
lockfile.write_text("# lock\n")
|
|
992
|
+
assert _find_cargo_lock_for(tmp_path) == lockfile
|
|
993
|
+
|
|
994
|
+
def test_parent_directory(self, tmp_path):
|
|
995
|
+
"""Workspace layout: lockfile lives at workspace root."""
|
|
996
|
+
(tmp_path / ".git").mkdir()
|
|
997
|
+
lockfile = tmp_path / "Cargo.lock"
|
|
998
|
+
lockfile.write_text("# lock\n")
|
|
999
|
+
member = tmp_path / "crates" / "alpha"
|
|
1000
|
+
member.mkdir(parents=True)
|
|
1001
|
+
(member / "Cargo.toml").write_text('[package]\nname = "alpha"\nversion = "0.1.0"\n')
|
|
1002
|
+
assert _find_cargo_lock_for(member) == lockfile
|
|
1003
|
+
|
|
1004
|
+
def test_no_lockfile_returns_none(self, tmp_path):
|
|
1005
|
+
(tmp_path / ".git").mkdir()
|
|
1006
|
+
crate = tmp_path / "crate"
|
|
1007
|
+
crate.mkdir()
|
|
1008
|
+
(crate / "Cargo.toml").write_text('[package]\nname = "x"\nversion = "0.1.0"\n')
|
|
1009
|
+
assert _find_cargo_lock_for(crate) is None
|
|
1010
|
+
|
|
1011
|
+
def test_stops_at_git_root(self, tmp_path):
|
|
1012
|
+
"""Must not escape the current repo, even if an outer Cargo.lock exists."""
|
|
1013
|
+
outer_lock = tmp_path / "Cargo.lock"
|
|
1014
|
+
outer_lock.write_text("# outer\n")
|
|
1015
|
+
repo = tmp_path / "inner_repo"
|
|
1016
|
+
repo.mkdir()
|
|
1017
|
+
(repo / ".git").mkdir()
|
|
1018
|
+
crate = repo / "crate"
|
|
1019
|
+
crate.mkdir()
|
|
1020
|
+
(crate / "Cargo.toml").write_text('[package]\nname = "x"\nversion = "0.1.0"\n')
|
|
1021
|
+
# Should NOT find the outer Cargo.lock — that's in a different repo.
|
|
1022
|
+
assert _find_cargo_lock_for(crate) is None
|
|
1023
|
+
|
|
1024
|
+
|
|
1025
|
+
class TestUpdateCargoLockVersion:
|
|
1026
|
+
"""Test cases for update_cargo_lock_version."""
|
|
1027
|
+
|
|
1028
|
+
@staticmethod
|
|
1029
|
+
def _make_lockfile(tmp_path: Path, content: str) -> Path:
|
|
1030
|
+
lockfile = tmp_path / "Cargo.lock"
|
|
1031
|
+
lockfile.write_text(content, encoding="utf-8")
|
|
1032
|
+
return lockfile
|
|
1033
|
+
|
|
1034
|
+
def test_local_crate_version_updated(self, tmp_path):
|
|
1035
|
+
"""Workspace member (no source field) gets its version rewritten."""
|
|
1036
|
+
content = (
|
|
1037
|
+
"# This file is automatically @generated by Cargo.\n"
|
|
1038
|
+
"version = 4\n\n"
|
|
1039
|
+
"[[package]]\n"
|
|
1040
|
+
'name = "arthash"\n'
|
|
1041
|
+
'version = "0.2.0"\n'
|
|
1042
|
+
"dependencies = [\n"
|
|
1043
|
+
' "matrixmultiply",\n'
|
|
1044
|
+
"]\n"
|
|
1045
|
+
)
|
|
1046
|
+
lockfile = self._make_lockfile(tmp_path, content)
|
|
1047
|
+
update_cargo_lock_version("arthash", "0.3.0", lockfile, 0, show_diff=False)
|
|
1048
|
+
new_content = lockfile.read_text()
|
|
1049
|
+
assert 'version = "0.3.0"' in new_content
|
|
1050
|
+
assert 'version = "0.2.0"' not in new_content
|
|
1051
|
+
|
|
1052
|
+
def test_registry_dep_with_same_name_not_touched(self, tmp_path):
|
|
1053
|
+
"""A registry dep named identically to the local crate must NOT be hit.
|
|
1054
|
+
|
|
1055
|
+
Registry entries have `source = "registry+..."` between `name` and
|
|
1056
|
+
`version`, so they fail the "name immediately followed by version"
|
|
1057
|
+
anchor. Local crates fit the anchor exactly.
|
|
1058
|
+
"""
|
|
1059
|
+
content = (
|
|
1060
|
+
"[[package]]\n"
|
|
1061
|
+
'name = "arthash"\n'
|
|
1062
|
+
'source = "registry+https://github.com/rust-lang/crates.io-index"\n'
|
|
1063
|
+
'version = "9.9.9"\n'
|
|
1064
|
+
'checksum = "deadbeef"\n\n'
|
|
1065
|
+
"[[package]]\n"
|
|
1066
|
+
'name = "arthash"\n'
|
|
1067
|
+
'version = "0.2.0"\n'
|
|
1068
|
+
"dependencies = [\n"
|
|
1069
|
+
' "matrixmultiply",\n'
|
|
1070
|
+
"]\n"
|
|
1071
|
+
)
|
|
1072
|
+
lockfile = self._make_lockfile(tmp_path, content)
|
|
1073
|
+
update_cargo_lock_version("arthash", "0.3.0", lockfile, 0, show_diff=False)
|
|
1074
|
+
new_content = lockfile.read_text()
|
|
1075
|
+
# registry entry untouched
|
|
1076
|
+
assert 'version = "9.9.9"' in new_content
|
|
1077
|
+
# local entry bumped
|
|
1078
|
+
assert 'version = "0.3.0"' in new_content
|
|
1079
|
+
# only one occurrence of the new version (didn't accidentally double-write)
|
|
1080
|
+
assert new_content.count('version = "0.3.0"') == 1
|
|
1081
|
+
|
|
1082
|
+
def test_crate_not_in_lockfile_is_noop(self, tmp_path):
|
|
1083
|
+
"""If the crate isn't recorded, nothing changes."""
|
|
1084
|
+
content = (
|
|
1085
|
+
"[[package]]\n"
|
|
1086
|
+
'name = "other"\n'
|
|
1087
|
+
'version = "1.0.0"\n'
|
|
1088
|
+
)
|
|
1089
|
+
lockfile = self._make_lockfile(tmp_path, content)
|
|
1090
|
+
update_cargo_lock_version("arthash", "0.3.0", lockfile, 0, show_diff=False)
|
|
1091
|
+
assert lockfile.read_text() == content
|
|
1092
|
+
|
|
1093
|
+
def test_missing_lockfile_is_noop(self, tmp_path):
|
|
1094
|
+
"""Pure non-Rust projects don't have a Cargo.lock — must not error."""
|
|
1095
|
+
lockfile = tmp_path / "Cargo.lock"
|
|
1096
|
+
# Should not raise.
|
|
1097
|
+
update_cargo_lock_version("arthash", "0.3.0", lockfile, 0, show_diff=False)
|
|
1098
|
+
assert not lockfile.exists()
|
|
1099
|
+
|
|
1100
|
+
def test_already_in_sync_is_noop(self, tmp_path):
|
|
1101
|
+
"""Idempotent: same version in -> file unchanged."""
|
|
1102
|
+
content = (
|
|
1103
|
+
"[[package]]\n"
|
|
1104
|
+
'name = "arthash"\n'
|
|
1105
|
+
'version = "0.3.0"\n'
|
|
1106
|
+
)
|
|
1107
|
+
lockfile = self._make_lockfile(tmp_path, content)
|
|
1108
|
+
before = lockfile.stat().st_mtime_ns
|
|
1109
|
+
update_cargo_lock_version("arthash", "0.3.0", lockfile, 0, show_diff=False)
|
|
1110
|
+
# Content identical; we explicitly skip the write in that case.
|
|
1111
|
+
assert lockfile.read_text() == content
|
|
1112
|
+
# And the file was not rewritten (mtime preserved). Some filesystems
|
|
1113
|
+
# have coarse mtime resolution, so this guard is a tightening only —
|
|
1114
|
+
# the content check above is the real assertion.
|
|
1115
|
+
assert lockfile.stat().st_mtime_ns == before
|
|
1116
|
+
|
|
1117
|
+
|
|
1118
|
+
class TestSyncCargoLockfiles:
|
|
1119
|
+
"""sync_cargo_lockfiles is the post-manifest step that rewrites
|
|
1120
|
+
every Cargo.lock entry matching a bumped manifest. Manifest update
|
|
1121
|
+
and lockfile sync are deliberately decoupled so workspace members
|
|
1122
|
+
sharing one lockfile don't trigger N rewrites of it."""
|
|
1123
|
+
|
|
1124
|
+
def test_syncs_sibling_lockfile(self, tmp_path):
|
|
1125
|
+
cargo_toml = tmp_path / "Cargo.toml"
|
|
1126
|
+
cargo_toml.write_text(
|
|
1127
|
+
"[package]\n"
|
|
1128
|
+
'name = "arthash"\n'
|
|
1129
|
+
'version = "0.3.0"\n',
|
|
1130
|
+
encoding="utf-8",
|
|
1131
|
+
)
|
|
1132
|
+
lockfile = tmp_path / "Cargo.lock"
|
|
1133
|
+
lockfile.write_text(
|
|
1134
|
+
"[[package]]\n"
|
|
1135
|
+
'name = "arthash"\n'
|
|
1136
|
+
'version = "0.2.0"\n',
|
|
1137
|
+
encoding="utf-8",
|
|
1138
|
+
)
|
|
1139
|
+
sync_cargo_lockfiles([cargo_toml], "0.3.0", 0, show_diff=False)
|
|
1140
|
+
assert 'version = "0.3.0"' in lockfile.read_text()
|
|
1141
|
+
|
|
1142
|
+
def test_syncs_workspace_root_lockfile(self, tmp_path):
|
|
1143
|
+
"""Workspace member's manifest, lockfile at the workspace root."""
|
|
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.3.0"\n',
|
|
1159
|
+
encoding="utf-8",
|
|
1160
|
+
)
|
|
1161
|
+
sync_cargo_lockfiles([cargo_toml], "0.3.0", 0, show_diff=False)
|
|
1162
|
+
assert 'version = "0.3.0"' in lockfile.read_text()
|
|
1163
|
+
|
|
1164
|
+
def test_no_lockfile_present_is_noop(self, tmp_path):
|
|
1165
|
+
"""Pure non-Rust projects or fresh crates may lack a lockfile."""
|
|
1166
|
+
(tmp_path / ".git").mkdir()
|
|
1167
|
+
cargo_toml = tmp_path / "Cargo.toml"
|
|
1168
|
+
cargo_toml.write_text(
|
|
1169
|
+
"[package]\n"
|
|
1170
|
+
'name = "alpha"\n'
|
|
1171
|
+
'version = "0.3.0"\n',
|
|
1172
|
+
encoding="utf-8",
|
|
1173
|
+
)
|
|
1174
|
+
# Should not raise.
|
|
1175
|
+
sync_cargo_lockfiles([cargo_toml], "0.3.0", 0, show_diff=False)
|
|
1176
|
+
|
|
1177
|
+
def test_workspace_members_dedup_to_one_lockfile_rewrite(self, tmp_path):
|
|
1178
|
+
"""Two members share one lockfile; both their entries must be
|
|
1179
|
+
rewritten, and the lockfile resolution must not duplicate."""
|
|
1180
|
+
(tmp_path / ".git").mkdir()
|
|
1181
|
+
lockfile = tmp_path / "Cargo.lock"
|
|
1182
|
+
lockfile.write_text(
|
|
1183
|
+
"[[package]]\n"
|
|
1184
|
+
'name = "alpha"\n'
|
|
1185
|
+
'version = "0.2.0"\n'
|
|
1186
|
+
"\n"
|
|
1187
|
+
"[[package]]\n"
|
|
1188
|
+
'name = "beta"\n'
|
|
1189
|
+
'version = "0.2.0"\n',
|
|
1190
|
+
encoding="utf-8",
|
|
1191
|
+
)
|
|
1192
|
+
alpha_toml = tmp_path / "crates" / "alpha" / "Cargo.toml"
|
|
1193
|
+
alpha_toml.parent.mkdir(parents=True)
|
|
1194
|
+
alpha_toml.write_text(
|
|
1195
|
+
"[package]\n"
|
|
1196
|
+
'name = "alpha"\n'
|
|
1197
|
+
'version = "0.3.0"\n',
|
|
1198
|
+
encoding="utf-8",
|
|
1199
|
+
)
|
|
1200
|
+
beta_toml = tmp_path / "crates" / "beta" / "Cargo.toml"
|
|
1201
|
+
beta_toml.parent.mkdir(parents=True)
|
|
1202
|
+
beta_toml.write_text(
|
|
1203
|
+
"[package]\n"
|
|
1204
|
+
'name = "beta"\n'
|
|
1205
|
+
'version = "0.3.0"\n',
|
|
1206
|
+
encoding="utf-8",
|
|
1207
|
+
)
|
|
1208
|
+
sync_cargo_lockfiles([alpha_toml, beta_toml], "0.3.0", 0, show_diff=False)
|
|
1209
|
+
new_content = lockfile.read_text()
|
|
1210
|
+
assert new_content.count('version = "0.3.0"') == 2
|
|
1211
|
+
assert 'version = "0.2.0"' not in new_content
|
|
1212
|
+
|
|
1213
|
+
def test_manifests_without_package_name_skipped(self, tmp_path):
|
|
1214
|
+
"""A pure [workspace] manifest has no `name`; nothing to do."""
|
|
1215
|
+
(tmp_path / ".git").mkdir()
|
|
1216
|
+
ws_toml = tmp_path / "Cargo.toml"
|
|
1217
|
+
ws_toml.write_text('[workspace]\nmembers = ["crates/alpha"]\n', encoding="utf-8")
|
|
1218
|
+
lockfile = tmp_path / "Cargo.lock"
|
|
1219
|
+
original = '[[package]]\nname = "alpha"\nversion = "0.2.0"\n'
|
|
1220
|
+
lockfile.write_text(original, encoding="utf-8")
|
|
1221
|
+
sync_cargo_lockfiles([ws_toml], "0.3.0", 0, show_diff=False)
|
|
1222
|
+
assert lockfile.read_text() == original
|
|
1223
|
+
|
|
1224
|
+
|
|
1225
|
+
class TestCargoLockfilesForManifests:
|
|
1226
|
+
"""Resolution of unique Cargo.lock files for a manifest set."""
|
|
1227
|
+
|
|
1228
|
+
def test_dedups_shared_workspace_lockfile(self, tmp_path):
|
|
1229
|
+
(tmp_path / ".git").mkdir()
|
|
1230
|
+
lockfile = tmp_path / "Cargo.lock"
|
|
1231
|
+
lockfile.write_text("# lock\n")
|
|
1232
|
+
alpha = tmp_path / "crates" / "alpha" / "Cargo.toml"
|
|
1233
|
+
alpha.parent.mkdir(parents=True)
|
|
1234
|
+
alpha.write_text('[package]\nname = "alpha"\nversion = "0.1.0"\n')
|
|
1235
|
+
beta = tmp_path / "crates" / "beta" / "Cargo.toml"
|
|
1236
|
+
beta.parent.mkdir(parents=True)
|
|
1237
|
+
beta.write_text('[package]\nname = "beta"\nversion = "0.1.0"\n')
|
|
1238
|
+
result = cargo_lockfiles_for_manifests([alpha, beta])
|
|
1239
|
+
assert result == [lockfile]
|
|
1240
|
+
|
|
1241
|
+
def test_ignores_non_cargo_paths(self, tmp_path):
|
|
1242
|
+
assert cargo_lockfiles_for_manifests([tmp_path / "package.json"]) == []
|
|
1243
|
+
|
|
1244
|
+
def test_missing_lockfile_returns_empty(self, tmp_path):
|
|
1245
|
+
(tmp_path / ".git").mkdir()
|
|
1246
|
+
cargo_toml = tmp_path / "Cargo.toml"
|
|
1247
|
+
cargo_toml.write_text('[package]\nname = "x"\nversion = "0.1.0"\n')
|
|
1248
|
+
assert cargo_lockfiles_for_manifests([cargo_toml]) == []
|
|
1249
|
+
|
|
1250
|
+
|
|
1251
|
+
class TestUpdatePackageJsonVersion:
|
|
1252
|
+
"""update_package_json_version handles both replace (most pkgs) and
|
|
1253
|
+
insert (pnpm/npm workspace roots that omit `version`)."""
|
|
1254
|
+
|
|
1255
|
+
def test_replaces_existing_version(self, tmp_path):
|
|
1256
|
+
pkg = tmp_path / "package.json"
|
|
1257
|
+
pkg.write_text('{\n "name": "x",\n "version": "0.1.0"\n}\n', encoding="utf-8")
|
|
1258
|
+
update_package_json_version(str(pkg), "0.2.0", 0, show_diff=False)
|
|
1259
|
+
assert '"version": "0.2.0"' in pkg.read_text()
|
|
1260
|
+
assert '"version": "0.1.0"' not in pkg.read_text()
|
|
1261
|
+
|
|
1262
|
+
def test_inserts_when_missing(self, tmp_path):
|
|
1263
|
+
"""Workspace root without `version` — must end up with one
|
|
1264
|
+
and remain valid JSON."""
|
|
1265
|
+
pkg = tmp_path / "package.json"
|
|
1266
|
+
pkg.write_text(
|
|
1267
|
+
'{\n "name": "monorepo",\n "private": true,\n "workspaces": ["packages/*"]\n}\n',
|
|
1268
|
+
encoding="utf-8",
|
|
1269
|
+
)
|
|
1270
|
+
update_package_json_version(str(pkg), "1.0.0", 0, show_diff=False)
|
|
1271
|
+
new_content = pkg.read_text()
|
|
1272
|
+
assert '"version": "1.0.0"' in new_content
|
|
1273
|
+
# Still valid JSON
|
|
1274
|
+
import json as _json
|
|
1275
|
+
data = _json.loads(new_content)
|
|
1276
|
+
assert data["version"] == "1.0.0"
|
|
1277
|
+
assert data["name"] == "monorepo"
|
|
1278
|
+
|
|
1279
|
+
def test_only_first_version_match_replaced(self, tmp_path):
|
|
1280
|
+
"""Avoid touching a nested `"version"` inside dependencies."""
|
|
1281
|
+
pkg = tmp_path / "package.json"
|
|
1282
|
+
pkg.write_text(
|
|
1283
|
+
'{\n'
|
|
1284
|
+
' "name": "x",\n'
|
|
1285
|
+
' "version": "0.1.0",\n'
|
|
1286
|
+
' "dependencies": {\n'
|
|
1287
|
+
' "left-pad": "1.3.0"\n'
|
|
1288
|
+
' }\n'
|
|
1289
|
+
'}\n',
|
|
1290
|
+
encoding="utf-8",
|
|
1291
|
+
)
|
|
1292
|
+
update_package_json_version(str(pkg), "0.2.0", 0, show_diff=False)
|
|
1293
|
+
new_content = pkg.read_text()
|
|
1294
|
+
assert '"version": "0.2.0"' in new_content
|
|
1295
|
+
# left-pad pin must remain unchanged
|
|
1296
|
+
assert '"left-pad": "1.3.0"' in new_content
|
|
1297
|
+
|
|
1298
|
+
def test_file_not_exists_is_noop(self, tmp_path):
|
|
1299
|
+
# Should not raise.
|
|
1300
|
+
update_package_json_version(str(tmp_path / "missing.json"), "1.0.0", 0, show_diff=False)
|
|
1301
|
+
|
|
1302
|
+
|
|
1303
|
+
class TestWorkspaceVersionDiscovery:
|
|
1304
|
+
"""When the root package.json omits `version` (typical pnpm/yarn
|
|
1305
|
+
monorepo root), version detection falls back to the highest
|
|
1306
|
+
version among workspace children."""
|
|
1307
|
+
|
|
1308
|
+
def test_pnpm_workspace_yaml_resolves_child_versions(self, tmp_path):
|
|
1309
|
+
(tmp_path / "package.json").write_text('{"name": "root", "private": true}\n', encoding="utf-8")
|
|
1310
|
+
(tmp_path / "pnpm-workspace.yaml").write_text('packages:\n - "packages/*"\n', encoding="utf-8")
|
|
1311
|
+
alpha = tmp_path / "packages" / "alpha"
|
|
1312
|
+
alpha.mkdir(parents=True)
|
|
1313
|
+
(alpha / "package.json").write_text('{"name": "@x/alpha", "version": "1.2.3"}\n', encoding="utf-8")
|
|
1314
|
+
beta = tmp_path / "packages" / "beta"
|
|
1315
|
+
beta.mkdir(parents=True)
|
|
1316
|
+
(beta / "package.json").write_text('{"name": "@x/beta", "version": "1.5.0"}\n', encoding="utf-8")
|
|
1317
|
+
|
|
1318
|
+
version = get_version_from_package_json(tmp_path)
|
|
1319
|
+
assert version is not None
|
|
1320
|
+
# Max of child versions
|
|
1321
|
+
assert (version.major, version.minor, version.patch) == (1, 5, 0)
|
|
1322
|
+
|
|
1323
|
+
def test_npm_workspaces_field_list(self, tmp_path):
|
|
1324
|
+
(tmp_path / "package.json").write_text(
|
|
1325
|
+
'{"name": "root", "private": true, "workspaces": ["packages/*"]}\n',
|
|
1326
|
+
encoding="utf-8",
|
|
1327
|
+
)
|
|
1328
|
+
alpha = tmp_path / "packages" / "alpha"
|
|
1329
|
+
alpha.mkdir(parents=True)
|
|
1330
|
+
(alpha / "package.json").write_text('{"name": "@x/alpha", "version": "0.4.0"}\n', encoding="utf-8")
|
|
1331
|
+
|
|
1332
|
+
version = get_version_from_package_json(tmp_path)
|
|
1333
|
+
assert version is not None
|
|
1334
|
+
assert (version.major, version.minor, version.patch) == (0, 4, 0)
|
|
1335
|
+
|
|
1336
|
+
def test_yarn_workspaces_object_form(self, tmp_path):
|
|
1337
|
+
(tmp_path / "package.json").write_text(
|
|
1338
|
+
'{"name": "root", "workspaces": {"packages": ["modules/*"]}}\n',
|
|
1339
|
+
encoding="utf-8",
|
|
1340
|
+
)
|
|
1341
|
+
alpha = tmp_path / "modules" / "alpha"
|
|
1342
|
+
alpha.mkdir(parents=True)
|
|
1343
|
+
(alpha / "package.json").write_text('{"name": "@x/alpha", "version": "2.0.1"}\n', encoding="utf-8")
|
|
1344
|
+
|
|
1345
|
+
version = get_version_from_package_json(tmp_path)
|
|
1346
|
+
assert version is not None
|
|
1347
|
+
assert (version.major, version.minor, version.patch) == (2, 0, 1)
|
|
1348
|
+
|
|
1349
|
+
def test_root_version_takes_precedence(self, tmp_path):
|
|
1350
|
+
"""If root has its own version, children are ignored."""
|
|
1351
|
+
(tmp_path / "package.json").write_text(
|
|
1352
|
+
'{"name": "root", "version": "5.0.0", "workspaces": ["packages/*"]}\n',
|
|
1353
|
+
encoding="utf-8",
|
|
1354
|
+
)
|
|
1355
|
+
alpha = tmp_path / "packages" / "alpha"
|
|
1356
|
+
alpha.mkdir(parents=True)
|
|
1357
|
+
(alpha / "package.json").write_text('{"version": "9.9.9"}\n', encoding="utf-8")
|
|
1358
|
+
version = get_version_from_package_json(tmp_path)
|
|
1359
|
+
assert version is not None
|
|
1360
|
+
assert (version.major, version.minor, version.patch) == (5, 0, 0)
|
|
1361
|
+
|
|
1362
|
+
def test_no_workspace_no_version_returns_none(self, tmp_path):
|
|
1363
|
+
(tmp_path / "package.json").write_text('{"name": "x"}\n', encoding="utf-8")
|
|
1364
|
+
assert get_version_from_package_json(tmp_path) is None
|
|
1365
|
+
|
|
1366
|
+
def test_child_without_version_skipped(self, tmp_path):
|
|
1367
|
+
(tmp_path / "package.json").write_text('{"workspaces": ["packages/*"]}\n', encoding="utf-8")
|
|
1368
|
+
alpha = tmp_path / "packages" / "alpha"
|
|
1369
|
+
alpha.mkdir(parents=True)
|
|
1370
|
+
(alpha / "package.json").write_text('{"name": "@x/alpha"}\n', encoding="utf-8")
|
|
1371
|
+
beta = tmp_path / "packages" / "beta"
|
|
1372
|
+
beta.mkdir(parents=True)
|
|
1373
|
+
(beta / "package.json").write_text('{"name": "@x/beta", "version": "0.1.0"}\n', encoding="utf-8")
|
|
1374
|
+
|
|
1375
|
+
version = get_version_from_package_json(tmp_path)
|
|
1376
|
+
assert version is not None
|
|
1377
|
+
assert (version.major, version.minor, version.patch) == (0, 1, 0)
|
|
1378
|
+
|
|
1379
|
+
def test_find_workspace_package_jsons_excludes_root(self, tmp_path):
|
|
1380
|
+
(tmp_path / "package.json").write_text('{"workspaces": ["."]}\n', encoding="utf-8")
|
|
1381
|
+
result = find_workspace_package_jsons(tmp_path)
|
|
1382
|
+
assert result == []
|
|
1383
|
+
|
|
1384
|
+
def test_version_from_workspace_children_no_workspace(self, tmp_path):
|
|
1385
|
+
"""No workspaces config at all → no children, returns None."""
|
|
1386
|
+
assert _version_from_workspace_children(tmp_path) is None
|
|
1387
|
+
|
|
1388
|
+
|
|
944
1389
|
class TestParseGitignore:
|
|
945
1390
|
"""Test cases for _parse_gitignore function."""
|
|
946
1391
|
|
|
@@ -311,14 +311,12 @@ version = "invalid"
|
|
|
311
311
|
str(file_path), r'version\s*=\s*".*?"', 'version = "1.2.3"', 0, show_diff=False
|
|
312
312
|
)
|
|
313
313
|
|
|
314
|
-
@patch("tgit.version.
|
|
314
|
+
@patch("tgit.version.update_package_json_version")
|
|
315
315
|
def test_update_version_in_file_package_json(self, mock_update, tmp_path):
|
|
316
316
|
"""Test update_version_in_file for package.json."""
|
|
317
317
|
file_path = tmp_path / "package.json"
|
|
318
318
|
update_version_in_file(0, "1.2.3", "package.json", file_path)
|
|
319
|
-
mock_update.assert_called_once_with(
|
|
320
|
-
str(file_path), r'"version":\s*".*?"', '"version": "1.2.3"', 0, show_diff=False
|
|
321
|
-
)
|
|
319
|
+
mock_update.assert_called_once_with(str(file_path), "1.2.3", 0, show_diff=False)
|
|
322
320
|
|
|
323
321
|
@patch("tgit.version.update_file")
|
|
324
322
|
def test_update_version_in_file_version_txt(self, mock_update, tmp_path):
|