tgit 0.34.2__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.
Files changed (53) hide show
  1. {tgit-0.34.2 → tgit-0.35.0}/CHANGELOG.md +12 -0
  2. {tgit-0.34.2 → tgit-0.35.0}/PKG-INFO +1 -1
  3. {tgit-0.34.2 → tgit-0.35.0}/pyproject.toml +1 -1
  4. {tgit-0.34.2 → tgit-0.35.0}/tests/integration/test_version_integration.py +104 -0
  5. {tgit-0.34.2 → tgit-0.35.0}/tests/unit/test_version.py +232 -23
  6. {tgit-0.34.2 → tgit-0.35.0}/tests/unit/test_version_coverage.py +2 -4
  7. {tgit-0.34.2 → tgit-0.35.0}/tgit/prompts/commit.txt +22 -6
  8. {tgit-0.34.2 → tgit-0.35.0}/tgit/version.py +229 -29
  9. {tgit-0.34.2 → tgit-0.35.0}/uv.lock +221 -244
  10. {tgit-0.34.2 → tgit-0.35.0}/.github/workflows/build.yml +0 -0
  11. {tgit-0.34.2 → tgit-0.35.0}/.github/workflows/ci.yml +0 -0
  12. {tgit-0.34.2 → tgit-0.35.0}/.github/workflows/claude-code-review.yml +0 -0
  13. {tgit-0.34.2 → tgit-0.35.0}/.github/workflows/claude.yml +0 -0
  14. {tgit-0.34.2 → tgit-0.35.0}/.gitignore +0 -0
  15. {tgit-0.34.2 → tgit-0.35.0}/.python-version +0 -0
  16. {tgit-0.34.2 → tgit-0.35.0}/.tgit/settings.json +0 -0
  17. {tgit-0.34.2 → tgit-0.35.0}/.vscode/extensions.json +0 -0
  18. {tgit-0.34.2 → tgit-0.35.0}/.vscode/launch.json +0 -0
  19. {tgit-0.34.2 → tgit-0.35.0}/AGENTS.md +0 -0
  20. {tgit-0.34.2 → tgit-0.35.0}/CLAUDE.md +0 -0
  21. {tgit-0.34.2 → tgit-0.35.0}/LICENSE +0 -0
  22. {tgit-0.34.2 → tgit-0.35.0}/README.md +0 -0
  23. {tgit-0.34.2 → tgit-0.35.0}/pyrightconfig.json +0 -0
  24. {tgit-0.34.2 → tgit-0.35.0}/scripts/publish.sh +0 -0
  25. {tgit-0.34.2 → tgit-0.35.0}/scripts/test.sh +0 -0
  26. {tgit-0.34.2 → tgit-0.35.0}/tests/README.md +0 -0
  27. {tgit-0.34.2 → tgit-0.35.0}/tests/__init__.py +0 -0
  28. {tgit-0.34.2 → tgit-0.35.0}/tests/conftest.py +0 -0
  29. {tgit-0.34.2 → tgit-0.35.0}/tests/integration/__init__.py +0 -0
  30. {tgit-0.34.2 → tgit-0.35.0}/tests/unit/__init__.py +0 -0
  31. {tgit-0.34.2 → tgit-0.35.0}/tests/unit/test_add.py +0 -0
  32. {tgit-0.34.2 → tgit-0.35.0}/tests/unit/test_changelog.py +0 -0
  33. {tgit-0.34.2 → tgit-0.35.0}/tests/unit/test_changelog_coverage.py +0 -0
  34. {tgit-0.34.2 → tgit-0.35.0}/tests/unit/test_cli.py +0 -0
  35. {tgit-0.34.2 → tgit-0.35.0}/tests/unit/test_commit.py +0 -0
  36. {tgit-0.34.2 → tgit-0.35.0}/tests/unit/test_commit_coverage.py +0 -0
  37. {tgit-0.34.2 → tgit-0.35.0}/tests/unit/test_interactive_settings.py +0 -0
  38. {tgit-0.34.2 → tgit-0.35.0}/tests/unit/test_settings.py +0 -0
  39. {tgit-0.34.2 → tgit-0.35.0}/tests/unit/test_types.py +0 -0
  40. {tgit-0.34.2 → tgit-0.35.0}/tests/unit/test_utils.py +0 -0
  41. {tgit-0.34.2 → tgit-0.35.0}/tests/unit/test_utils_coverage.py +0 -0
  42. {tgit-0.34.2 → tgit-0.35.0}/tgit/__init__.py +0 -0
  43. {tgit-0.34.2 → tgit-0.35.0}/tgit/add.py +0 -0
  44. {tgit-0.34.2 → tgit-0.35.0}/tgit/changelog.py +0 -0
  45. {tgit-0.34.2 → tgit-0.35.0}/tgit/cli.py +0 -0
  46. {tgit-0.34.2 → tgit-0.35.0}/tgit/commit.py +0 -0
  47. {tgit-0.34.2 → tgit-0.35.0}/tgit/constants.py +0 -0
  48. {tgit-0.34.2 → tgit-0.35.0}/tgit/interactive_settings.py +0 -0
  49. {tgit-0.34.2 → tgit-0.35.0}/tgit/py.typed +0 -0
  50. {tgit-0.34.2 → tgit-0.35.0}/tgit/settings.py +0 -0
  51. {tgit-0.34.2 → tgit-0.35.0}/tgit/shared.py +0 -0
  52. {tgit-0.34.2 → tgit-0.35.0}/tgit/types.py +0 -0
  53. {tgit-0.34.2 → tgit-0.35.0}/tgit/utils/__init__.py +0 -0
