git-cai-cli 0.11.0__tar.gz → 0.11.2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {git_cai_cli-0.11.0 → git_cai_cli-0.11.2}/.linters/.proselintrc +1 -1
- {git_cai_cli-0.11.0/src/git_cai_cli.egg-info → git_cai_cli-0.11.2}/PKG-INFO +1 -1
- git_cai_cli-0.11.2/cai_config.yml +35 -0
- {git_cai_cli-0.11.0 → git_cai_cli-0.11.2}/src/git_cai_cli/_version.py +3 -3
- {git_cai_cli-0.11.0 → git_cai_cli-0.11.2}/src/git_cai_cli/core/config.py +9 -1
- {git_cai_cli-0.11.0 → git_cai_cli-0.11.2}/src/git_cai_cli/core/llm.py +53 -71
- git_cai_cli-0.11.2/src/git_cai_cli/core/prompts_fallback.py +36 -0
- {git_cai_cli-0.11.0 → git_cai_cli-0.11.2}/src/git_cai_cli/core/squash.py +29 -26
- git_cai_cli-0.11.2/src/git_cai_cli/defaults/commit_prompt.md +12 -0
- git_cai_cli-0.11.2/src/git_cai_cli/defaults/squash_prompt.md +10 -0
- {git_cai_cli-0.11.0 → git_cai_cli-0.11.2/src/git_cai_cli.egg-info}/PKG-INFO +1 -1
- {git_cai_cli-0.11.0 → git_cai_cli-0.11.2}/src/git_cai_cli.egg-info/SOURCES.txt +1 -0
- {git_cai_cli-0.11.0 → git_cai_cli-0.11.2}/tests/unit/test_gitutils.py +1 -1
- {git_cai_cli-0.11.0 → git_cai_cli-0.11.2}/tests/unit/test_llm.py +3 -2
- {git_cai_cli-0.11.0 → git_cai_cli-0.11.2}/tests/unit/test_squash.py +4 -0
- {git_cai_cli-0.11.0 → git_cai_cli-0.11.2}/uv.lock +247 -231
- git_cai_cli-0.11.0/src/git_cai_cli/core/prompts_fallback.py +0 -26
- git_cai_cli-0.11.0/src/git_cai_cli/defaults/commit_prompt.md +0 -6
- git_cai_cli-0.11.0/src/git_cai_cli/defaults/squash_prompt.md +0 -9
- {git_cai_cli-0.11.0 → git_cai_cli-0.11.2}/.caiignore +0 -0
- {git_cai_cli-0.11.0 → git_cai_cli-0.11.2}/.gitattributes +0 -0
- {git_cai_cli-0.11.0 → git_cai_cli-0.11.2}/.github/cd/.SRCINFO +0 -0
- {git_cai_cli-0.11.0 → git_cai_cli-0.11.2}/.github/cd/PKGBUILD +0 -0
- {git_cai_cli-0.11.0 → git_cai_cli-0.11.2}/.github/ci/_version.py +0 -0
- {git_cai_cli-0.11.0 → git_cai_cli-0.11.2}/.github/ci/cai_config.ci.yml +0 -0
- {git_cai_cli-0.11.0 → git_cai_cli-0.11.2}/.github/ci/tokens.ci.yml +0 -0
- {git_cai_cli-0.11.0 → git_cai_cli-0.11.2}/.github/workflows/python-tests.yml +0 -0
- {git_cai_cli-0.11.0 → git_cai_cli-0.11.2}/.github/workflows/release.yml +0 -0
- {git_cai_cli-0.11.0 → git_cai_cli-0.11.2}/.github/workflows/release_aur.yml +0 -0
- {git_cai_cli-0.11.0 → git_cai_cli-0.11.2}/.gitignore +0 -0
- {git_cai_cli-0.11.0 → git_cai_cli-0.11.2}/.linters/.bandit.yml +0 -0
- {git_cai_cli-0.11.0 → git_cai_cli-0.11.2}/.linters/.checkov.yml +0 -0
- {git_cai_cli-0.11.0 → git_cai_cli-0.11.2}/.linters/.flake8 +0 -0
- {git_cai_cli-0.11.0 → git_cai_cli-0.11.2}/.linters/.ls-lint.yml +0 -0
- {git_cai_cli-0.11.0 → git_cai_cli-0.11.2}/.linters/.markdown-link-check.json +0 -0
- {git_cai_cli-0.11.0 → git_cai_cli-0.11.2}/.linters/.markdownlint.json +0 -0
- {git_cai_cli-0.11.0 → git_cai_cli-0.11.2}/.linters/.pylintrc +0 -0
- {git_cai_cli-0.11.0 → git_cai_cli-0.11.2}/.linters/.yamllint.yml +0 -0
- {git_cai_cli-0.11.0 → git_cai_cli-0.11.2}/.linters/check_git_branch_name.sh +0 -0
- {git_cai_cli-0.11.0 → git_cai_cli-0.11.2}/.linters/lychee.toml +0 -0
- {git_cai_cli-0.11.0 → git_cai_cli-0.11.2}/.linters/pyrightconfig.json +0 -0
- {git_cai_cli-0.11.0 → git_cai_cli-0.11.2}/.markdownlintignore +0 -0
- {git_cai_cli-0.11.0 → git_cai_cli-0.11.2}/.mega-linter.yml +0 -0
- {git_cai_cli-0.11.0 → git_cai_cli-0.11.2}/.semgrepignore +0 -0
- {git_cai_cli-0.11.0 → git_cai_cli-0.11.2}/.trivyignore +0 -0
- {git_cai_cli-0.11.0 → git_cai_cli-0.11.2}/CLAUDE.md +0 -0
- {git_cai_cli-0.11.0 → git_cai_cli-0.11.2}/LICENSE +0 -0
- {git_cai_cli-0.11.0 → git_cai_cli-0.11.2}/Makefile +0 -0
- {git_cai_cli-0.11.0 → git_cai_cli-0.11.2}/README.md +0 -0
- {git_cai_cli-0.11.0 → git_cai_cli-0.11.2}/docs/git-cai.txt +0 -0
- {git_cai_cli-0.11.0 → git_cai_cli-0.11.2}/docs/man/git-cai.1 +0 -0
- {git_cai_cli-0.11.0 → git_cai_cli-0.11.2}/pyproject.toml +0 -0
- {git_cai_cli-0.11.0 → git_cai_cli-0.11.2}/setup.cfg +0 -0
- {git_cai_cli-0.11.0 → git_cai_cli-0.11.2}/src/git_cai_cli/__init__.py +0 -0
- {git_cai_cli-0.11.0 → git_cai_cli-0.11.2}/src/git_cai_cli/cli/__init__.py +0 -0
- {git_cai_cli-0.11.0 → git_cai_cli-0.11.2}/src/git_cai_cli/cli/cli.py +0 -0
- {git_cai_cli-0.11.0 → git_cai_cli-0.11.2}/src/git_cai_cli/cli/helptext.py +0 -0
- {git_cai_cli-0.11.0 → git_cai_cli-0.11.2}/src/git_cai_cli/cli/modes.py +0 -0
- {git_cai_cli-0.11.0 → git_cai_cli-0.11.2}/src/git_cai_cli/core/__init__.py +0 -0
- {git_cai_cli-0.11.0 → git_cai_cli-0.11.2}/src/git_cai_cli/core/completion.py +0 -0
- {git_cai_cli-0.11.0 → git_cai_cli-0.11.2}/src/git_cai_cli/core/editors.py +0 -0
- {git_cai_cli-0.11.0 → git_cai_cli-0.11.2}/src/git_cai_cli/core/gitutils.py +0 -0
- {git_cai_cli-0.11.0 → git_cai_cli-0.11.2}/src/git_cai_cli/core/languages.py +0 -0
- {git_cai_cli-0.11.0 → git_cai_cli-0.11.2}/src/git_cai_cli/core/options.py +0 -0
- {git_cai_cli-0.11.0 → git_cai_cli-0.11.2}/src/git_cai_cli/core/spinner.py +0 -0
- {git_cai_cli-0.11.0 → git_cai_cli-0.11.2}/src/git_cai_cli/core/validate.py +0 -0
- {git_cai_cli-0.11.0 → git_cai_cli-0.11.2}/src/git_cai_cli/defaults/__init__.py +0 -0
- {git_cai_cli-0.11.0 → git_cai_cli-0.11.2}/src/git_cai_cli/main.py +0 -0
- {git_cai_cli-0.11.0 → git_cai_cli-0.11.2}/src/git_cai_cli.egg-info/dependency_links.txt +0 -0
- {git_cai_cli-0.11.0 → git_cai_cli-0.11.2}/src/git_cai_cli.egg-info/entry_points.txt +0 -0
- {git_cai_cli-0.11.0 → git_cai_cli-0.11.2}/src/git_cai_cli.egg-info/requires.txt +0 -0
- {git_cai_cli-0.11.0 → git_cai_cli-0.11.2}/src/git_cai_cli.egg-info/top_level.txt +0 -0
- {git_cai_cli-0.11.0 → git_cai_cli-0.11.2}/tests/conftest.py +0 -0
- {git_cai_cli-0.11.0 → git_cai_cli-0.11.2}/tests/integration/test_cli_integration.py +0 -0
- {git_cai_cli-0.11.0 → git_cai_cli-0.11.2}/tests/integration/test_config_integration.py +0 -0
- {git_cai_cli-0.11.0 → git_cai_cli-0.11.2}/tests/integration/test_gitutils_integration.py +0 -0
- {git_cai_cli-0.11.0 → git_cai_cli-0.11.2}/tests/integration/test_modes_integration.py +0 -0
- {git_cai_cli-0.11.0 → git_cai_cli-0.11.2}/tests/integration/test_options_integration.py +0 -0
- {git_cai_cli-0.11.0 → git_cai_cli-0.11.2}/tests/integration/test_squash_integration.py +0 -0
- {git_cai_cli-0.11.0 → git_cai_cli-0.11.2}/tests/unit/test_amend.py +0 -0
- {git_cai_cli-0.11.0 → git_cai_cli-0.11.2}/tests/unit/test_branch_context.py +0 -0
- {git_cai_cli-0.11.0 → git_cai_cli-0.11.2}/tests/unit/test_cli.py +0 -0
- {git_cai_cli-0.11.0 → git_cai_cli-0.11.2}/tests/unit/test_completion.py +0 -0
- {git_cai_cli-0.11.0 → git_cai_cli-0.11.2}/tests/unit/test_config.py +0 -0
- {git_cai_cli-0.11.0 → git_cai_cli-0.11.2}/tests/unit/test_conventional.py +0 -0
- {git_cai_cli-0.11.0 → git_cai_cli-0.11.2}/tests/unit/test_helptext.py +0 -0
- {git_cai_cli-0.11.0 → git_cai_cli-0.11.2}/tests/unit/test_main.py +0 -0
- {git_cai_cli-0.11.0 → git_cai_cli-0.11.2}/tests/unit/test_modes.py +0 -0
- {git_cai_cli-0.11.0 → git_cai_cli-0.11.2}/tests/unit/test_options.py +0 -0
- {git_cai_cli-0.11.0 → git_cai_cli-0.11.2}/tests/unit/test_prompt_loading.py +0 -0
- {git_cai_cli-0.11.0 → git_cai_cli-0.11.2}/tests/unit/test_set_config.py +0 -0
- {git_cai_cli-0.11.0 → git_cai_cli-0.11.2}/tests/unit/test_spinner.py +0 -0
- {git_cai_cli-0.11.0 → git_cai_cli-0.11.2}/tests/unit/test_validate.py +0 -0
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
default: anthropic
|
|
2
|
+
language: en
|
|
3
|
+
style: professional
|
|
4
|
+
emoji: true
|
|
5
|
+
conventional: false
|
|
6
|
+
branch_context: false
|
|
7
|
+
load_tokens_from: /home/thorsten/.config/cai/tokens.yml
|
|
8
|
+
prompt_file: ""
|
|
9
|
+
squash_prompt_file: ""
|
|
10
|
+
token_logging: true
|
|
11
|
+
measure_time: true
|
|
12
|
+
anthropic:
|
|
13
|
+
model: claude-opus-4-6
|
|
14
|
+
temperature: 0
|
|
15
|
+
deepseek:
|
|
16
|
+
model: deepseek-chat
|
|
17
|
+
temperature: 0
|
|
18
|
+
gemini:
|
|
19
|
+
model: gemini-2.5-flash
|
|
20
|
+
temperature: 0
|
|
21
|
+
groq:
|
|
22
|
+
model: moonshotai/kimi-k2-instruct
|
|
23
|
+
temperature: 0
|
|
24
|
+
mistral:
|
|
25
|
+
model: codestral-2508
|
|
26
|
+
temperature: 0
|
|
27
|
+
ollama:
|
|
28
|
+
model: llama3.1
|
|
29
|
+
temperature: 0
|
|
30
|
+
openai:
|
|
31
|
+
model: gpt-5.2
|
|
32
|
+
temperature: 0
|
|
33
|
+
xai:
|
|
34
|
+
model: grok-4-1-fast-reasoning
|
|
35
|
+
temperature: 0
|
|
@@ -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.11.
|
|
22
|
-
__version_tuple__ = version_tuple = (0, 11,
|
|
21
|
+
__version__ = version = '0.11.2'
|
|
22
|
+
__version_tuple__ = version_tuple = (0, 11, 2)
|
|
23
23
|
|
|
24
|
-
__commit_id__ = commit_id = '
|
|
24
|
+
__commit_id__ = commit_id = 'g24b24b5ca'
|
|
@@ -268,7 +268,15 @@ def load_config(
|
|
|
268
268
|
|
|
269
269
|
try:
|
|
270
270
|
with fallback_config_file.open("r", encoding="utf-8") as f:
|
|
271
|
-
|
|
271
|
+
raw = yaml.safe_load(f)
|
|
272
|
+
if not isinstance(raw, dict):
|
|
273
|
+
log.warning(
|
|
274
|
+
"Home config %s is not a YAML mapping, using defaults",
|
|
275
|
+
fallback_config_file,
|
|
276
|
+
)
|
|
277
|
+
config = default_config
|
|
278
|
+
else:
|
|
279
|
+
config = raw or default_config
|
|
272
280
|
log.debug("Home config loaded successfully")
|
|
273
281
|
except yaml.YAMLError:
|
|
274
282
|
log.error("Failed to parse home config %s", fallback_config_file)
|
|
@@ -164,7 +164,10 @@ class CommitMessageGenerator:
|
|
|
164
164
|
|
|
165
165
|
content = git_diff
|
|
166
166
|
if context:
|
|
167
|
-
content =
|
|
167
|
+
content = (
|
|
168
|
+
f"{git_diff}\n\n"
|
|
169
|
+
f"--- Additional context from the author ---\n{context}"
|
|
170
|
+
)
|
|
168
171
|
|
|
169
172
|
return self._dispatch_generate(content=content, system_prompt=prompt)
|
|
170
173
|
|
|
@@ -192,8 +195,8 @@ class CommitMessageGenerator:
|
|
|
192
195
|
|
|
193
196
|
if emoji_value:
|
|
194
197
|
emoji_instruction = (
|
|
195
|
-
"Use relevant emojis
|
|
196
|
-
"
|
|
198
|
+
"Use relevant emojis at the start of the headline and in bullet points "
|
|
199
|
+
"where they add clarity. Keep emojis purposeful — one per bullet at most."
|
|
197
200
|
)
|
|
198
201
|
log.info("Emojis are enabled for commit messages.")
|
|
199
202
|
else:
|
|
@@ -236,7 +239,7 @@ class CommitMessageGenerator:
|
|
|
236
239
|
log.info("Style setting is 'none' — no style instruction added to prompt.")
|
|
237
240
|
return ""
|
|
238
241
|
|
|
239
|
-
return f"Write the commit message in the following tone style: {style}."
|
|
242
|
+
return f"Write the commit message in the following tone style: {style}. Apply this tone to both the headline and the bullet points."
|
|
240
243
|
|
|
241
244
|
def _conventional_instruction(self) -> str:
|
|
242
245
|
"""
|
|
@@ -248,12 +251,12 @@ class CommitMessageGenerator:
|
|
|
248
251
|
log.info("Conventional Commits format enabled.")
|
|
249
252
|
return (
|
|
250
253
|
"Follow the Conventional Commits specification. "
|
|
251
|
-
"The
|
|
254
|
+
"The headline MUST be structured as: <type>(<optional scope>): <description>. "
|
|
252
255
|
"Allowed types: feat, fix, docs, style, refactor, perf, test, build, ci, chore, revert. "
|
|
253
256
|
"The scope is optional and describes the section of the codebase affected. "
|
|
254
257
|
"Use a '!' after the type/scope for breaking changes (e.g., 'feat!: ...' or 'feat(api)!: ...'). "
|
|
255
|
-
"The description must be a concise summary in imperative mood. "
|
|
256
|
-
"Additional details
|
|
258
|
+
"The description must be a concise summary in imperative mood, lowercase, no trailing period. "
|
|
259
|
+
"Additional details follow as bullet points in the body after a blank line."
|
|
257
260
|
)
|
|
258
261
|
|
|
259
262
|
def _branch_instruction(self) -> str:
|
|
@@ -270,8 +273,8 @@ class CommitMessageGenerator:
|
|
|
270
273
|
log.info("Branch context enabled: '%s'.", branch_name)
|
|
271
274
|
return (
|
|
272
275
|
f"The current Git branch is '{branch_name}'. "
|
|
273
|
-
"Use the branch name as additional context to
|
|
274
|
-
"
|
|
276
|
+
"Use the branch name as additional context to infer the intent "
|
|
277
|
+
"and scope of the changes — but do not include the branch name in the message."
|
|
275
278
|
)
|
|
276
279
|
|
|
277
280
|
def _config_instructions(self) -> str:
|
|
@@ -422,15 +425,12 @@ class CommitMessageGenerator:
|
|
|
422
425
|
|
|
423
426
|
data = response.json()
|
|
424
427
|
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
)
|
|
432
|
-
except (KeyError, TypeError, AttributeError):
|
|
433
|
-
self._log_token_usage("anthropic", None, None)
|
|
428
|
+
usage = data.get("usage") or {}
|
|
429
|
+
self._log_token_usage(
|
|
430
|
+
"anthropic",
|
|
431
|
+
usage.get("input_tokens"),
|
|
432
|
+
usage.get("output_tokens"),
|
|
433
|
+
)
|
|
434
434
|
|
|
435
435
|
return data["content"][0]["text"].strip()
|
|
436
436
|
|
|
@@ -492,15 +492,12 @@ class CommitMessageGenerator:
|
|
|
492
492
|
|
|
493
493
|
data = response.json()
|
|
494
494
|
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
)
|
|
502
|
-
except (KeyError, TypeError, AttributeError):
|
|
503
|
-
self._log_token_usage("gemini", None, None)
|
|
495
|
+
usage = data.get("usageMetadata") or {}
|
|
496
|
+
self._log_token_usage(
|
|
497
|
+
"gemini",
|
|
498
|
+
usage.get("promptTokenCount"),
|
|
499
|
+
usage.get("candidatesTokenCount"),
|
|
500
|
+
)
|
|
504
501
|
|
|
505
502
|
return data["candidates"][0]["content"]["parts"][0]["text"].strip()
|
|
506
503
|
|
|
@@ -553,15 +550,12 @@ class CommitMessageGenerator:
|
|
|
553
550
|
|
|
554
551
|
data = response.json()
|
|
555
552
|
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
)
|
|
563
|
-
except (KeyError, TypeError, AttributeError):
|
|
564
|
-
self._log_token_usage("groq", None, None)
|
|
553
|
+
usage = data.get("usage") or {}
|
|
554
|
+
self._log_token_usage(
|
|
555
|
+
"groq",
|
|
556
|
+
usage.get("prompt_tokens"),
|
|
557
|
+
usage.get("completion_tokens"),
|
|
558
|
+
)
|
|
565
559
|
|
|
566
560
|
return data["choices"][0]["message"]["content"].strip()
|
|
567
561
|
|
|
@@ -612,15 +606,12 @@ class CommitMessageGenerator:
|
|
|
612
606
|
|
|
613
607
|
data = response.json()
|
|
614
608
|
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
)
|
|
622
|
-
except (KeyError, TypeError, AttributeError):
|
|
623
|
-
self._log_token_usage("mistral", None, None)
|
|
609
|
+
usage = data.get("usage") or {}
|
|
610
|
+
self._log_token_usage(
|
|
611
|
+
"mistral",
|
|
612
|
+
usage.get("prompt_tokens"),
|
|
613
|
+
usage.get("completion_tokens"),
|
|
614
|
+
)
|
|
624
615
|
|
|
625
616
|
return data["choices"][0]["message"]["content"].strip()
|
|
626
617
|
|
|
@@ -780,14 +771,11 @@ class CommitMessageGenerator:
|
|
|
780
771
|
data = response.json()
|
|
781
772
|
|
|
782
773
|
# Extract token usage from Ollama response
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
)
|
|
789
|
-
except (KeyError, TypeError, AttributeError):
|
|
790
|
-
self._log_token_usage("ollama", None, None)
|
|
774
|
+
self._log_token_usage(
|
|
775
|
+
"ollama",
|
|
776
|
+
data.get("prompt_eval_count") if isinstance(data, dict) else None,
|
|
777
|
+
data.get("eval_count") if isinstance(data, dict) else None,
|
|
778
|
+
)
|
|
791
779
|
|
|
792
780
|
# /api/chat format
|
|
793
781
|
if isinstance(data, dict) and isinstance(data.get("message"), dict):
|
|
@@ -847,15 +835,12 @@ class CommitMessageGenerator:
|
|
|
847
835
|
stream=False,
|
|
848
836
|
)
|
|
849
837
|
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
)
|
|
857
|
-
except (KeyError, TypeError, AttributeError):
|
|
858
|
-
self._log_token_usage(provider_name, None, None)
|
|
838
|
+
usage = completion.usage
|
|
839
|
+
self._log_token_usage(
|
|
840
|
+
provider_name,
|
|
841
|
+
getattr(usage, "prompt_tokens", None),
|
|
842
|
+
getattr(usage, "completion_tokens", None),
|
|
843
|
+
)
|
|
859
844
|
|
|
860
845
|
return completion.choices[0].message.content.strip()
|
|
861
846
|
|
|
@@ -905,15 +890,12 @@ class CommitMessageGenerator:
|
|
|
905
890
|
|
|
906
891
|
data = response.json()
|
|
907
892
|
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
)
|
|
915
|
-
except (KeyError, TypeError, AttributeError):
|
|
916
|
-
self._log_token_usage("xai", None, None)
|
|
893
|
+
usage = data.get("usage") or {}
|
|
894
|
+
self._log_token_usage(
|
|
895
|
+
"xai",
|
|
896
|
+
usage.get("prompt_tokens"),
|
|
897
|
+
usage.get("completion_tokens"),
|
|
898
|
+
)
|
|
917
899
|
|
|
918
900
|
return data["choices"][0]["message"]["content"].strip()
|
|
919
901
|
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Hardcoded fallback prompts used as a last resort when no prompt file is found.
|
|
3
|
+
|
|
4
|
+
This module exists to break the circular dependency between config.py and llm.py.
|
|
5
|
+
Both modules can safely import from here without creating import cycles.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
HARDCODED_COMMIT_PROMPT = (
|
|
9
|
+
"You are an expert software engineer assistant. "
|
|
10
|
+
"Your task is to generate a concise, professional git commit message "
|
|
11
|
+
"from the provided git diff.\n"
|
|
12
|
+
"\n"
|
|
13
|
+
"Rules:\n"
|
|
14
|
+
'- Write a single headline in imperative mood (e.g. "Add", "Fix", "Refactor"), max 50 characters.\n'
|
|
15
|
+
"- Below the headline, leave one blank line, then add a bullet-point list of the most important changes.\n"
|
|
16
|
+
"- Each bullet should explain *what* changed and *why*, not repeat filenames or obvious details.\n"
|
|
17
|
+
"- Group related changes into one bullet instead of listing every file separately.\n"
|
|
18
|
+
"- Keep the total message short — aim for clarity over completeness.\n"
|
|
19
|
+
"- Output only the raw commit message. No markdown fences, no quotes, no extra commentary.\n"
|
|
20
|
+
"- If you detect sensitive information (keys, tokens, passwords), "
|
|
21
|
+
"warn about it at the very top before the headline."
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
HARDCODED_SQUASH_PROMPT = (
|
|
25
|
+
"You are an expert software engineer assistant. "
|
|
26
|
+
"Your task is to summarize multiple existing commit messages "
|
|
27
|
+
"into a single, coherent git commit message that captures the overall intent.\n"
|
|
28
|
+
"\n"
|
|
29
|
+
"Rules:\n"
|
|
30
|
+
"- Do not list or echo each original commit. Synthesize them into a unified narrative.\n"
|
|
31
|
+
"- Write one clear headline in imperative mood (max 50 characters) that captures the main purpose.\n"
|
|
32
|
+
"- Below the headline, leave one blank line, then add a concise bullet list "
|
|
33
|
+
"describing the key themes of the work.\n"
|
|
34
|
+
"- Focus on *why* the changes were made, not just *what* was touched.\n"
|
|
35
|
+
"- Output only the raw commit message. No markdown fences, no quotes, no extra commentary."
|
|
36
|
+
)
|
|
@@ -251,7 +251,10 @@ def squash_branch(
|
|
|
251
251
|
|
|
252
252
|
result = commit_with_edit_template(msg)
|
|
253
253
|
if result != 0:
|
|
254
|
-
log.info(
|
|
254
|
+
log.info(
|
|
255
|
+
"Commit aborted — squash cancelled. "
|
|
256
|
+
"Note: staged changes were already committed."
|
|
257
|
+
)
|
|
255
258
|
return
|
|
256
259
|
|
|
257
260
|
else:
|
|
@@ -314,35 +317,35 @@ def squash_branch(
|
|
|
314
317
|
tf.flush()
|
|
315
318
|
tf_name = Path(tf.name)
|
|
316
319
|
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
320
|
+
try:
|
|
321
|
+
original_hash = sha256_of_file(tf_name)
|
|
322
|
+
|
|
323
|
+
editor = get_git_editor()
|
|
324
|
+
parts = shlex.split(editor)
|
|
325
|
+
if not shutil.which(parts[0]):
|
|
326
|
+
# This editor command requires shell interpretation
|
|
327
|
+
rc = subprocess.run(
|
|
328
|
+
f'{editor} "{tf_name}"',
|
|
329
|
+
shell=True,
|
|
330
|
+
check=False, # nosemgrep
|
|
331
|
+
).returncode # nosec # nosemgrep
|
|
332
|
+
else:
|
|
333
|
+
rc = subprocess.run(parts + [str(tf_name)], check=False).returncode
|
|
334
|
+
|
|
335
|
+
if rc != 0:
|
|
336
|
+
log.info("Editor exited non-zero — squash cancelled.")
|
|
337
|
+
return
|
|
330
338
|
|
|
331
|
-
|
|
332
|
-
log.info("Editor exited non-zero — squash cancelled.")
|
|
333
|
-
tf_name.unlink(missing_ok=True)
|
|
334
|
-
return
|
|
339
|
+
new_hash = sha256_of_file(tf_name)
|
|
335
340
|
|
|
336
|
-
|
|
341
|
+
if new_hash == original_hash:
|
|
342
|
+
log.info("Squash cancelled (user did not save message).")
|
|
343
|
+
return
|
|
337
344
|
|
|
338
|
-
|
|
339
|
-
|
|
345
|
+
# User saved → read message into final_message
|
|
346
|
+
final_message = tf_name.read_text(encoding="utf-8").strip()
|
|
347
|
+
finally:
|
|
340
348
|
tf_name.unlink(missing_ok=True)
|
|
341
|
-
return
|
|
342
|
-
|
|
343
|
-
# User saved → read message into final_message
|
|
344
|
-
final_message = tf_name.read_text(encoding="utf-8").strip()
|
|
345
|
-
tf_name.unlink(missing_ok=True)
|
|
346
349
|
|
|
347
350
|
# 5) Perform squash
|
|
348
351
|
subprocess.run(["git", "reset", "--soft", merge_base], check=True)
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
You are an expert software engineer assistant.
|
|
2
|
+
Your task is to generate a concise, professional git commit message
|
|
3
|
+
from the provided git diff.
|
|
4
|
+
|
|
5
|
+
Rules:
|
|
6
|
+
- Write a single headline in imperative mood (e.g. "Add", "Fix", "Refactor"), max 50 characters.
|
|
7
|
+
- Below the headline, leave one blank line, then add a bullet-point list of the most important changes.
|
|
8
|
+
- Each bullet should explain *what* changed and *why*, not repeat filenames or obvious details.
|
|
9
|
+
- Group related changes into one bullet instead of listing every file separately.
|
|
10
|
+
- Keep the total message short — aim for clarity over completeness.
|
|
11
|
+
- Output only the raw commit message. No markdown fences, no quotes, no extra commentary.
|
|
12
|
+
- If you detect sensitive information (keys, tokens, passwords), warn about it at the very top before the headline.
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
You are an expert software engineer assistant.
|
|
2
|
+
Your task is to summarize multiple existing commit messages
|
|
3
|
+
into a single, coherent git commit message that captures the overall intent.
|
|
4
|
+
|
|
5
|
+
Rules:
|
|
6
|
+
- Do not list or echo each original commit. Synthesize them into a unified narrative.
|
|
7
|
+
- Write one clear headline in imperative mood (max 50 characters) that captures the main purpose.
|
|
8
|
+
- Below the headline, leave one blank line, then add a concise bullet list describing the key themes of the work.
|
|
9
|
+
- Focus on *why* the changes were made, not just *what* was touched.
|
|
10
|
+
- Output only the raw commit message. No markdown fences, no quotes, no extra commentary.
|
|
@@ -219,7 +219,7 @@ def test_get_git_editor_falls_back_to_env():
|
|
|
219
219
|
get_git_editor() should fall back to environment variables if git var fails.
|
|
220
220
|
"""
|
|
221
221
|
with patch("subprocess.run", side_effect=subprocess.CalledProcessError(1, "cmd")):
|
|
222
|
-
with patch.dict(os.environ, {"EDITOR": "nano"}):
|
|
222
|
+
with patch.dict(os.environ, {"EDITOR": "nano"}, clear=True):
|
|
223
223
|
assert get_git_editor() == "nano"
|
|
224
224
|
|
|
225
225
|
|
|
@@ -60,7 +60,8 @@ def test_emoji_enabled(generator):
|
|
|
60
60
|
Test that the _emoji_instruction method returns the correct string when emojis are enabled
|
|
61
61
|
"""
|
|
62
62
|
assert (
|
|
63
|
-
"Use relevant emojis
|
|
63
|
+
"Use relevant emojis at the start of the headline and in bullet points "
|
|
64
|
+
"where they add clarity. Keep emojis purposeful — one per bullet at most."
|
|
64
65
|
in generator._emoji_instruction()
|
|
65
66
|
)
|
|
66
67
|
|
|
@@ -937,7 +938,7 @@ def test_generate_appends_context_to_diff(generator):
|
|
|
937
938
|
call_args = mock.call_args
|
|
938
939
|
content = call_args[1]["content"] if "content" in call_args[1] else call_args[0][0]
|
|
939
940
|
assert "diff output" in content
|
|
940
|
-
assert "Additional context
|
|
941
|
+
assert "Additional context from the author" in content
|
|
941
942
|
assert "Fixes JIRA-1234" in content
|
|
942
943
|
|
|
943
944
|
|
|
@@ -84,6 +84,10 @@ def test_aborts_on_unstaged_changes(mock_repo_root) -> None:
|
|
|
84
84
|
"file.py", # unstaged
|
|
85
85
|
],
|
|
86
86
|
),
|
|
87
|
+
patch(
|
|
88
|
+
"git_cai_cli.core.squash.load_config", return_value={"default": "openai"}
|
|
89
|
+
),
|
|
90
|
+
patch("git_cai_cli.core.squash.load_token", return_value="token"),
|
|
87
91
|
):
|
|
88
92
|
squash_branch()
|
|
89
93
|
|