tgit 0.25.0__tar.gz → 0.26.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 (48) hide show
  1. {tgit-0.25.0 → tgit-0.26.0}/CHANGELOG.md +12 -0
  2. {tgit-0.25.0 → tgit-0.26.0}/PKG-INFO +1 -1
  3. {tgit-0.25.0 → tgit-0.26.0}/pyproject.toml +1 -1
  4. {tgit-0.25.0 → tgit-0.26.0}/tests/unit/test_commit.py +70 -5
  5. {tgit-0.25.0 → tgit-0.26.0}/tgit/commit.py +21 -4
  6. {tgit-0.25.0 → tgit-0.26.0}/tgit/prompts/commit.txt +18 -5
  7. tgit-0.26.0/uv.lock +902 -0
  8. tgit-0.25.0/uv.lock +0 -874
  9. {tgit-0.25.0 → tgit-0.26.0}/.claude/settings.local.json +0 -0
  10. {tgit-0.25.0 → tgit-0.26.0}/.github/workflows/build.yml +0 -0
  11. {tgit-0.25.0 → tgit-0.26.0}/.github/workflows/ci.yml +0 -0
  12. {tgit-0.25.0 → tgit-0.26.0}/.github/workflows/claude-code-review.yml +0 -0
  13. {tgit-0.25.0 → tgit-0.26.0}/.github/workflows/claude.yml +0 -0
  14. {tgit-0.25.0 → tgit-0.26.0}/.gitignore +0 -0
  15. {tgit-0.25.0 → tgit-0.26.0}/.python-version +0 -0
  16. {tgit-0.25.0 → tgit-0.26.0}/.tgit/settings.json +0 -0
  17. {tgit-0.25.0 → tgit-0.26.0}/.vscode/extensions.json +0 -0
  18. {tgit-0.25.0 → tgit-0.26.0}/.vscode/launch.json +0 -0
  19. {tgit-0.25.0 → tgit-0.26.0}/CLAUDE.md +0 -0
  20. {tgit-0.25.0 → tgit-0.26.0}/README.md +0 -0
  21. {tgit-0.25.0 → tgit-0.26.0}/pyrightconfig.json +0 -0
  22. {tgit-0.25.0 → tgit-0.26.0}/scripts/publish.sh +0 -0
  23. {tgit-0.25.0 → tgit-0.26.0}/scripts/test.sh +0 -0
  24. {tgit-0.25.0 → tgit-0.26.0}/tests/README.md +0 -0
  25. {tgit-0.25.0 → tgit-0.26.0}/tests/__init__.py +0 -0
  26. {tgit-0.25.0 → tgit-0.26.0}/tests/conftest.py +0 -0
  27. {tgit-0.25.0 → tgit-0.26.0}/tests/integration/__init__.py +0 -0
  28. {tgit-0.25.0 → tgit-0.26.0}/tests/integration/test_version_integration.py +0 -0
  29. {tgit-0.25.0 → tgit-0.26.0}/tests/unit/__init__.py +0 -0
  30. {tgit-0.25.0 → tgit-0.26.0}/tests/unit/test_add.py +0 -0
  31. {tgit-0.25.0 → tgit-0.26.0}/tests/unit/test_changelog.py +0 -0
  32. {tgit-0.25.0 → tgit-0.26.0}/tests/unit/test_cli.py +0 -0
  33. {tgit-0.25.0 → tgit-0.26.0}/tests/unit/test_interactive_settings.py +0 -0
  34. {tgit-0.25.0 → tgit-0.26.0}/tests/unit/test_settings.py +0 -0
  35. {tgit-0.25.0 → tgit-0.26.0}/tests/unit/test_types.py +0 -0
  36. {tgit-0.25.0 → tgit-0.26.0}/tests/unit/test_utils.py +0 -0
  37. {tgit-0.25.0 → tgit-0.26.0}/tests/unit/test_version.py +0 -0
  38. {tgit-0.25.0 → tgit-0.26.0}/tgit/__init__.py +0 -0
  39. {tgit-0.25.0 → tgit-0.26.0}/tgit/add.py +0 -0
  40. {tgit-0.25.0 → tgit-0.26.0}/tgit/changelog.py +0 -0
  41. {tgit-0.25.0 → tgit-0.26.0}/tgit/cli.py +0 -0
  42. {tgit-0.25.0 → tgit-0.26.0}/tgit/constants.py +0 -0
  43. {tgit-0.25.0 → tgit-0.26.0}/tgit/interactive_settings.py +0 -0
  44. {tgit-0.25.0 → tgit-0.26.0}/tgit/settings.py +0 -0
  45. {tgit-0.25.0 → tgit-0.26.0}/tgit/shared.py +0 -0
  46. {tgit-0.25.0 → tgit-0.26.0}/tgit/types.py +0 -0
  47. {tgit-0.25.0 → tgit-0.26.0}/tgit/utils/__init__.py +0 -0
  48. {tgit-0.25.0 → tgit-0.26.0}/tgit/version.py +0 -0
