git-cai-cli 0.17.0__tar.gz → 0.17.1__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 (113) hide show
  1. {git_cai_cli-0.17.0 → git_cai_cli-0.17.1}/PKG-INFO +1 -1
  2. {git_cai_cli-0.17.0 → git_cai_cli-0.17.1}/docs/git-cai.txt +2 -1
  3. {git_cai_cli-0.17.0 → git_cai_cli-0.17.1}/docs/man/git-cai.1 +4 -3
  4. {git_cai_cli-0.17.0 → git_cai_cli-0.17.1}/src/git_cai_cli/_version.py +3 -3
  5. {git_cai_cli-0.17.0 → git_cai_cli-0.17.1}/src/git_cai_cli/cli/cli.py +0 -1
  6. {git_cai_cli-0.17.0 → git_cai_cli-0.17.1}/src/git_cai_cli/cli/modes.py +3 -2
  7. {git_cai_cli-0.17.0 → git_cai_cli-0.17.1}/src/git_cai_cli/core/gitutils.py +12 -4
  8. {git_cai_cli-0.17.0 → git_cai_cli-0.17.1}/src/git_cai_cli/core/init.py +4 -4
  9. {git_cai_cli-0.17.0 → git_cai_cli-0.17.1}/src/git_cai_cli/core/options.py +4 -8
  10. {git_cai_cli-0.17.0 → git_cai_cli-0.17.1}/src/git_cai_cli/core/squash.py +3 -4
  11. {git_cai_cli-0.17.0 → git_cai_cli-0.17.1}/src/git_cai_cli/main.py +4 -2
  12. {git_cai_cli-0.17.0 → git_cai_cli-0.17.1}/src/git_cai_cli.egg-info/PKG-INFO +1 -1
  13. git_cai_cli-0.17.1/src/git_cai_cli.egg-info/scm_version.json +8 -0
  14. {git_cai_cli-0.17.0 → git_cai_cli-0.17.1}/tests/integration/test_gitutils_integration.py +22 -0
  15. {git_cai_cli-0.17.0 → git_cai_cli-0.17.1}/tests/integration/test_modes_integration.py +4 -16
  16. {git_cai_cli-0.17.0 → git_cai_cli-0.17.1}/tests/unit/test_gitutils.py +12 -12
  17. {git_cai_cli-0.17.0 → git_cai_cli-0.17.1}/tests/unit/test_options.py +1 -1
  18. {git_cai_cli-0.17.0 → git_cai_cli-0.17.1}/tests/unit/test_squash.py +4 -5
  19. git_cai_cli-0.17.0/src/git_cai_cli.egg-info/scm_version.json +0 -8
  20. {git_cai_cli-0.17.0 → git_cai_cli-0.17.1}/.caiignore +0 -0
  21. {git_cai_cli-0.17.0 → git_cai_cli-0.17.1}/.gitattributes +0 -0
  22. {git_cai_cli-0.17.0 → git_cai_cli-0.17.1}/.github/cd/.SRCINFO +0 -0
  23. {git_cai_cli-0.17.0 → git_cai_cli-0.17.1}/.github/cd/PKGBUILD +0 -0
  24. {git_cai_cli-0.17.0 → git_cai_cli-0.17.1}/.github/ci/_version.py +0 -0
  25. {git_cai_cli-0.17.0 → git_cai_cli-0.17.1}/.github/ci/cai_config.ci.yml +0 -0
  26. {git_cai_cli-0.17.0 → git_cai_cli-0.17.1}/.github/ci/tokens.ci.yml +0 -0
  27. {git_cai_cli-0.17.0 → git_cai_cli-0.17.1}/.github/workflows/python-tests.yml +0 -0
  28. {git_cai_cli-0.17.0 → git_cai_cli-0.17.1}/.github/workflows/release.yml +0 -0
  29. {git_cai_cli-0.17.0 → git_cai_cli-0.17.1}/.github/workflows/release_aur.yml +0 -0
  30. {git_cai_cli-0.17.0 → git_cai_cli-0.17.1}/.gitignore +0 -0
  31. {git_cai_cli-0.17.0 → git_cai_cli-0.17.1}/.linters/.bandit.yml +0 -0
  32. {git_cai_cli-0.17.0 → git_cai_cli-0.17.1}/.linters/.checkov.yml +0 -0
  33. {git_cai_cli-0.17.0 → git_cai_cli-0.17.1}/.linters/.flake8 +0 -0
  34. {git_cai_cli-0.17.0 → git_cai_cli-0.17.1}/.linters/.ls-lint.yml +0 -0
  35. {git_cai_cli-0.17.0 → git_cai_cli-0.17.1}/.linters/.markdownlint.json +0 -0
  36. {git_cai_cli-0.17.0 → git_cai_cli-0.17.1}/.linters/.proselintrc.json +0 -0
  37. {git_cai_cli-0.17.0 → git_cai_cli-0.17.1}/.linters/.pylintrc +0 -0
  38. {git_cai_cli-0.17.0 → git_cai_cli-0.17.1}/.linters/.yamllint.yml +0 -0
  39. {git_cai_cli-0.17.0 → git_cai_cli-0.17.1}/.linters/check_git_branch_name.sh +0 -0
  40. {git_cai_cli-0.17.0 → git_cai_cli-0.17.1}/.linters/lychee.toml +0 -0
  41. {git_cai_cli-0.17.0 → git_cai_cli-0.17.1}/.linters/pyrightconfig.json +0 -0
  42. {git_cai_cli-0.17.0 → git_cai_cli-0.17.1}/.markdownlintignore +0 -0
  43. {git_cai_cli-0.17.0 → git_cai_cli-0.17.1}/.mega-linter.yml +0 -0
  44. {git_cai_cli-0.17.0 → git_cai_cli-0.17.1}/.semgrepignore +0 -0
  45. {git_cai_cli-0.17.0 → git_cai_cli-0.17.1}/.trivyignore +0 -0
  46. {git_cai_cli-0.17.0 → git_cai_cli-0.17.1}/CLAUDE.md +0 -0
  47. {git_cai_cli-0.17.0 → git_cai_cli-0.17.1}/LICENSE +0 -0
  48. {git_cai_cli-0.17.0 → git_cai_cli-0.17.1}/Makefile +0 -0
  49. {git_cai_cli-0.17.0 → git_cai_cli-0.17.1}/README.md +0 -0
  50. {git_cai_cli-0.17.0 → git_cai_cli-0.17.1}/cai_config.yml +0 -0
  51. {git_cai_cli-0.17.0 → git_cai_cli-0.17.1}/pyproject.toml +0 -0
  52. {git_cai_cli-0.17.0 → git_cai_cli-0.17.1}/setup.cfg +0 -0
  53. {git_cai_cli-0.17.0 → git_cai_cli-0.17.1}/src/git_cai_cli/__init__.py +0 -0
  54. {git_cai_cli-0.17.0 → git_cai_cli-0.17.1}/src/git_cai_cli/cli/__init__.py +0 -0
  55. {git_cai_cli-0.17.0 → git_cai_cli-0.17.1}/src/git_cai_cli/cli/helptext.py +0 -0
  56. {git_cai_cli-0.17.0 → git_cai_cli-0.17.1}/src/git_cai_cli/core/__init__.py +0 -0
  57. {git_cai_cli-0.17.0 → git_cai_cli-0.17.1}/src/git_cai_cli/core/changelog.py +0 -0
  58. {git_cai_cli-0.17.0 → git_cai_cli-0.17.1}/src/git_cai_cli/core/completion.py +0 -0
  59. {git_cai_cli-0.17.0 → git_cai_cli-0.17.1}/src/git_cai_cli/core/config.py +0 -0
  60. {git_cai_cli-0.17.0 → git_cai_cli-0.17.1}/src/git_cai_cli/core/doctor.py +0 -0
  61. {git_cai_cli-0.17.0 → git_cai_cli-0.17.1}/src/git_cai_cli/core/explain.py +0 -0
  62. {git_cai_cli-0.17.0 → git_cai_cli-0.17.1}/src/git_cai_cli/core/generation.py +0 -0
  63. {git_cai_cli-0.17.0 → git_cai_cli-0.17.1}/src/git_cai_cli/core/languages.py +0 -0
  64. {git_cai_cli-0.17.0 → git_cai_cli-0.17.1}/src/git_cai_cli/core/llm.py +0 -0
  65. {git_cai_cli-0.17.0 → git_cai_cli-0.17.1}/src/git_cai_cli/core/pr.py +0 -0
  66. {git_cai_cli-0.17.0 → git_cai_cli-0.17.1}/src/git_cai_cli/core/prompts_fallback.py +0 -0
  67. {git_cai_cli-0.17.0 → git_cai_cli-0.17.1}/src/git_cai_cli/core/release.py +0 -0
  68. {git_cai_cli-0.17.0 → git_cai_cli-0.17.1}/src/git_cai_cli/core/secrets.py +0 -0
  69. {git_cai_cli-0.17.0 → git_cai_cli-0.17.1}/src/git_cai_cli/core/spinner.py +0 -0
  70. {git_cai_cli-0.17.0 → git_cai_cli-0.17.1}/src/git_cai_cli/core/split.py +0 -0
  71. {git_cai_cli-0.17.0 → git_cai_cli-0.17.1}/src/git_cai_cli/core/stats.py +0 -0
  72. {git_cai_cli-0.17.0 → git_cai_cli-0.17.1}/src/git_cai_cli/core/validate.py +0 -0
  73. {git_cai_cli-0.17.0 → git_cai_cli-0.17.1}/src/git_cai_cli.egg-info/SOURCES.txt +0 -0
  74. {git_cai_cli-0.17.0 → git_cai_cli-0.17.1}/src/git_cai_cli.egg-info/dependency_links.txt +0 -0
  75. {git_cai_cli-0.17.0 → git_cai_cli-0.17.1}/src/git_cai_cli.egg-info/entry_points.txt +0 -0
  76. {git_cai_cli-0.17.0 → git_cai_cli-0.17.1}/src/git_cai_cli.egg-info/requires.txt +0 -0
  77. {git_cai_cli-0.17.0 → git_cai_cli-0.17.1}/src/git_cai_cli.egg-info/scm_file_list.json +0 -0
  78. {git_cai_cli-0.17.0 → git_cai_cli-0.17.1}/src/git_cai_cli.egg-info/top_level.txt +0 -0
  79. {git_cai_cli-0.17.0 → git_cai_cli-0.17.1}/tests/conftest.py +0 -0
  80. {git_cai_cli-0.17.0 → git_cai_cli-0.17.1}/tests/integration/test_cli_integration.py +0 -0
  81. {git_cai_cli-0.17.0 → git_cai_cli-0.17.1}/tests/integration/test_config_integration.py +0 -0
  82. {git_cai_cli-0.17.0 → git_cai_cli-0.17.1}/tests/integration/test_options_integration.py +0 -0
  83. {git_cai_cli-0.17.0 → git_cai_cli-0.17.1}/tests/integration/test_pr_integration.py +0 -0
  84. {git_cai_cli-0.17.0 → git_cai_cli-0.17.1}/tests/integration/test_squash_integration.py +0 -0
  85. {git_cai_cli-0.17.0 → git_cai_cli-0.17.1}/tests/unit/test_amend.py +0 -0
  86. {git_cai_cli-0.17.0 → git_cai_cli-0.17.1}/tests/unit/test_branch_context.py +0 -0
  87. {git_cai_cli-0.17.0 → git_cai_cli-0.17.1}/tests/unit/test_changelog.py +0 -0
  88. {git_cai_cli-0.17.0 → git_cai_cli-0.17.1}/tests/unit/test_classification.py +0 -0
  89. {git_cai_cli-0.17.0 → git_cai_cli-0.17.1}/tests/unit/test_cli.py +0 -0
  90. {git_cai_cli-0.17.0 → git_cai_cli-0.17.1}/tests/unit/test_completion.py +0 -0
  91. {git_cai_cli-0.17.0 → git_cai_cli-0.17.1}/tests/unit/test_config.py +0 -0
  92. {git_cai_cli-0.17.0 → git_cai_cli-0.17.1}/tests/unit/test_conventional.py +0 -0
  93. {git_cai_cli-0.17.0 → git_cai_cli-0.17.1}/tests/unit/test_custom_provider.py +0 -0
  94. {git_cai_cli-0.17.0 → git_cai_cli-0.17.1}/tests/unit/test_doctor.py +0 -0
  95. {git_cai_cli-0.17.0 → git_cai_cli-0.17.1}/tests/unit/test_explain.py +0 -0
  96. {git_cai_cli-0.17.0 → git_cai_cli-0.17.1}/tests/unit/test_generation.py +0 -0
  97. {git_cai_cli-0.17.0 → git_cai_cli-0.17.1}/tests/unit/test_helptext.py +0 -0
  98. {git_cai_cli-0.17.0 → git_cai_cli-0.17.1}/tests/unit/test_init.py +0 -0
  99. {git_cai_cli-0.17.0 → git_cai_cli-0.17.1}/tests/unit/test_llm.py +0 -0
  100. {git_cai_cli-0.17.0 → git_cai_cli-0.17.1}/tests/unit/test_main.py +0 -0
  101. {git_cai_cli-0.17.0 → git_cai_cli-0.17.1}/tests/unit/test_modes.py +0 -0
  102. {git_cai_cli-0.17.0 → git_cai_cli-0.17.1}/tests/unit/test_pr.py +0 -0
  103. {git_cai_cli-0.17.0 → git_cai_cli-0.17.1}/tests/unit/test_print.py +0 -0
  104. {git_cai_cli-0.17.0 → git_cai_cli-0.17.1}/tests/unit/test_prompt_loading.py +0 -0
  105. {git_cai_cli-0.17.0 → git_cai_cli-0.17.1}/tests/unit/test_release.py +0 -0
  106. {git_cai_cli-0.17.0 → git_cai_cli-0.17.1}/tests/unit/test_secrets.py +0 -0
  107. {git_cai_cli-0.17.0 → git_cai_cli-0.17.1}/tests/unit/test_set_config.py +0 -0
  108. {git_cai_cli-0.17.0 → git_cai_cli-0.17.1}/tests/unit/test_signoff.py +0 -0
  109. {git_cai_cli-0.17.0 → git_cai_cli-0.17.1}/tests/unit/test_spinner.py +0 -0
  110. {git_cai_cli-0.17.0 → git_cai_cli-0.17.1}/tests/unit/test_split.py +0 -0
  111. {git_cai_cli-0.17.0 → git_cai_cli-0.17.1}/tests/unit/test_stats.py +0 -0
  112. {git_cai_cli-0.17.0 → git_cai_cli-0.17.1}/tests/unit/test_validate.py +0 -0
  113. {git_cai_cli-0.17.0 → git_cai_cli-0.17.1}/uv.lock +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: git-cai-cli
