git-copilot-commit 0.5.3__tar.gz → 0.5.4__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 (27) hide show
  1. {git_copilot_commit-0.5.3 → git_copilot_commit-0.5.4}/PKG-INFO +1 -1
  2. {git_copilot_commit-0.5.3 → git_copilot_commit-0.5.4}/src/git_copilot_commit/cli.py +45 -10
  3. {git_copilot_commit-0.5.3 → git_copilot_commit-0.5.4}/src/git_copilot_commit/split_commits.py +9 -3
  4. {git_copilot_commit-0.5.3 → git_copilot_commit-0.5.4}/tests/test_cli.py +64 -0
  5. {git_copilot_commit-0.5.3 → git_copilot_commit-0.5.4}/tests/test_split_commits.py +14 -0
  6. {git_copilot_commit-0.5.3 → git_copilot_commit-0.5.4}/.github/dependabot.yml +0 -0
  7. {git_copilot_commit-0.5.3 → git_copilot_commit-0.5.4}/.github/workflows/ci.yml +0 -0
  8. {git_copilot_commit-0.5.3 → git_copilot_commit-0.5.4}/.gitignore +0 -0
  9. {git_copilot_commit-0.5.3 → git_copilot_commit-0.5.4}/.justfile +0 -0
  10. {git_copilot_commit-0.5.3 → git_copilot_commit-0.5.4}/.python-version +0 -0
  11. {git_copilot_commit-0.5.3 → git_copilot_commit-0.5.4}/LICENSE +0 -0
  12. {git_copilot_commit-0.5.3 → git_copilot_commit-0.5.4}/README.md +0 -0
  13. {git_copilot_commit-0.5.3 → git_copilot_commit-0.5.4}/pyproject.toml +0 -0
  14. {git_copilot_commit-0.5.3 → git_copilot_commit-0.5.4}/src/git_copilot_commit/__init__.py +0 -0
  15. {git_copilot_commit-0.5.3 → git_copilot_commit-0.5.4}/src/git_copilot_commit/git.py +0 -0
  16. {git_copilot_commit-0.5.3 → git_copilot_commit-0.5.4}/src/git_copilot_commit/github_copilot.py +0 -0
  17. {git_copilot_commit-0.5.3 → git_copilot_commit-0.5.4}/src/git_copilot_commit/prompts/commit-message-generator-prompt.md +0 -0
  18. {git_copilot_commit-0.5.3 → git_copilot_commit-0.5.4}/src/git_copilot_commit/prompts/split-commit-planner-prompt.md +0 -0
  19. {git_copilot_commit-0.5.3 → git_copilot_commit-0.5.4}/src/git_copilot_commit/py.typed +0 -0
  20. {git_copilot_commit-0.5.3 → git_copilot_commit-0.5.4}/src/git_copilot_commit/settings.py +0 -0
  21. {git_copilot_commit-0.5.3 → git_copilot_commit-0.5.4}/src/git_copilot_commit/version.py +0 -0
  22. {git_copilot_commit-0.5.3 → git_copilot_commit-0.5.4}/tests/conftest.py +0 -0
  23. {git_copilot_commit-0.5.3 → git_copilot_commit-0.5.4}/tests/test_git.py +0 -0
  24. {git_copilot_commit-0.5.3 → git_copilot_commit-0.5.4}/tests/test_github_copilot_utils.py +0 -0
  25. {git_copilot_commit-0.5.3 → git_copilot_commit-0.5.4}/tests/test_settings.py +0 -0
  26. {git_copilot_commit-0.5.3 → git_copilot_commit-0.5.4}/uv.lock +0 -0
  27. {git_copilot_commit-0.5.3 → git_copilot_commit-0.5.4}/vhs/demo.vhs +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: git-copilot-commit
3
- Version: 0.5.3
3
+ Version: 0.5.4
4
4
  Summary: Automatically generate and commit changes using GitHub Copilot
5
5
  Author-email: Dheepak Krishnamurthy <1813121+kdheepak@users.noreply.github.com>
6
6
  License-File: LICENSE