@@ -1,3 +1,15 @@
1
+ ## v0.26.0
2
+
3
+ [v0.25.0...v0.26.0](https://github.com/Jannchie/tgit/compare/v0.25.0...v0.26.0)
4
+
5
+ ### :sparkles: Features
6
+
7
+ - **commit**: add secret detection support - By [Jannchie](mailto:jannchie@gmail.com) in [e1f5207](https://github.com/Jannchie/tgit/commit/e1f5207)
8
+
9
+ ### :wrench: Chores
10
+
11
+ - **commit**: add default token limit and defaults to commit model - By [Jannchie](mailto:jannchie@gmail.com) in [66d4d45](https://github.com/Jannchie/tgit/commit/66d4d45)
12
+
1
13
  ## v0.25.0
2
14
 
3
15
  [v0.24.1...v0.25.0](https://github.com/Jannchie/tgit/compare/v0.24.1...v0.25.0)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: tgit
3
- Version: 0.25.0
3
+ Version: 0.26.0
4
4
  Summary: Tool for Git Interaction Temptation (tgit): An elegant CLI tool that simplifies and streamlines your Git workflow, making version control a breeze.
5
5
  Author-email: Jannchie <jannchie@gmail.com>
6
6
  License-Expression: MIT
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "tgit"
3
- version = "0.25.0"
3
+ version = "0.26.0"
4
4
  description = "Tool for Git Interaction Temptation (tgit): An elegant CLI tool that simplifies and streamlines your Git workflow, making version control a breeze."
5
5
  authors = [{ name = "Jannchie", email = "jannchie@gmail.com" }]
6
6
  dependencies = [
@@ -10,6 +10,7 @@ from click.testing import CliRunner
10
10
  from tgit.commit import (
11
11
  CommitArgs,
12
12
  CommitData,
13
+ PotentialSecret,
13
14
  TemplateParams,
14
15
  get_changed_files_from_status,
15
16
  get_file_change_sizes,
@@ -61,7 +62,7 @@ class TestCommitData:
61
62
 
62
63
  def test_commit_data_creation(self):
63
64
  """Test creating CommitData instance."""
64
- data = CommitData(type="feat", scope="auth", msg="add login functionality", is_breaking=False)
65
+ data = CommitData(type="feat", scope="auth", msg="add login functionality", is_breaking=False, secrets=[])
65
66
  assert data.type == "feat"
66
67
  assert data.scope == "auth"
67
68
  assert data.msg == "add login functionality"
@@ -69,9 +70,16 @@ class TestCommitData:
69
70
 
70
71
  def test_commit_data_with_none_scope(self):
71
72
  """Test CommitData with None scope."""
72
- data = CommitData(type="fix", scope=None, msg="fix bug", is_breaking=False)
73
+ data = CommitData(type="fix", scope=None, msg="fix bug", is_breaking=False, secrets=[])
73
74
  assert data.scope is None
74
75
 
76
+ def test_commit_data_with_secrets(self):
77
+ """Test CommitData with suspected secrets."""
78
+ secret = PotentialSecret(file="config.env", description="looks like api key")
79
+ data = CommitData(type="chore", scope=None, msg="update config", is_breaking=False, secrets=[secret])
80
+ assert len(data.secrets) == 1
81
+ assert data.secrets[0].file == "config.env"
82
+
75
83
 
76
84
  class TestGetChangedFilesFromStatus:
77
85
  """Test get_changed_files_from_status function."""
@@ -296,7 +304,7 @@ class TestGenerateCommitWithAI:
296
304
 
297
305
  # Mock the response
298
306
  mock_response = Mock()
299
- mock_commit_data = CommitData(type="feat", scope="auth", msg="add login", is_breaking=False)
307
+ mock_commit_data = CommitData(type="feat", scope="auth", msg="add login", is_breaking=False, secrets=[])
300
308
  mock_response.output_parsed = mock_commit_data
301
309
  mock_client.responses.parse.return_value = mock_response
302
310
 
@@ -334,7 +342,7 @@ class TestGenerateCommitWithAI:
334
342
  mock_settings.model = "o1-mini"
335
343
 
336
344
  mock_response = Mock()
337
- mock_commit_data = CommitData(type="fix", scope=None, msg="correct bug", is_breaking=False)
345
+ mock_commit_data = CommitData(type="fix", scope=None, msg="correct bug", is_breaking=False, secrets=[])
338
346
  mock_response.output_parsed = mock_commit_data
339
347
  mock_client.responses.parse.return_value = mock_response
340
348
 
@@ -396,7 +404,7 @@ class TestGetAICommand:
396
404
  mock_repo_instance.active_branch.name = "main"
397
405
  mock_settings.commit.emoji = True
398
406
 
399
- mock_commit_data = CommitData(type="feat", scope="auth", msg="add login", is_breaking=False)
407
+ mock_commit_data = CommitData(type="feat", scope="auth", msg="add login", is_breaking=False, secrets=[])
400
408
  mock_generate.return_value = mock_commit_data
401
409
  mock_get_commit_command.return_value = "git commit -m 'feat(auth): add login'"
402
410
 
@@ -406,6 +414,63 @@ class TestGetAICommand:
406
414
  mock_generate.assert_called_once()
407
415
  mock_get_commit_command.assert_called_once_with("feat", "auth", "add login", use_emoji=True, is_breaking=False)
408
416
 
417
+ @patch("tgit.commit.click.confirm")
418
+ @patch("tgit.commit.Path.cwd")
419
+ @patch("tgit.commit.git.Repo")
420
+ @patch("tgit.commit.get_filtered_diff_files")
421
+ @patch("tgit.commit._generate_commit_with_ai")
422
+ @patch("tgit.commit.get_commit_command")
423
+ @patch("tgit.commit.settings")
424
+ def test_get_ai_command_detected_secrets_abort(self, mock_settings, mock_get_commit_command, mock_generate, mock_get_files, mock_repo, mock_cwd, mock_confirm):
425
+ """Test get_ai_command aborts when secrets are detected and user declines."""
426
+ mock_cwd.return_value = Path(tempfile.gettempdir())
427
+ mock_repo_instance = Mock()
428
+ mock_repo.return_value = mock_repo_instance
429
+ mock_get_files.return_value = (["src/file.py"], [])
430
+ mock_repo_instance.git.diff.return_value = "diff content"
431
+ mock_repo_instance.active_branch.name = "main"
432
+ mock_settings.commit.emoji = True
433
+
434
+ secret = PotentialSecret(file="src/file.py", description="possible api key")
435
+ mock_commit_data = CommitData(type="feat", scope="auth", msg="add login", is_breaking=False, secrets=[secret])
436
+ mock_generate.return_value = mock_commit_data
437
+ mock_confirm.return_value = False
438
+
439
+ result = get_ai_command()
440
+
441
+ assert result is None
442
+ mock_confirm.assert_called_once_with("Detected potential secrets. Continue with commit?", default=False)
443
+ mock_get_commit_command.assert_not_called()
444
+
445
+ @patch("tgit.commit.click.confirm")
446
+ @patch("tgit.commit.Path.cwd")
447
+ @patch("tgit.commit.git.Repo")
448
+ @patch("tgit.commit.get_filtered_diff_files")
449
+ @patch("tgit.commit._generate_commit_with_ai")
450
+ @patch("tgit.commit.get_commit_command")
451
+ @patch("tgit.commit.settings")
452
+ def test_get_ai_command_detected_secrets_continue(self, mock_settings, mock_get_commit_command, mock_generate, mock_get_files, mock_repo, mock_cwd, mock_confirm):
453
+ """Test get_ai_command continues when secrets are detected and user agrees."""
454
+ mock_cwd.return_value = Path(tempfile.gettempdir())
455
+ mock_repo_instance = Mock()
456
+ mock_repo.return_value = mock_repo_instance
457
+ mock_get_files.return_value = (["src/file.py"], [])
458
+ mock_repo_instance.git.diff.return_value = "diff content"
459
+ mock_repo_instance.active_branch.name = "main"
460
+ mock_settings.commit.emoji = True
461
+
462
+ secret = PotentialSecret(file="src/file.py", description="possible api key")
463
+ mock_commit_data = CommitData(type="feat", scope="auth", msg="add login", is_breaking=False, secrets=[secret])
464
+ mock_generate.return_value = mock_commit_data
465
+ mock_get_commit_command.return_value = "git commit -m 'feat(auth): add login'"
466
+ mock_confirm.return_value = True
467
+
468
+ result = get_ai_command()
469
+
470
+ assert result == "git commit -m 'feat(auth): add login'"
471
+ mock_confirm.assert_called_once_with("Detected potential secrets. Continue with commit?", default=False)
472
+ mock_get_commit_command.assert_called_once_with("feat", "auth", "add login", use_emoji=True, is_breaking=False)
473
+
409
474
  @patch("tgit.commit.Path.cwd")
410
475
  @patch("tgit.commit.git.Repo")
411
476
  @patch("tgit.commit.get_filtered_diff_files")
@@ -8,7 +8,7 @@ from typing import TYPE_CHECKING, Any
8
8
  import click
9
9
  import git
10
10
  from jinja2 import Environment, FileSystemLoader
11
- from pydantic import BaseModel
11
+ from pydantic import BaseModel, Field
12
12
  from rich import get_console, print
13
13
 
14
14
  from tgit.constants import DEFAULT_MODEL, REASONING_MODEL_HINTS
@@ -25,6 +25,7 @@ with importlib.resources.path("tgit", "prompts") as prompt_path:
25
25
  commit_types = ["feat", "fix", "chore", "docs", "style", "refactor", "perf", "wip"]
26
26
  commit_file = "commit.txt"
27
27
  commit_prompt_template = env.get_template("commit.txt")
28
+ DEFAULT_MAX_OUTPUT_TOKENS = 256
28
29
 
29
30
  # Define click arguments/options at module level to avoid B008
30
31
  MESSAGE_ARG = click.argument(
@@ -65,11 +66,17 @@ class TemplateParams:
65
66
  specified_type: str | None = None
66
67
 
67
68
 
69
+ class PotentialSecret(BaseModel):
70
+ file: str
71
+ description: str
72
+
73
+
68
74
  class CommitData(BaseModel):
69
75
  type: str
70
- scope: str | None
76
+ scope: str | None = None
71
77
  msg: str
72
- is_breaking: bool
78
+ is_breaking: bool = False
79
+ secrets: list[PotentialSecret] = Field(default_factory=list)
73
80
 
74
81
 
75
82
  def _supports_reasoning(model: str) -> bool:
@@ -196,7 +203,7 @@ def _generate_commit_with_ai(diff: str, specified_type: str | None, current_bran
196
203
  {"role": "user", "content": diff},
197
204
  ],
198
205
  "model": model_name,
199
- "max_output_tokens": 50,
206
+ "max_output_tokens": DEFAULT_MAX_OUTPUT_TOKENS,
200
207
  "text_format": CommitData,
201
208
  }
202
209
  if _supports_reasoning(model_name):
@@ -243,6 +250,16 @@ def get_ai_command(specified_type: str | None = None) -> str | None:
243
250
  print(e)
244
251
  return None
245
252
 
253
+ detected_secrets: list[PotentialSecret] = resp.secrets if resp.secrets else []
254
+ if detected_secrets:
255
+ print("[red]Detected potential secrets in these files:[/red]")
256
+ for secret in detected_secrets:
257
+ print(f"[red]- {secret.file}: {secret.description}[/red]")
258
+ proceed = click.confirm("Detected potential secrets. Continue with commit?", default=False)
259
+ if not proceed:
260
+ print("[yellow]Commit aborted. Please review sensitive content.[/yellow]")
261
+ return None
262
+
246
263
  # 如果用户指定了类型,则使用用户指定的类型,否则使用 AI 生成的类型
247
264
  commit_type = specified_type if specified_type is not None else resp.type
248
265
 
@@ -34,12 +34,18 @@ Mark `is_breaking: true` only if changes:
34
34
  - Require user action for compatibility
35
35
  - Remove or significantly change existing functionality
36
36
 
37
+ ### Secret Detection
38
+ - Review the diff for potential secrets (API keys, tokens, passwords, private keys, credentials, etc.).
39
+ - For every suspected secret, add an entry to the `secrets` array with the file path and a brief description of why it is sensitive.
40
+ - If no secrets are found, return an empty array.
41
+
37
42
  ## Analysis Process
38
43
  1. Review the diff comprehensively
39
44
  2. Identify the primary type of change
40
45
  3. Determine appropriate scope from modified files/areas
41
46
  4. Craft a concise message covering main changes
42
47
  5. Assess backward compatibility impact
48
+ 6. Flag any suspected secrets
43
49
 
44
50
  ## Output Format
45
51
  Return valid JSON matching this structure:
@@ -48,15 +54,22 @@ Return valid JSON matching this structure:
48
54
  "type": "string",
49
55
  "scope": "string|null",
50
56
  "msg": "string",
51
- "is_breaking": "boolean"
57
+ "is_breaking": "boolean",
58
+ "secrets": [
59
+ {
60
+ "file": "string",
61
+ "description": "string"
62
+ }
63
+ ]
52
64
  }
53
65
  ```
54
66
 
55
67
  ## Examples
56
68
  ```json
57
- {"type": "feat", "scope": "auth", "msg": "add oauth2 login", "is_breaking": false}
58
- {"type": "fix", "scope": "api", "msg": "handle null user responses", "is_breaking": false}
59
- {"type": "refactor", "scope": null, "msg": "restructure project layout", "is_breaking": true}
69
+ {"type": "feat", "scope": "auth", "msg": "add oauth2 login", "is_breaking": false, "secrets": []}
70
+ {"type": "fix", "scope": "api", "msg": "handle null user responses", "is_breaking": false, "secrets": []}
71
+ {"type": "refactor", "scope": null, "msg": "restructure project layout", "is_breaking": true, "secrets": []}
72
+ {"type": "chore", "scope": "config", "msg": "update secrets storage", "is_breaking": false, "secrets": [{"file": "config/.env", "description": "detected value resembling api key"}]}
60
73
  ```
61
74
 
62
- Now analyze the provided diff and generate the commit message.
75
+ Now analyze the provided diff and generate the commit message.