git-copilot-commit 0.5.3__py3-none-any.whl → 0.5.5__py3-none-any.whl
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_copilot_commit/cli.py +91 -10
- git_copilot_commit/prompts/split-commit-planner-prompt.md +5 -0
- git_copilot_commit/split_commits.py +9 -3
- {git_copilot_commit-0.5.3.dist-info → git_copilot_commit-0.5.5.dist-info}/METADATA +1 -1
- {git_copilot_commit-0.5.3.dist-info → git_copilot_commit-0.5.5.dist-info}/RECORD +8 -8
- {git_copilot_commit-0.5.3.dist-info → git_copilot_commit-0.5.5.dist-info}/WHEEL +0 -0
- {git_copilot_commit-0.5.3.dist-info → git_copilot_commit-0.5.5.dist-info}/entry_points.txt +0 -0
- {git_copilot_commit-0.5.3.dist-info → git_copilot_commit-0.5.5.dist-info}/licenses/LICENSE +0 -0
git_copilot_commit/cli.py
CHANGED
|
@@ -5,6 +5,7 @@ git-copilot-commit - AI-powered Git commit assistant
|
|
|
5
5
|
from dataclasses import dataclass
|
|
6
6
|
from pathlib import Path
|
|
7
7
|
import os
|
|
8
|
+
import re
|
|
8
9
|
import sys
|
|
9
10
|
from typing import Annotated, Sequence
|
|
10
11
|
|
|
@@ -94,6 +95,19 @@ class PreparedSplitCommit:
|
|
|
94
95
|
patch_units: tuple[PatchUnit, ...]
|
|
95
96
|
|
|
96
97
|
|
|
98
|
+
CORE_CHANGE_COMMIT_TYPES = frozenset({"feat", "fix", "perf", "refactor", "revert"})
|
|
99
|
+
FOLLOW_UP_COMMIT_TYPE_PRIORITY = {
|
|
100
|
+
"test": 2,
|
|
101
|
+
"docs": 3,
|
|
102
|
+
"style": 4,
|
|
103
|
+
"chore": 4,
|
|
104
|
+
}
|
|
105
|
+
CONVENTIONAL_COMMIT_TYPE_PATTERN = re.compile(
|
|
106
|
+
r"^\s*([a-z]+)(?:\([^)\r\n]*\))?(?:!)?:",
|
|
107
|
+
re.IGNORECASE,
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
|
|
97
111
|
def preprocess_cli_args(args: Sequence[str]) -> list[str]:
|
|
98
112
|
"""Normalize CLI arguments before Click parses them."""
|
|
99
113
|
processed_args: list[str] = []
|
|
@@ -142,6 +156,37 @@ def preprocess_cli_args(args: Sequence[str]) -> list[str]:
|
|
|
142
156
|
return processed_args
|
|
143
157
|
|
|
144
158
|
|
|
159
|
+
def extract_conventional_commit_type(message: str) -> str | None:
|
|
160
|
+
"""Extract the Conventional Commit type from a generated title line."""
|
|
161
|
+
match = CONVENTIONAL_COMMIT_TYPE_PATTERN.match(message.strip())
|
|
162
|
+
if match is None:
|
|
163
|
+
return None
|
|
164
|
+
|
|
165
|
+
return match.group(1).lower()
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def order_prepared_split_commits(
|
|
169
|
+
prepared_commits: Sequence[PreparedSplitCommit],
|
|
170
|
+
) -> list[PreparedSplitCommit]:
|
|
171
|
+
"""Order planned commits in a developer-friendly execution sequence."""
|
|
172
|
+
|
|
173
|
+
def sort_key(item: tuple[int, PreparedSplitCommit]) -> tuple[int, int]:
|
|
174
|
+
index, prepared_commit = item
|
|
175
|
+
commit_type = extract_conventional_commit_type(prepared_commit.message)
|
|
176
|
+
|
|
177
|
+
if commit_type in CORE_CHANGE_COMMIT_TYPES:
|
|
178
|
+
priority = 0
|
|
179
|
+
elif commit_type is None:
|
|
180
|
+
priority = 1
|
|
181
|
+
else:
|
|
182
|
+
priority = FOLLOW_UP_COMMIT_TYPE_PRIORITY.get(commit_type, 1)
|
|
183
|
+
|
|
184
|
+
return priority, index
|
|
185
|
+
|
|
186
|
+
ordered_items = sorted(enumerate(prepared_commits), key=sort_key)
|
|
187
|
+
return [prepared_commit for _, prepared_commit in ordered_items]
|
|
188
|
+
|
|
189
|
+
|
|
145
190
|
def run(args: Sequence[str] | None = None) -> None:
|
|
146
191
|
"""Run the CLI entrypoint with argument normalization."""
|
|
147
192
|
raw_args = list(args) if args is not None else sys.argv[1:]
|
|
@@ -341,7 +386,7 @@ def generate_commit_message_for_prompt(
|
|
|
341
386
|
)
|
|
342
387
|
|
|
343
388
|
|
|
344
|
-
def
|
|
389
|
+
def should_retry_with_compact_prompt(exc: github_copilot.CopilotError) -> bool:
|
|
345
390
|
message_parts = [str(exc)]
|
|
346
391
|
if isinstance(exc, github_copilot.CopilotHttpError) and exc.detail:
|
|
347
392
|
message_parts.append(exc.detail)
|
|
@@ -360,6 +405,7 @@ def should_fallback_to_status_only(exc: github_copilot.CopilotError) -> bool:
|
|
|
360
405
|
"max prompt tokens",
|
|
361
406
|
"input tokens",
|
|
362
407
|
"prompt tokens",
|
|
408
|
+
"prompt token count",
|
|
363
409
|
)
|
|
364
410
|
return any(indicator in haystack for indicator in indicators)
|
|
365
411
|
|
|
@@ -379,7 +425,7 @@ def generate_commit_message_for_status(
|
|
|
379
425
|
http_client_config=http_client_config,
|
|
380
426
|
)
|
|
381
427
|
except github_copilot.CopilotError as exc:
|
|
382
|
-
if not
|
|
428
|
+
if not should_retry_with_compact_prompt(exc):
|
|
383
429
|
raise
|
|
384
430
|
|
|
385
431
|
console.print(
|
|
@@ -507,31 +553,65 @@ def request_split_commit_plan(
|
|
|
507
553
|
http_client_config: github_copilot.HttpClientConfig | None = None,
|
|
508
554
|
) -> SplitCommitPlan:
|
|
509
555
|
"""Request and validate a split-commit plan for the staged patch units."""
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
556
|
+
planner_system_prompt = load_named_prompt(SPLIT_COMMIT_PLANNER_PROMPT_FILENAME)
|
|
557
|
+
planner_prompt = build_split_plan_prompt(
|
|
558
|
+
status,
|
|
559
|
+
patch_units,
|
|
560
|
+
preferred_commits=preferred_commits,
|
|
561
|
+
context=context,
|
|
562
|
+
)
|
|
517
563
|
|
|
564
|
+
try:
|
|
518
565
|
with console.status(
|
|
519
566
|
"[yellow]Planning split commits from [bold]staged hunks[/] ...[/yellow]"
|
|
520
567
|
):
|
|
521
568
|
response = ask_copilot_with_system_prompt(
|
|
522
|
-
|
|
569
|
+
planner_system_prompt,
|
|
523
570
|
planner_prompt,
|
|
524
571
|
model=model,
|
|
525
572
|
http_client_config=http_client_config,
|
|
526
573
|
)
|
|
574
|
+
except github_copilot.CopilotError as exc:
|
|
575
|
+
if not should_retry_with_compact_prompt(exc):
|
|
576
|
+
print_copilot_error("Could not generate a split commit plan", exc)
|
|
577
|
+
raise typer.Exit(1)
|
|
578
|
+
|
|
579
|
+
console.print(
|
|
580
|
+
"[yellow]Staged patch units exceeded the model context window; retrying split planning with summaries only.[/yellow]"
|
|
581
|
+
)
|
|
582
|
+
else:
|
|
527
583
|
return parse_split_plan_response(
|
|
528
584
|
response,
|
|
529
585
|
patch_units,
|
|
530
586
|
)
|
|
587
|
+
|
|
588
|
+
compact_planner_prompt = build_split_plan_prompt(
|
|
589
|
+
status,
|
|
590
|
+
patch_units,
|
|
591
|
+
preferred_commits=preferred_commits,
|
|
592
|
+
context=context,
|
|
593
|
+
include_patches=False,
|
|
594
|
+
)
|
|
595
|
+
|
|
596
|
+
try:
|
|
597
|
+
with console.status(
|
|
598
|
+
"[yellow]Planning split commits from [bold]patch summaries[/] ...[/yellow]"
|
|
599
|
+
):
|
|
600
|
+
response = ask_copilot_with_system_prompt(
|
|
601
|
+
planner_system_prompt,
|
|
602
|
+
compact_planner_prompt,
|
|
603
|
+
model=model,
|
|
604
|
+
http_client_config=http_client_config,
|
|
605
|
+
)
|
|
531
606
|
except github_copilot.CopilotError as exc:
|
|
532
607
|
print_copilot_error("Could not generate a split commit plan", exc)
|
|
533
608
|
raise typer.Exit(1)
|
|
534
609
|
|
|
610
|
+
return parse_split_plan_response(
|
|
611
|
+
response,
|
|
612
|
+
patch_units,
|
|
613
|
+
)
|
|
614
|
+
|
|
535
615
|
|
|
536
616
|
def request_split_commit_messages(
|
|
537
617
|
plan: SplitCommitPlan,
|
|
@@ -827,6 +907,7 @@ def handle_split_commit_flow(
|
|
|
827
907
|
context=context,
|
|
828
908
|
http_client_config=http_client_config,
|
|
829
909
|
)
|
|
910
|
+
prepared_commits = order_prepared_split_commits(prepared_commits)
|
|
830
911
|
|
|
831
912
|
if len(prepared_commits) == 1:
|
|
832
913
|
console.print(
|
|
@@ -15,6 +15,11 @@ small number of coherent commits.
|
|
|
15
15
|
- Do not split a patch unit further.
|
|
16
16
|
- Preserve the natural order of work. If unit `u1` appears before `u4` in the
|
|
17
17
|
diff, prefer to keep that order within a commit.
|
|
18
|
+
- Order commits the way experienced software developers usually land them:
|
|
19
|
+
foundational product code first, then tests that validate that code, then
|
|
20
|
+
docs/examples, then style/chore follow-ups.
|
|
21
|
+
- If a docs or test unit depends on a feat/fix/refactor/perf unit, place the
|
|
22
|
+
docs or test commit after the underlying code change.
|
|
18
23
|
- If multiple units belong to the same logical change, keep them together.
|
|
19
24
|
- If the staged changes are best represented as a single coherent commit, return
|
|
20
25
|
one commit.
|
|
@@ -173,6 +173,7 @@ def build_split_plan_prompt(
|
|
|
173
173
|
*,
|
|
174
174
|
preferred_commits: int | None = None,
|
|
175
175
|
context: str = "",
|
|
176
|
+
include_patches: bool = True,
|
|
176
177
|
) -> str:
|
|
177
178
|
"""Build the user prompt used to request a split-commit plan."""
|
|
178
179
|
if not patch_units:
|
|
@@ -193,7 +194,7 @@ def build_split_plan_prompt(
|
|
|
193
194
|
"`git status`:",
|
|
194
195
|
f"```\n{status.get_porcelain_output()}\n```",
|
|
195
196
|
"",
|
|
196
|
-
"Patch units:",
|
|
197
|
+
"Patch units:" if include_patches else "Patch units (summaries only):",
|
|
197
198
|
]
|
|
198
199
|
)
|
|
199
200
|
|
|
@@ -204,10 +205,15 @@ def build_split_plan_prompt(
|
|
|
204
205
|
f"Path: {unit.path}",
|
|
205
206
|
f"Kind: {unit.kind}",
|
|
206
207
|
f"Summary: {unit.summary}",
|
|
207
|
-
"Patch:",
|
|
208
|
-
f"```diff\n{unit.patch}\n```",
|
|
209
208
|
]
|
|
210
209
|
)
|
|
210
|
+
if include_patches:
|
|
211
|
+
prompt_parts.extend(
|
|
212
|
+
[
|
|
213
|
+
"Patch:",
|
|
214
|
+
f"```diff\n{unit.patch}\n```",
|
|
215
|
+
]
|
|
216
|
+
)
|
|
211
217
|
|
|
212
218
|
return "\n".join(prompt_parts)
|
|
213
219
|
|
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
git_copilot_commit/__init__.py,sha256=v3x5oBkxwKJEZLv62QqSmP3iqNKLtZgrWZfH8eFzlQg,60
|
|
2
|
-
git_copilot_commit/cli.py,sha256=
|
|
2
|
+
git_copilot_commit/cli.py,sha256=yN7fq43i3TFerhEOMm4ZHUy2rbiH0n70tIcwrxdWCdc,33929
|
|
3
3
|
git_copilot_commit/git.py,sha256=vNuh2j8TGmocLio4XgPpbXIktlgczNdEt2Fg1c40wuk,14962
|
|
4
4
|
git_copilot_commit/github_copilot.py,sha256=2SKVVFmQ2yETAySjSRYKQTujW-1vZEW4rFKSr--dtLs,50427
|
|
5
5
|
git_copilot_commit/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
6
6
|
git_copilot_commit/settings.py,sha256=WrM10_J3F7QBfOVmPDWpNZrNHhmZSeN-9FqQZxgdWvQ,3730
|
|
7
|
-
git_copilot_commit/split_commits.py,sha256=
|
|
7
|
+
git_copilot_commit/split_commits.py,sha256=rHyuVJggjmYjbva7BVqsM3aZRxUgOKkuZtxxvFRcu6Q,15060
|
|
8
8
|
git_copilot_commit/version.py,sha256=AieHOUX52g6N67HL0iLWtDKrgOYyulxwHWViu26Jrd4,105
|
|
9
9
|
git_copilot_commit/prompts/commit-message-generator-prompt.md,sha256=EHAS6w15vLQ-kgT1N7nPG2nWqdeTmlHje_kN9yZIoZQ,2378
|
|
10
|
-
git_copilot_commit/prompts/split-commit-planner-prompt.md,sha256=
|
|
11
|
-
git_copilot_commit-0.5.
|
|
12
|
-
git_copilot_commit-0.5.
|
|
13
|
-
git_copilot_commit-0.5.
|
|
14
|
-
git_copilot_commit-0.5.
|
|
15
|
-
git_copilot_commit-0.5.
|
|
10
|
+
git_copilot_commit/prompts/split-commit-planner-prompt.md,sha256=JActWTKMbdR9eaJyCbQ6UwzyRu5jJi8Iec7H7qWAmAA,1418
|
|
11
|
+
git_copilot_commit-0.5.5.dist-info/METADATA,sha256=8lx9m_ZB9r3MOmWAz-pb8XLHZH1cp2PC_IvWlqRp9yw,6649
|
|
12
|
+
git_copilot_commit-0.5.5.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
|
|
13
|
+
git_copilot_commit-0.5.5.dist-info/entry_points.txt,sha256=-D4bQqiuSPwQJG2zx--vJbZD1iqB5coUfoJ_gmC3rSg,66
|
|
14
|
+
git_copilot_commit-0.5.5.dist-info/licenses/LICENSE,sha256=14lNZAoKJPI1U7eGpletjN_PFm1JwP1vT_0jFKY6eWg,1065
|
|
15
|
+
git_copilot_commit-0.5.5.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|