@@ -341,7 +341,7 @@ def generate_commit_message_for_prompt(
341
341
  )
342
342
 
343
343
 
344
- def should_fallback_to_status_only(exc: github_copilot.CopilotError) -> bool:
344
+ def should_retry_with_compact_prompt(exc: github_copilot.CopilotError) -> bool:
345
345
  message_parts = [str(exc)]
346
346
  if isinstance(exc, github_copilot.CopilotHttpError) and exc.detail:
347
347
  message_parts.append(exc.detail)
@@ -360,6 +360,7 @@ def should_fallback_to_status_only(exc: github_copilot.CopilotError) -> bool:
360
360
  "max prompt tokens",
361
361
  "input tokens",
362
362
  "prompt tokens",
363
+ "prompt token count",
363
364
  )
364
365
  return any(indicator in haystack for indicator in indicators)
365
366
 
@@ -379,7 +380,7 @@ def generate_commit_message_for_status(
379
380
  http_client_config=http_client_config,
380
381
  )
381
382
  except github_copilot.CopilotError as exc:
382
- if not should_fallback_to_status_only(exc):
383
+ if not should_retry_with_compact_prompt(exc):
383
384
  raise
384
385
 
385
386
  console.print(
@@ -507,31 +508,65 @@ def request_split_commit_plan(
507
508
  http_client_config: github_copilot.HttpClientConfig | None = None,
508
509
  ) -> SplitCommitPlan:
509
510
  """Request and validate a split-commit plan for the staged patch units."""
510
- try:
511
- planner_prompt = build_split_plan_prompt(
512
- status,
513
- patch_units,
514
- preferred_commits=preferred_commits,
515
- context=context,
516
- )
511
+ planner_system_prompt = load_named_prompt(SPLIT_COMMIT_PLANNER_PROMPT_FILENAME)
512
+ planner_prompt = build_split_plan_prompt(
513
+ status,
514
+ patch_units,
515
+ preferred_commits=preferred_commits,
516
+ context=context,
517
+ )
517
518
 
519
+ try:
518
520
  with console.status(
519
521
  "[yellow]Planning split commits from [bold]staged hunks[/] ...[/yellow]"
520
522
  ):
521
523
  response = ask_copilot_with_system_prompt(
522
- load_named_prompt(SPLIT_COMMIT_PLANNER_PROMPT_FILENAME),
524
+ planner_system_prompt,
523
525
  planner_prompt,
524
526
  model=model,
525
527
  http_client_config=http_client_config,
526
528
  )
529
+ except github_copilot.CopilotError as exc:
530
+ if not should_retry_with_compact_prompt(exc):
531
+ print_copilot_error("Could not generate a split commit plan", exc)
532
+ raise typer.Exit(1)
533
+
534
+ console.print(
535
+ "[yellow]Staged patch units exceeded the model context window; retrying split planning with summaries only.[/yellow]"
536
+ )
537
+ else:
527
538
  return parse_split_plan_response(
528
539
  response,
529
540
  patch_units,
530
541
  )
542
+
543
+ compact_planner_prompt = build_split_plan_prompt(
544
+ status,
545
+ patch_units,
546
+ preferred_commits=preferred_commits,
547
+ context=context,
548
+ include_patches=False,
549
+ )
550
+
551
+ try:
552
+ with console.status(
553
+ "[yellow]Planning split commits from [bold]patch summaries[/] ...[/yellow]"
554
+ ):
555
+ response = ask_copilot_with_system_prompt(
556
+ planner_system_prompt,
557
+ compact_planner_prompt,
558
+ model=model,
559
+ http_client_config=http_client_config,
560
+ )
531
561
  except github_copilot.CopilotError as exc:
532
562
  print_copilot_error("Could not generate a split commit plan", exc)
533
563
  raise typer.Exit(1)
534
564
 
565
+ return parse_split_plan_response(
566
+ response,
567
+ patch_units,
568
+ )
569
+
535
570
 
536
571
  def request_split_commit_messages(
537
572
  plan: SplitCommitPlan,
@@ -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
 
@@ -142,6 +142,70 @@ def test_generate_commit_message_for_status_retries_without_diff_on_context_over
142
142
  mock_print.assert_called()
143
143
 
144
144
 
145
+ def test_request_split_commit_plan_retries_without_patches_on_context_overflow(
146
+ monkeypatch: pytest.MonkeyPatch,
147
+ ) -> None:
148
+ status = GitStatus(
149
+ files=[
150
+ GitFile(path="src/example.py", status=" ", staged_status="M"),
151
+ GitFile(path="README.md", status=" ", staged_status="A"),
152
+ ],
153
+ staged_diff=(
154
+ "diff --git a/src/example.py b/src/example.py\n+print('hi')\n"
155
+ "diff --git a/README.md b/README.md\n+# hi\n"
156
+ ),
157
+ unstaged_diff="",
158
+ )
159
+ patch_units = (
160
+ PatchUnit(
161
+ id="u1",
162
+ order=0,
163
+ path="src/example.py",
164
+ staged_status="M",
165
+ kind="hunk",
166
+ patch="diff --git a/src/example.py b/src/example.py\n+print('hi')\n",
167
+ summary="src/example.py hunk 1/1 @@ -1 +1 @@ (+1/-0)",
168
+ ),
169
+ PatchUnit(
170
+ id="u2",
171
+ order=1,
172
+ path="README.md",
173
+ staged_status="A",
174
+ kind="new_file",
175
+ patch="diff --git a/README.md b/README.md\n+# hi\n",
176
+ summary="add README.md (+1/-0)",
177
+ ),
178
+ )
179
+ mock_print = Mock()
180
+ mock_ask = Mock(
181
+ side_effect=[
182
+ github_copilot.CopilotHttpError(
183
+ 400,
184
+ "Bad Request",
185
+ (
186
+ '{"error":{"message":"prompt token count of 1719062 exceeds '
187
+ 'the limit of 128000","code":"model_max_prompt_tokens_exceeded"}}'
188
+ ),
189
+ ),
190
+ '{"commits":[{"unit_ids":["u1"]},{"unit_ids":["u2"]}]}',
191
+ ]
192
+ )
193
+ monkeypatch.setattr(cli.console, "print", mock_print)
194
+ monkeypatch.setattr(cli, "load_named_prompt", Mock(return_value="system prompt"))
195
+ monkeypatch.setattr(cli, "ask_copilot_with_system_prompt", mock_ask)
196
+
197
+ plan = cli.request_split_commit_plan(status, patch_units)
198
+
199
+ assert [commit.unit_ids for commit in plan.commits] == [("u1",), ("u2",)]
200
+ assert mock_ask.call_count == 2
201
+ first_prompt = mock_ask.call_args_list[0].args[1]
202
+ second_prompt = mock_ask.call_args_list[1].args[1]
203
+ assert "```diff" in first_prompt
204
+ assert "```diff" not in second_prompt
205
+ assert "Patch units (summaries only):" in second_prompt
206
+ mock_print.assert_called()
207
+
208
+
145
209
  def test_display_split_commit_plan_shows_files_not_hunk_summaries(
146
210
  monkeypatch: pytest.MonkeyPatch,
147
211
  ) -> None:
@@ -99,6 +99,20 @@ def test_build_split_plan_prompt_includes_unit_details() -> None:
99
99
  assert "Kind: new_file" in prompt
100
100
 
101
101
 
102
+ def test_build_split_plan_prompt_can_omit_raw_patches() -> None:
103
+ units = extract_patch_units(MULTI_FILE_DIFF)
104
+
105
+ prompt = build_split_plan_prompt(
106
+ make_status(),
107
+ units,
108
+ include_patches=False,
109
+ )
110
+
111
+ assert "Patch units (summaries only):" in prompt
112
+ assert "Summary: src/app.py hunk 1/2" in prompt
113
+ assert "```diff" not in prompt
114
+
115
+
102
116
  def test_build_split_plan_prompt_supports_preferred_commit_count() -> None:
103
117
  units = extract_patch_units(MULTI_FILE_DIFF)
104
118