@@ -1,3 +1,15 @@
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
+
1
13
  ## v0.34.2
2
14
 
3
15
  [v0.34.1...v0.34.2](https://github.com/Jannchie/tgit/compare/v0.34.1...v0.34.2)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: tgit
3
- Version: 0.34.2
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
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "tgit"
3
- version = "0.34.2"
3
+ version = "0.35.0"
4
4
  description = "AI-powered Git CLI for commits, changelogs, and semantic versioning."
5
5
  authors = [{ name = "Jannchie", email = "jannchie@gmail.com" }]
6
6
  dependencies = [
@@ -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")
@@ -19,8 +19,11 @@ from tgit.version import (
19
19
  _parse_gitignore,
20
20
  _prompt_for_version_choice,
21
21
  _should_ignore_path,
22
+ _version_from_workspace_children,
22
23
  bump_version,
24
+ cargo_lockfiles_for_manifests,
23
25
  execute_git_commands,
26
+ find_workspace_package_jsons,
24
27
  format_diff_lines,
25
28
  get_current_version,
26
29
  get_custom_version,
@@ -39,9 +42,11 @@ from tgit.version import (
39
42
  get_version_from_version_txt,
40
43
  handle_version,
41
44
  show_file_diff,
45
+ sync_cargo_lockfiles,
42
46
  update_cargo_lock_version,
43
47
  update_cargo_toml_version,
44
48
  update_file,
49
+ update_package_json_version,
45
50
  update_version_files,
46
51
  update_version_in_file,
47
52
  version,
@@ -1110,37 +1115,32 @@ class TestUpdateCargoLockVersion:
1110
1115
  assert lockfile.stat().st_mtime_ns == before
1111
1116
 
1112
1117
 
1113
- class TestUpdateVersionInFileCargoIntegration:
1114
- """update_version_in_file should sync Cargo.lock when it sees Cargo.toml."""
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."""
1115
1123
 
1116
- def test_cargo_toml_branch_syncs_sibling_lockfile(self, tmp_path):
1124
+ def test_syncs_sibling_lockfile(self, tmp_path):
1117
1125
  cargo_toml = tmp_path / "Cargo.toml"
1118
1126
  cargo_toml.write_text(
1119
1127
  "[package]\n"
1120
1128
  'name = "arthash"\n'
1121
- 'version = "0.2.0"\n'
1122
- "\n"
1123
- "[dependencies]\n"
1124
- 'matrixmultiply = "0.3"\n',
1129
+ 'version = "0.3.0"\n',
1125
1130
  encoding="utf-8",
1126
1131
  )
1127
1132
  lockfile = tmp_path / "Cargo.lock"
1128
1133
  lockfile.write_text(
1129
1134
  "[[package]]\n"
1130
1135
  'name = "arthash"\n'
1131
- 'version = "0.2.0"\n'
1132
- "dependencies = [\n"
1133
- ' "matrixmultiply",\n'
1134
- "]\n",
1136
+ 'version = "0.2.0"\n',
1135
1137
  encoding="utf-8",
1136
1138
  )
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()
1139
+ sync_cargo_lockfiles([cargo_toml], "0.3.0", 0, show_diff=False)
1140
1140
  assert 'version = "0.3.0"' in lockfile.read_text()
1141
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."""
1142
+ def test_syncs_workspace_root_lockfile(self, tmp_path):
1143
+ """Workspace member's manifest, lockfile at the workspace root."""
1144
1144
  (tmp_path / ".git").mkdir()
1145
1145
  lockfile = tmp_path / "Cargo.lock"
1146
1146
  lockfile.write_text(
@@ -1155,26 +1155,235 @@ class TestUpdateVersionInFileCargoIntegration:
1155
1155
  cargo_toml.write_text(
1156
1156
  "[package]\n"
1157
1157
  'name = "alpha"\n'
1158
- 'version = "0.2.0"\n',
1158
+ 'version = "0.3.0"\n',
1159
1159
  encoding="utf-8",
1160
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()
1161
+ sync_cargo_lockfiles([cargo_toml], "0.3.0", 0, show_diff=False)
1163
1162
  assert 'version = "0.3.0"' in lockfile.read_text()
1164
1163
 
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."""
1164
+ def test_no_lockfile_present_is_noop(self, tmp_path):
1165
+ """Pure non-Rust projects or fresh crates may lack a lockfile."""
1167
1166
  (tmp_path / ".git").mkdir()
1168
1167
  cargo_toml = tmp_path / "Cargo.toml"
1169
1168
  cargo_toml.write_text(
1170
1169
  "[package]\n"
1171
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'
1172
1189
  'version = "0.2.0"\n',
1173
1190
  encoding="utf-8",
1174
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):
1175
1299
  # 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()
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
1178
1387
 
1179
1388
 
1180
1389
  class TestParseGitignore:
@@ -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.update_file")
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):
@@ -65,12 +65,27 @@ Select the most appropriate type based on the semantic versioning rules above.
65
65
  - Cover the primary change(s) in the diff
66
66
  - If multiple distinct changes, separate with " && " (e.g., "update api && fix validation")
67
67
 
68
- ### Secret Detection
69
- - Review the diff for potential secrets (API keys, tokens, passwords, private keys, credentials, etc.).
70
- - For every suspected secret, add an entry to the `secrets` array with the file path, a brief description, and a `level`.
71
- - Use `level: "error"` when an actual secret value appears (tokens, passwords, private keys, credentials, connection strings, or high-entropy values).
72
- - Use `level: "warning"` when only key names or variable names appear without values (e.g., `API_KEY`, `SECRET_KEY`, `PASSWORD`).
73
- - If no secrets are found, return an empty array.
68
+ ### Risky Content & Files Detection
69
+ Review the diff and the list of changed files for anything that should not normally be committed. For every finding, append an entry to the `secrets` array with the file path, a brief description, and a `level` (`warning` or `error`). Return an empty array if nothing is found.
70
+
71
+ Flag two broad categories:
72
+
73
+ **1. Secrets / credentials embedded in code or config**
74
+ - `level: "error"` when an actual secret value appears: tokens, passwords, private keys, credentials, connection strings, OAuth client secrets, or high-entropy strings that look like generated keys.
75
+ - `level: "warning"` when only key names or variable names appear without values (e.g., `API_KEY=`, `SECRET_KEY=`, `PASSWORD=`).
76
+
77
+ **2. Files that should not be tracked in version control**
78
+ Flag these as `level: "error"` (description should explain what kind of file it is and why it shouldn't be committed). Use judgment based on filename, file path, and content:
79
+ - Log files: `*.log`, `npm-debug.log*`, `yarn-error.log*`, `pnpm-debug.log*`, anything under a `logs/` directory.
80
+ - Local/runtime environment files: `.env`, `.env.local`, `.env.*.local`, `.env.development.local`, `.env.production.local`. (Example/template env files like `.env.example`, `.env.sample`, `.env.template` are fine — do NOT flag them.)
81
+ - Private key / credential files: `*.pem`, `*.key`, `*.p12`, `*.pfx`, `id_rsa`, `id_ed25519`, `id_dsa`, `id_ecdsa`, `credentials.json`, `secrets.json`, `secrets.yaml`, `secrets.yml`.
82
+ - OS metadata: `.DS_Store`, `Thumbs.db`, `desktop.ini`, `ehthumbs.db`.
83
+ - Editor swap / backup files: `*.swp`, `*.swo`, `*~`, `*.orig`, `*.bak`, `*.rej`.
84
+ - Dependency / build / cache directories that virtually never belong in source control: `node_modules/`, `__pycache__/`, `*.pyc`, `*.pyo`, `.pytest_cache/`, `.mypy_cache/`, `.ruff_cache/`, `.tox/`, `.coverage`, `htmlcov/`, `*.egg-info/`, `.next/`, `.nuxt/`, `.turbo/`, `.parcel-cache/`, `.venv/`, `venv/`, `dist/`, `build/`, `target/debug/`, `target/release/`, `coverage/`.
85
+ - Runtime databases / data dumps that look like artifacts: `*.sqlite`, `*.sqlite3`, `*.db`, large data dumps in obvious artifact locations.
86
+ - Generic temp files: `*.tmp`, `*.temp`, core dumps (`core.*`).
87
+
88
+ Apply judgment — these patterns are heuristics, not absolute rules. If a file matches a pattern but the project genuinely tracks it on purpose (e.g., a fixture SQLite under `tests/fixtures/`, a vendored binary, a checked-in `dist/` for a published artifact), do not flag it. Conversely, flag files that clearly look like artifacts even if they don't match the patterns above.
74
89
 
75
90
  ## Output Format
76
91
  Return valid JSON matching this structure:
@@ -97,6 +112,7 @@ Return valid JSON matching this structure:
97
112
  {"type": "refactor", "scope": "api", "msg": "restructure endpoint handlers", "is_breaking": false, "secrets": []}
98
113
  {"type": "feat", "scope": "api", "msg": "add user deletion endpoint", "is_breaking": true, "secrets": []}
99
114
  {"type": "chore", "scope": "deps", "msg": "update dependencies", "is_breaking": false, "secrets": [{"file": "config/.env", "description": "detected value resembling api key", "level": "error"}]}
115
+ {"type": "chore", "scope": null, "msg": "update build pipeline", "is_breaking": false, "secrets": [{"file": "logs/server.log", "description": "log file should not be committed", "level": "error"}, {"file": "node_modules/lodash/package.json", "description": "node_modules dependency directory should not be committed", "level": "error"}]}
100
116
  ```
101
117
 
102
118
  Now analyze the provided diff and generate the commit message.