3
- Version: 0.17.0
3
+ Version: 0.17.1
4
4
  Summary: Use LLM to create git commit messages
5
5
  Author-email: Thorsten Foltz <thorsten.foltz@live.com>
6
6
  License-Expression: MIT
@@ -324,7 +324,8 @@ Useful when a working tree contains unrelated staged changes and the commit
324
324
  should focus on a subset of them, or when you want the LLM to concentrate
325
325
  on a specific file.
326
326
  +
327
- Cannot be used with `--list`, `--update`, or `--squash`.
327
+ Only valid in the default commit mode; cannot be used with `--amend`,
328
+ `--list`, `--update`, or `--squash`.
328
329
  +
329
330
  ----
330
331
  git cai -f src/foo.py
@@ -2,12 +2,12 @@
2
2
  .\" Title: git-cai
3
3
  .\" Author: Thorsten Foltz
4
4
  .\" Generator: Asciidoctor 2.0.26
5
- .\" Date: 2026-09-25
5
+ .\" Date: 2026-10-11
6
6
  .\" Manual: \ \&
7
7
  .\" Source: \ \&
8
8
  .\" Language: English
9
9
  .\"
10
- .TH "GIT\-CAI" "1" "2026-09-25" "\ \&" "\ \&"
10
+ .TH "GIT\-CAI" "1" "2026-10-11" "\ \&" "\ \&"
11
11
  .ie \n(.g .ds Aq \(aq
12
12
  .el .ds Aq '
13
13
  .ss \n[.ss] 0
@@ -606,7 +606,8 @@ Useful when a working tree contains unrelated staged changes and the commit
606
606
  should focus on a subset of them, or when you want the LLM to concentrate
607
607
  on a specific file.
608
608
  .sp
609
- Cannot be used with \f(CR\-\-list\fP, \f(CR\-\-update\fP, or \f(CR\-\-squash\fP.
609
+ Only valid in the default commit mode; cannot be used with \f(CR\-\-amend\fP,
610
+ \f(CR\-\-list\fP, \f(CR\-\-update\fP, or \f(CR\-\-squash\fP.
610
611
  .sp
611
612
  .if n .RS 4
612
613
  .nf
@@ -18,7 +18,7 @@ version_tuple: tuple[int | str, ...]
18
18
  commit_id: str | None
19
19
  __commit_id__: str | None
20
20
 
21
- __version__ = version = '0.17.0'
22
- __version_tuple__ = version_tuple = (0, 17, 0)
21
+ __version__ = version = '0.17.1'
22
+ __version_tuple__ = version_tuple = (0, 17, 1)
23
23
 
24
- __commit_id__ = commit_id = 'g457d701c5'
24
+ __commit_id__ = commit_id = 'g1fd24936a'
@@ -83,7 +83,6 @@ def callback( # pylint: disable=too-many-arguments,too-many-positional-argument
83
83
  "--list",
84
84
  "-l",
85
85
  help="List information (e.g. languages, styles, providers, config)",
86
- is_flag=True,
87
86
  ),
88
87
  list_arg: str = typer.Argument(
89
88
  None, help="Optional argument for --list or --squash (see docs)"
@@ -163,9 +163,10 @@ def validate_options(
163
163
  )
164
164
  raise typer.Exit(code=1)
165
165
 
166
- if files and mode not in (Mode.COMMIT, Mode.AMEND):
166
+ # AMEND regenerates from the last commit's diff and never reads --files.
167
+ if files and mode is not Mode.COMMIT:
167
168
  typer.echo(
168
- "Error: --files can only be used in COMMIT or AMEND mode.",
169
+ "Error: --files can only be used in COMMIT mode.",
169
170
  err=True,
170
171
  )
171
172
  raise typer.Exit(code=1)
@@ -328,12 +328,18 @@ def git_diff_excluding(
328
328
  """Run `git diff --cached` honoring `.caiignore` and an optional file whitelist."""
329
329
  exclude_files = _load_caiignore_patterns(repo_root)
330
330
 
331
+ # `:/` and `top` anchor pathspecs at the repo root, so running from a
332
+ # subdirectory still sees every staged file (the commit includes them all)
333
+ # and `.caiignore` patterns match relative to the root. `files` stay
334
+ # relative to the user's cwd, as typed. A leading `/` is dropped: `top`
335
+ # already anchors at the root, and in long form it would mean an
336
+ # absolute path that silently matches nothing.
331
337
  cmd = ["git", "diff", "--cached", "--"]
332
338
  if files:
333
339
  cmd.extend(files)
334
340
  else:
335
- cmd.append(".")
336
- cmd.extend(f":!{pattern}" for pattern in exclude_files)
341
+ cmd.append(":/")
342
+ cmd.extend(f":(top,exclude){pattern.lstrip('/')}" for pattern in exclude_files)
337
343
 
338
344
  result = run_cmd(cmd, capture_output=True, text=True, check=False)
339
345
  if result.returncode != 0:
@@ -423,13 +429,15 @@ def collect_staged_file_contents(
423
429
  """
424
430
  exclude_patterns = _load_caiignore_patterns(repo_root)
425
431
 
432
+ # Same pathspec rules as git_diff_excluding: whole repo by default,
433
+ # `files` relative to the user's cwd. Output names are root-relative.
426
434
  cmd = ["git", "diff", "--cached", "--name-only", "--"]
427
435
  if files:
428
436
  cmd.extend(files)
429
437
  else:
430
- cmd.append(".")
438
+ cmd.append(":/")
431
439
 
432
- result = run_cmd(cmd, capture_output=True, text=True, check=False, cwd=repo_root)
440
+ result = run_cmd(cmd, capture_output=True, text=True, check=False)
433
441
  if result.returncode != 0:
434
442
  log.error("git diff --name-only failed: %s", result.stderr.strip())
435
443
  return ""
@@ -124,7 +124,7 @@ def _write_tokens_file(provider: str, token: str, tokens_path: Path) -> None:
124
124
 
125
125
  if provider in tokens and not typer.confirm(
126
126
  f"A {provider} token already exists in {tokens_path}. Overwrite?",
127
- default=False,
127
+ default=None,
128
128
  ):
129
129
  typer.echo("Keeping existing token.")
130
130
  return
@@ -178,7 +178,7 @@ def run_init_wizard(
178
178
  try:
179
179
  if config_path.exists() and config_path.stat().st_size > 0:
180
180
  if not typer.confirm(
181
- f"\n{config_path} already exists. Overwrite?", default=False
181
+ f"\n{config_path} already exists. Overwrite?", default=None
182
182
  ):
183
183
  typer.echo("Aborted. Existing configuration left untouched.")
184
184
  return 0
@@ -186,7 +186,7 @@ def run_init_wizard(
186
186
  provider = _pick_provider()
187
187
  language = _pick_language()
188
188
  style = _pick_style()
189
- emoji = typer.confirm("Use emoji in commit messages?", default=True)
189
+ emoji = typer.confirm("Use emoji in commit messages?", default=None)
190
190
 
191
191
  if provider not in TOKENLESS_PROVIDERS:
192
192
  token = _read_token(provider)
@@ -205,6 +205,6 @@ def run_init_wizard(
205
205
  "and run 'git cai'."
206
206
  )
207
207
  return 0
208
- except KeyboardInterrupt:
208
+ except (KeyboardInterrupt, typer.Abort):
209
209
  typer.echo("\nAborted.", err=True)
210
210
  return 130
@@ -104,14 +104,10 @@ class CliManager:
104
104
  print(f"⬆️ Update available: {current_version} → {latest_version}")
105
105
 
106
106
  if not auto_confirm:
107
- choice = (
108
- input(
109
- "Do you want to update now using 'pipx upgrade git-cai-cli'? [yes/no]: "
110
- )
111
- .strip()
112
- .lower()
113
- )
114
- if choice not in ("y", "yes"):
107
+ if not typer.confirm(
108
+ "Do you want to update now using 'pipx upgrade git-cai-cli'?",
109
+ default=None,
110
+ ):
115
111
  print("❌ Update cancelled.")
116
112
  return
117
113
 
@@ -11,6 +11,7 @@ import tempfile
11
11
  import time
12
12
  from pathlib import Path
13
13
 
14
+ import typer
14
15
  from git_cai_cli.core.config import (
15
16
  apply_provider_overrides,
16
17
  load_config,
@@ -140,8 +141,7 @@ def _resolve_squash_target(squash_arg: str) -> str:
140
141
  count,
141
142
  branch_commits,
142
143
  )
143
- choice = input("Continue anyway? [yes/no]: ").strip().lower()
144
- if choice not in ("y", "yes"):
144
+ if not typer.confirm("Continue anyway?", default=None):
145
145
  log.info("Squash cancelled.")
146
146
  sys.exit(0)
147
147
 
@@ -472,8 +472,7 @@ def squash_branch(
472
472
  " git push --force-with-lease\n\n"
473
473
  "This is a safe force-push that prevents overwriting others' commits.\n\n"
474
474
  )
475
- choice = input("Shall I execute it for you now? [yes/no]: ").strip().lower()
476
- if choice in ("y", "yes"):
475
+ if typer.confirm("Shall I execute it for you now?", default=None):
477
476
  push_result = subprocess.run(
478
477
  ["git", "push", "--force-with-lease"],
479
478
  capture_output=True,
@@ -82,7 +82,7 @@ def _route_false_alarm(findings: list, repo_root: Path | None) -> None:
82
82
  err=True,
83
83
  )
84
84
  typer.echo(" 3) skip — send it this once; remember nothing", err=True)
85
- choice = typer.prompt("Choose", default="3").strip()
85
+ choice = typer.prompt("Choose", default="3", err=True).strip()
86
86
 
87
87
  if choice == "1":
88
88
  if repo_root is None:
@@ -384,7 +384,9 @@ def run(
384
384
  )
385
385
  raise typer.Exit(code=1)
386
386
  if not typer.confirm(
387
- "Send this content to the provider anyway?", default=False
387
+ "Send this content to the provider anyway?",
388
+ default=None,
389
+ err=True,
388
390
  ):
389
391
  typer.echo("Aborted. Nothing was sent.", err=True)
390
392
  raise typer.Exit(code=1)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: git-cai-cli
3
- Version: 0.17.0
3
+ Version: 0.17.1
4
4
  Summary: Use LLM to create git commit messages
5
5
  Author-email: Thorsten Foltz <thorsten.foltz@live.com>
6
6
  License-Expression: MIT
@@ -0,0 +1,8 @@
1
+ {
2
+ "tag": "0.17.1",
3
+ "distance": 0,
4
+ "node": "g1fd24936a28a9ef5d38cc65864fc8fa14b130c30",
5
+ "dirty": false,
6
+ "branch": "HEAD",
7
+ "node_date": "2026-10-11"
8
+ }
@@ -68,3 +68,25 @@ def test_git_diff_excluding_integration(temp_git_repo):
68
68
 
69
69
  assert any("file1.txt" in h for h in headers)
70
70
  assert not any("file2.txt" in h for h in headers)
71
+
72
+
73
+ def test_git_diff_excluding_from_subdirectory_sees_whole_repo(temp_git_repo):
74
+ """Run from a subdirectory: every staged file is included (the commit
75
+ takes them all) and root `.caiignore` patterns still match."""
76
+ (temp_git_repo / ".caiignore").write_text("/file2.txt\n")
77
+ sub = temp_git_repo / "sub"
78
+ sub.mkdir()
79
+ (sub / "inner.txt").write_text("inner\n")
80
+ (temp_git_repo / "file1.txt").write_text("changed 1\n")
81
+ (temp_git_repo / "file2.txt").write_text("changed 2\n")
82
+ subprocess.run(["git", "add", "."], cwd=temp_git_repo, check=True)
83
+
84
+ diff = git_diff_excluding(
85
+ repo_root=temp_git_repo,
86
+ run_cmd=lambda *args, **kwargs: subprocess.run(*args, cwd=sub, **kwargs),
87
+ )
88
+ headers = [line for line in diff.splitlines() if line.startswith("diff --git")]
89
+
90
+ assert any("sub/inner.txt" in h for h in headers)
91
+ assert any("file1.txt" in h for h in headers)
92
+ assert not any("file2.txt" in h for h in headers)
@@ -174,10 +174,10 @@ def test_validate_options_valid_combinations():
174
174
 
175
175
  @pytest.mark.parametrize(
176
176
  "bad_mode",
177
- [Mode.LIST, Mode.UPDATE, Mode.SQUASH],
177
+ [Mode.LIST, Mode.UPDATE, Mode.SQUASH, Mode.AMEND],
178
178
  )
179
- def test_validate_options_files_rejected_outside_commit_amend(bad_mode, capsys):
180
- """--files must be rejected in LIST/UPDATE/SQUASH modes."""
179
+ def test_validate_options_files_rejected_outside_commit(bad_mode, capsys):
180
+ """--files must be rejected outside COMMIT (AMEND never reads it)."""
181
181
  with pytest.raises(typer.Exit) as exc:
182
182
  modes.validate_options(
183
183
  mode=bad_mode,
@@ -188,7 +188,7 @@ def test_validate_options_files_rejected_outside_commit_amend(bad_mode, capsys):
188
188
  files=["x.py"],
189
189
  )
190
190
  captured = capsys.readouterr()
191
- assert "--files can only be used in COMMIT or AMEND mode." in captured.err
191
+ assert "--files can only be used in COMMIT mode." in captured.err
192
192
  assert exc.value.exit_code == 1
193
193
 
194
194
 
@@ -202,15 +202,3 @@ def test_validate_options_files_allowed_in_commit_mode():
202
202
  version_flag=False,
203
203
  files=["src/foo.py", "src/bar.py"],
204
204
  )
205
-
206
-
207
- def test_validate_options_files_allowed_in_amend_mode():
208
- """--files is allowed in AMEND mode."""
209
- modes.validate_options(
210
- mode=Mode.AMEND,
211
- stage_tracked=False,
212
- enable_debug=False,
213
- help_flag=False,
214
- version_flag=False,
215
- files=["src/foo.py"],
216
- )
@@ -181,7 +181,7 @@ def test_get_current_branch_not_in_repo():
181
181
 
182
182
  def test_git_diff_excluding_reads_ignore_file_and_excludes_patterns(tmp_path):
183
183
  """
184
- git_diff_excluding() should append :!pattern for each line in .caiignore.
184
+ git_diff_excluding() should append :(top,exclude)pattern for each line in .caiignore.
185
185
  """
186
186
  repo_root = tmp_path
187
187
  ignore_file = repo_root / ".caiignore"
@@ -193,8 +193,8 @@ def test_git_diff_excluding_reads_ignore_file_and_excludes_patterns(tmp_path):
193
193
 
194
194
  def fake_run(cmd, capture_output, text, check):
195
195
  # Ensure ignore patterns were added
196
- assert ":!*.pyc" in cmd
197
- assert ":!build/" in cmd
196
+ assert ":(top,exclude)*.pyc" in cmd
197
+ assert ":(top,exclude)build/" in cmd
198
198
  return mock_proc
199
199
 
200
200
  output = git_diff_excluding(repo_root, run_cmd=fake_run)
@@ -252,7 +252,7 @@ def test_git_diff_excluding_no_caiignore(tmp_path):
252
252
 
253
253
  def fake_run(cmd, **kwargs):
254
254
  # No :! patterns should be in the command
255
- assert not any(arg.startswith(":!") for arg in cmd)
255
+ assert not any(arg.startswith(":(top,exclude)") for arg in cmd)
256
256
  mock_proc = MagicMock()
257
257
  mock_proc.returncode = 0
258
258
  mock_proc.stdout = "diff output"
@@ -270,7 +270,7 @@ def test_git_diff_excluding_empty_caiignore(tmp_path):
270
270
  (repo_root / ".caiignore").write_text("# only comments\n\n")
271
271
 
272
272
  def fake_run(cmd, **kwargs):
273
- assert not any(arg.startswith(":!") for arg in cmd)
273
+ assert not any(arg.startswith(":(top,exclude)") for arg in cmd)
274
274
  mock_proc = MagicMock()
275
275
  mock_proc.returncode = 0
276
276
  mock_proc.stdout = "diff output"
@@ -305,13 +305,13 @@ def test_git_diff_excluding_with_files_filter(tmp_path):
305
305
  assert "a.py" in captured_cmd
306
306
  assert "b.py" in captured_cmd
307
307
  # Excludes still apply
308
- assert ":!*.pyc" in captured_cmd
309
- # The generic `.` pathspec must NOT be present when files are passed
310
- assert "." not in captured_cmd
308
+ assert ":(top,exclude)*.pyc" in captured_cmd
309
+ # The whole-repo `:/` pathspec must NOT be present when files are passed
310
+ assert ":/" not in captured_cmd
311
311
 
312
312
 
313
- def test_git_diff_excluding_without_files_uses_dot(tmp_path):
314
- """When `files` is None, `.` is used as pathspec."""
313
+ def test_git_diff_excluding_without_files_uses_repo_root(tmp_path):
314
+ """When `files` is None, `:/` (whole repo) is used as pathspec."""
315
315
  repo_root = tmp_path
316
316
 
317
317
  captured_cmd = []
@@ -325,8 +325,8 @@ def test_git_diff_excluding_without_files_uses_dot(tmp_path):
325
325
 
326
326
  git_diff_excluding(repo_root, run_cmd=fake_run)
327
327
 
328
- # Make sure it contains `.` as the generic pathspec
329
- assert "." in captured_cmd
328
+ # Whole repo, not just the cwd subtree
329
+ assert ":/" in captured_cmd
330
330
 
331
331
 
332
332
  # ------------------------------------------------------------------------------
@@ -75,7 +75,7 @@ def test_check_and_update_user_declines(capsys) -> None:
75
75
  with (
76
76
  patch("git_cai_cli.core.options.version", return_value="1.0.0"),
77
77
  patch("git_cai_cli.core.options.requests.get", return_value=response),
78
- patch("builtins.input", return_value="no"),
78
+ patch("typer.confirm", return_value=False),
79
79
  ):
80
80
  manager.check_and_update()
81
81
 
@@ -2,7 +2,6 @@
2
2
  Unit tests for git_cai_cli.core.squash.squash_branch function.
3
3
  """
4
4
 
5
- import builtins
6
5
  import subprocess
7
6
  import typing
8
7
  from pathlib import Path
@@ -311,7 +310,7 @@ def test_force_push_prompt_and_execution(
311
310
  patch("git_cai_cli.core.squash.sha256_of_file", side_effect=["a", "b"]),
312
311
  patch("subprocess.run", run_mock),
313
312
  patch("git_cai_cli.core.squash._has_upstream", return_value=True),
314
- patch.object(builtins, "input", return_value="yes"),
313
+ patch("typer.confirm", return_value=True),
315
314
  ):
316
315
  squash_branch()
317
316
 
@@ -352,7 +351,7 @@ def test_force_push_failure_is_handled(
352
351
  patch("git_cai_cli.core.squash.sha256_of_file", side_effect=["a", "b"]),
353
352
  patch("subprocess.run", run_mock),
354
353
  patch("git_cai_cli.core.squash._has_upstream", return_value=True),
355
- patch.object(builtins, "input", return_value="yes"),
354
+ patch("typer.confirm", return_value=True),
356
355
  ):
357
356
  squash_branch()
358
357
 
@@ -399,7 +398,7 @@ def test_resolve_squash_target_number_exceeds_branch_warns() -> None:
399
398
  patch("git_cai_cli.core.squash._count_total_commits", return_value=20),
400
399
  patch("git_cai_cli.core.squash._get_branch_base", return_value="BASE"),
401
400
  patch("git_cai_cli.core.squash._count_commits_on_branch", return_value=3),
402
- patch.object(builtins, "input", return_value="no"),
401
+ patch("typer.confirm", return_value=False),
403
402
  pytest.raises(SystemExit),
404
403
  ):
405
404
  _resolve_squash_target("5")
@@ -411,7 +410,7 @@ def test_resolve_squash_target_number_exceeds_branch_continues() -> None:
411
410
  patch("git_cai_cli.core.squash._count_total_commits", return_value=20),
412
411
  patch("git_cai_cli.core.squash._get_branch_base", return_value="BASE"),
413
412
  patch("git_cai_cli.core.squash._count_commits_on_branch", return_value=3),
414
- patch.object(builtins, "input", return_value="yes"),
413
+ patch("typer.confirm", return_value=True),
415
414
  patch("subprocess.check_output", return_value="def456\n"),
416
415
  ):
417
416
  result = _resolve_squash_target("5")
@@ -1,8 +0,0 @@
1
- {
2
- "tag": "0.17.0",
3
- "distance": 0,
4
- "node": "g457d701c5538634e7963874b8a30f5a7cbeddd9e",
5
- "dirty": false,
6
- "branch": "HEAD",
7
- "node_date": "2026-09-25"
8
- }
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