git-copilot-commit 0.5.7__py3-none-any.whl → 0.6.1__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 +271 -177
- git_copilot_commit/git.py +123 -0
- git_copilot_commit/llms/__init__.py +0 -0
- git_copilot_commit/llms/copilot.py +799 -0
- git_copilot_commit/llms/core.py +802 -0
- git_copilot_commit/llms/openai_api.py +219 -0
- git_copilot_commit/llms/providers.py +373 -0
- git_copilot_commit/prompts/commit-message-generator-prompt.md +4 -3
- git_copilot_commit/prompts/split-commit-planner-prompt.md +1 -0
- {git_copilot_commit-0.5.7.dist-info → git_copilot_commit-0.6.1.dist-info}/METADATA +86 -8
- git_copilot_commit-0.6.1.dist-info/RECORD +19 -0
- git_copilot_commit/github_copilot.py +0 -1579
- git_copilot_commit-0.5.7.dist-info/RECORD +0 -15
- {git_copilot_commit-0.5.7.dist-info → git_copilot_commit-0.6.1.dist-info}/WHEEL +0 -0
- {git_copilot_commit-0.5.7.dist-info → git_copilot_commit-0.6.1.dist-info}/entry_points.txt +0 -0
- {git_copilot_commit-0.5.7.dist-info → git_copilot_commit-0.6.1.dist-info}/licenses/LICENSE +0 -0
git_copilot_commit/cli.py
CHANGED
|
@@ -9,12 +9,10 @@ import re
|
|
|
9
9
|
import sys
|
|
10
10
|
from typing import Annotated, Sequence
|
|
11
11
|
|
|
12
|
-
import
|
|
13
|
-
import typer
|
|
12
|
+
import cyclopts
|
|
14
13
|
from rich.console import Console
|
|
15
14
|
from rich.panel import Panel
|
|
16
|
-
from rich.prompt import Confirm
|
|
17
|
-
from typer.main import get_command
|
|
15
|
+
from rich.prompt import Confirm, Prompt
|
|
18
16
|
|
|
19
17
|
from .git import GitRepository, GitError, GitStatus, NotAGitRepositoryError
|
|
20
18
|
from .split_commits import (
|
|
@@ -29,10 +27,12 @@ from .split_commits import (
|
|
|
29
27
|
)
|
|
30
28
|
from .settings import Settings
|
|
31
29
|
from .version import __version__
|
|
32
|
-
from . import
|
|
30
|
+
from .llms import copilot
|
|
31
|
+
from .llms import core as llm
|
|
32
|
+
from .llms import providers
|
|
33
33
|
|
|
34
34
|
console = Console()
|
|
35
|
-
app =
|
|
35
|
+
app = cyclopts.App(help=__doc__, version=lambda: f"git-copilot-commit {__version__}")
|
|
36
36
|
|
|
37
37
|
COMMIT_MESSAGE_PROMPT_FILENAME = "commit-message-generator-prompt.md"
|
|
38
38
|
SPLIT_COMMIT_PLANNER_PROMPT_FILENAME = "split-commit-planner-prompt.md"
|
|
@@ -55,22 +55,50 @@ NATIVE_TLS_HELP = (
|
|
|
55
55
|
|
|
56
56
|
CaBundleOption = Annotated[
|
|
57
57
|
str | None,
|
|
58
|
-
|
|
58
|
+
cyclopts.Parameter(name="--ca-bundle", help=CA_BUNDLE_HELP),
|
|
59
59
|
]
|
|
60
60
|
InsecureOption = Annotated[
|
|
61
61
|
bool,
|
|
62
|
-
|
|
62
|
+
cyclopts.Parameter(name="--insecure", help="Disable SSL certificate verification."),
|
|
63
63
|
]
|
|
64
64
|
NativeTlsOption = Annotated[
|
|
65
65
|
bool,
|
|
66
|
-
|
|
66
|
+
cyclopts.Parameter(
|
|
67
|
+
name="--native-tls",
|
|
68
|
+
negative="--no-native-tls",
|
|
69
|
+
help=NATIVE_TLS_HELP,
|
|
70
|
+
),
|
|
71
|
+
]
|
|
72
|
+
ProviderOption = Annotated[
|
|
73
|
+
str | None,
|
|
74
|
+
cyclopts.Parameter(
|
|
75
|
+
name="--provider",
|
|
76
|
+
help="LLM provider to use: copilot or openai.",
|
|
77
|
+
),
|
|
78
|
+
]
|
|
79
|
+
BaseUrlOption = Annotated[
|
|
80
|
+
str | None,
|
|
81
|
+
cyclopts.Parameter(
|
|
82
|
+
name="--base-url",
|
|
83
|
+
help=(
|
|
84
|
+
"Base URL for an OpenAI-compatible provider, for example "
|
|
85
|
+
"http://127.0.0.1:11434/v1."
|
|
86
|
+
),
|
|
87
|
+
),
|
|
88
|
+
]
|
|
89
|
+
ApiKeyOption = Annotated[
|
|
90
|
+
str | None,
|
|
91
|
+
cyclopts.Parameter(
|
|
92
|
+
name="--api-key",
|
|
93
|
+
help="API key for an OpenAI-compatible provider. Omit when the server does not require one.",
|
|
94
|
+
),
|
|
67
95
|
]
|
|
68
96
|
|
|
69
97
|
|
|
70
98
|
SplitOption = Annotated[
|
|
71
99
|
bool,
|
|
72
|
-
|
|
73
|
-
"--split",
|
|
100
|
+
cyclopts.Parameter(
|
|
101
|
+
name="--split",
|
|
74
102
|
help=(
|
|
75
103
|
"Split staged hunks into multiple commits automatically. Pass "
|
|
76
104
|
"`--split=N` to express a preference for N commits."
|
|
@@ -79,10 +107,10 @@ SplitOption = Annotated[
|
|
|
79
107
|
]
|
|
80
108
|
SplitCountOption = Annotated[
|
|
81
109
|
int | None,
|
|
82
|
-
|
|
83
|
-
"--split-count",
|
|
84
|
-
|
|
85
|
-
|
|
110
|
+
cyclopts.Parameter(
|
|
111
|
+
name="--split-count",
|
|
112
|
+
show=False,
|
|
113
|
+
validator=cyclopts.validators.Number(gte=1),
|
|
86
114
|
),
|
|
87
115
|
]
|
|
88
116
|
|
|
@@ -198,39 +226,13 @@ def order_prepared_split_commits(
|
|
|
198
226
|
def run(args: Sequence[str] | None = None) -> None:
|
|
199
227
|
"""Run the CLI entrypoint with argument normalization."""
|
|
200
228
|
raw_args = list(args) if args is not None else sys.argv[1:]
|
|
201
|
-
|
|
202
|
-
command.main(
|
|
203
|
-
args=preprocess_cli_args(raw_args),
|
|
204
|
-
prog_name=Path(sys.argv[0]).name,
|
|
205
|
-
)
|
|
229
|
+
app(preprocess_cli_args(raw_args))
|
|
206
230
|
|
|
207
231
|
|
|
208
|
-
def
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
@app.callback(invoke_without_command=True)
|
|
215
|
-
def main(
|
|
216
|
-
ctx: typer.Context,
|
|
217
|
-
_: bool = typer.Option(
|
|
218
|
-
False, "--version", callback=version_callback, help="Show version and exit"
|
|
219
|
-
),
|
|
220
|
-
):
|
|
221
|
-
"""
|
|
222
|
-
Automatically commit changes in the current git repository.
|
|
223
|
-
"""
|
|
224
|
-
if ctx.invoked_subcommand is None:
|
|
225
|
-
# Show help when no command is provided
|
|
226
|
-
console.print(ctx.get_help())
|
|
227
|
-
raise typer.Exit()
|
|
228
|
-
else:
|
|
229
|
-
# Don't show version for print command to avoid interfering with pipes
|
|
230
|
-
if ctx.invoked_subcommand != "echo":
|
|
231
|
-
console.print(
|
|
232
|
-
f"[bold]{(__package__ or 'git_copilot_commit').replace('_', '-')}[/] - [bold green]v{__version__}[/]\n"
|
|
233
|
-
)
|
|
232
|
+
def print_cli_banner() -> None:
|
|
233
|
+
"""Render the CLI banner before command output."""
|
|
234
|
+
package_name = (__package__ or "git_copilot_commit").replace("_", "-")
|
|
235
|
+
console.print(f"[bold]{package_name}[/] - [bold green]v{__version__}[/]\n")
|
|
234
236
|
|
|
235
237
|
|
|
236
238
|
def get_prompt_locations(filename: str):
|
|
@@ -253,7 +255,7 @@ def resolve_prompt_file() -> Path | None:
|
|
|
253
255
|
console.print(
|
|
254
256
|
f"[red]Configured default prompt file in {settings.config_file} is invalid.[/red]"
|
|
255
257
|
)
|
|
256
|
-
raise
|
|
258
|
+
raise SystemExit(1)
|
|
257
259
|
|
|
258
260
|
if configured_prompt_file is None:
|
|
259
261
|
return None
|
|
@@ -271,7 +273,7 @@ def load_system_prompt() -> str:
|
|
|
271
273
|
console.print(
|
|
272
274
|
f"[red]Error reading prompt file {resolved_prompt_file}: {exc}[/red]"
|
|
273
275
|
)
|
|
274
|
-
raise
|
|
276
|
+
raise SystemExit(1)
|
|
275
277
|
|
|
276
278
|
return load_named_prompt(COMMIT_MESSAGE_PROMPT_FILENAME)
|
|
277
279
|
|
|
@@ -285,7 +287,7 @@ def load_named_prompt(filename: str) -> str:
|
|
|
285
287
|
continue
|
|
286
288
|
|
|
287
289
|
console.print(f"[red]Error: Prompt file {filename} not found in any location[/red]")
|
|
288
|
-
raise
|
|
290
|
+
raise SystemExit(1)
|
|
289
291
|
|
|
290
292
|
|
|
291
293
|
def build_http_client_config(
|
|
@@ -293,29 +295,29 @@ def build_http_client_config(
|
|
|
293
295
|
ca_bundle: str | None,
|
|
294
296
|
insecure: bool,
|
|
295
297
|
native_tls: bool,
|
|
296
|
-
) ->
|
|
298
|
+
) -> llm.HttpClientConfig:
|
|
297
299
|
if ca_bundle is not None:
|
|
298
300
|
ca_bundle = os.path.expanduser(ca_bundle)
|
|
299
|
-
return
|
|
301
|
+
return llm.HttpClientConfig(
|
|
300
302
|
native_tls=native_tls,
|
|
301
303
|
insecure=insecure,
|
|
302
304
|
ca_bundle=ca_bundle,
|
|
303
305
|
)
|
|
304
306
|
|
|
305
307
|
|
|
306
|
-
def
|
|
307
|
-
"""Render
|
|
308
|
-
if isinstance(exc,
|
|
308
|
+
def print_llm_error(message: str, exc: llm.LLMError) -> None:
|
|
309
|
+
"""Render LLM errors, with rich formatting for model selection issues."""
|
|
310
|
+
if isinstance(exc, llm.ModelSelectionError):
|
|
309
311
|
console.print(f"[red]{message}[/red]")
|
|
310
|
-
|
|
312
|
+
llm.print_model_selection_error(exc)
|
|
311
313
|
return
|
|
312
314
|
|
|
313
315
|
console.print(f"[red]{message}: {exc}[/red]")
|
|
314
316
|
|
|
315
317
|
|
|
316
|
-
def display_selected_model(model:
|
|
317
|
-
"""Show the resolved
|
|
318
|
-
details = [
|
|
318
|
+
def display_selected_model(model: llm.Model) -> None:
|
|
319
|
+
"""Show the resolved model for the current command."""
|
|
320
|
+
details = [llm.infer_api_surface(model)]
|
|
319
321
|
if model.vendor:
|
|
320
322
|
details.insert(0, model.vendor)
|
|
321
323
|
console.print(f"[green]Using model:[/green] {model.id} ({', '.join(details)})")
|
|
@@ -330,7 +332,7 @@ def build_commit_message_prompt(
|
|
|
330
332
|
"""Build the prompt used to generate a commit message."""
|
|
331
333
|
if not status.has_staged_changes:
|
|
332
334
|
console.print("[red]No staged changes to commit.[/red]")
|
|
333
|
-
raise
|
|
335
|
+
raise SystemExit()
|
|
334
336
|
|
|
335
337
|
prompt_parts = [
|
|
336
338
|
"`git status`:\n",
|
|
@@ -352,20 +354,26 @@ def build_commit_message_prompt(
|
|
|
352
354
|
|
|
353
355
|
|
|
354
356
|
def normalize_model_name(model: str | None) -> str | None:
|
|
355
|
-
"""Normalize model names accepted by the CLI to
|
|
356
|
-
if model is not None
|
|
357
|
-
|
|
357
|
+
"""Normalize model names accepted by the CLI to provider model ids."""
|
|
358
|
+
if model is not None:
|
|
359
|
+
for prefix in (
|
|
360
|
+
"copilot/",
|
|
361
|
+
"openai-compatible/",
|
|
362
|
+
):
|
|
363
|
+
if model.startswith(prefix):
|
|
364
|
+
return model.replace(prefix, "", 1)
|
|
358
365
|
return model
|
|
359
366
|
|
|
360
367
|
|
|
361
|
-
def
|
|
368
|
+
def ask_llm_with_system_prompt(
|
|
362
369
|
system_prompt: str,
|
|
363
370
|
prompt: str,
|
|
364
371
|
model: str | None = None,
|
|
365
|
-
|
|
372
|
+
provider_config: providers.ProviderConfig | None = None,
|
|
373
|
+
http_client_config: llm.HttpClientConfig | None = None,
|
|
366
374
|
) -> str:
|
|
367
|
-
"""Send a prepared prompt to
|
|
368
|
-
return
|
|
375
|
+
"""Send a prepared prompt to the selected LLM provider."""
|
|
376
|
+
return providers.ask(
|
|
369
377
|
f"""
|
|
370
378
|
# System Prompt
|
|
371
379
|
|
|
@@ -375,6 +383,7 @@ def ask_copilot_with_system_prompt(
|
|
|
375
383
|
|
|
376
384
|
{prompt}
|
|
377
385
|
""",
|
|
386
|
+
provider_config=provider_config,
|
|
378
387
|
model=normalize_model_name(model),
|
|
379
388
|
http_client_config=http_client_config,
|
|
380
389
|
)
|
|
@@ -383,20 +392,22 @@ def ask_copilot_with_system_prompt(
|
|
|
383
392
|
def generate_commit_message_for_prompt(
|
|
384
393
|
prompt: str,
|
|
385
394
|
model: str | None = None,
|
|
386
|
-
|
|
395
|
+
provider_config: providers.ProviderConfig | None = None,
|
|
396
|
+
http_client_config: llm.HttpClientConfig | None = None,
|
|
387
397
|
) -> str:
|
|
388
398
|
"""Generate a conventional commit message from a prepared prompt."""
|
|
389
|
-
return
|
|
399
|
+
return ask_llm_with_system_prompt(
|
|
390
400
|
load_system_prompt(),
|
|
391
401
|
prompt,
|
|
392
402
|
model=model,
|
|
403
|
+
provider_config=provider_config,
|
|
393
404
|
http_client_config=http_client_config,
|
|
394
405
|
)
|
|
395
406
|
|
|
396
407
|
|
|
397
|
-
def should_retry_with_compact_prompt(exc:
|
|
408
|
+
def should_retry_with_compact_prompt(exc: llm.LLMError) -> bool:
|
|
398
409
|
message_parts = [str(exc)]
|
|
399
|
-
if isinstance(exc,
|
|
410
|
+
if isinstance(exc, llm.LLMHttpError) and exc.detail:
|
|
400
411
|
message_parts.append(exc.detail)
|
|
401
412
|
|
|
402
413
|
haystack = " ".join(part.strip() for part in message_parts if part).lower()
|
|
@@ -422,7 +433,8 @@ def generate_commit_message_for_status(
|
|
|
422
433
|
status: GitStatus,
|
|
423
434
|
model: str | None = None,
|
|
424
435
|
context: str = "",
|
|
425
|
-
|
|
436
|
+
provider_config: providers.ProviderConfig | None = None,
|
|
437
|
+
http_client_config: llm.HttpClientConfig | None = None,
|
|
426
438
|
) -> str:
|
|
427
439
|
"""Generate a commit message for a staged status snapshot."""
|
|
428
440
|
full_prompt = build_commit_message_prompt(status, context=context)
|
|
@@ -430,9 +442,10 @@ def generate_commit_message_for_status(
|
|
|
430
442
|
return generate_commit_message_for_prompt(
|
|
431
443
|
full_prompt,
|
|
432
444
|
model=model,
|
|
445
|
+
provider_config=provider_config,
|
|
433
446
|
http_client_config=http_client_config,
|
|
434
447
|
)
|
|
435
|
-
except
|
|
448
|
+
except llm.LLMError as exc:
|
|
436
449
|
if not should_retry_with_compact_prompt(exc):
|
|
437
450
|
raise
|
|
438
451
|
|
|
@@ -447,6 +460,7 @@ def generate_commit_message_for_status(
|
|
|
447
460
|
return generate_commit_message_for_prompt(
|
|
448
461
|
fallback_prompt,
|
|
449
462
|
model=model,
|
|
463
|
+
provider_config=provider_config,
|
|
450
464
|
http_client_config=http_client_config,
|
|
451
465
|
)
|
|
452
466
|
|
|
@@ -466,35 +480,35 @@ def commit_with_retry_no_verify(
|
|
|
466
480
|
"Retry commit with [bold]`-n`[/] (skip hooks) using the same commit message?",
|
|
467
481
|
default=True,
|
|
468
482
|
):
|
|
469
|
-
raise
|
|
483
|
+
raise SystemExit(1)
|
|
470
484
|
|
|
471
485
|
try:
|
|
472
486
|
return repo.commit(message, use_editor=use_editor, no_verify=True, env=env)
|
|
473
487
|
except GitError as retry_error:
|
|
474
488
|
console.print(f"[red]Commit with -n failed: {retry_error}[/red]")
|
|
475
|
-
raise
|
|
489
|
+
raise SystemExit(1)
|
|
476
490
|
|
|
477
491
|
|
|
478
492
|
def ensure_copilot_authentication(
|
|
479
|
-
http_client_config:
|
|
493
|
+
http_client_config: llm.HttpClientConfig,
|
|
480
494
|
) -> None:
|
|
481
495
|
"""Authenticate if no cached Copilot credentials are available."""
|
|
482
496
|
try:
|
|
483
|
-
existing_credentials =
|
|
484
|
-
except
|
|
497
|
+
existing_credentials = copilot.load_credentials()
|
|
498
|
+
except copilot.LLMError:
|
|
485
499
|
existing_credentials = None
|
|
486
500
|
|
|
487
501
|
if existing_credentials is not None:
|
|
488
502
|
return
|
|
489
503
|
|
|
490
504
|
try:
|
|
491
|
-
|
|
505
|
+
copilot.login(
|
|
492
506
|
force=True,
|
|
493
507
|
http_client_config=http_client_config,
|
|
494
508
|
)
|
|
495
|
-
except
|
|
496
|
-
|
|
497
|
-
raise
|
|
509
|
+
except copilot.LLMError as exc:
|
|
510
|
+
print_llm_error("Authentication failed", exc)
|
|
511
|
+
raise SystemExit(1)
|
|
498
512
|
|
|
499
513
|
|
|
500
514
|
def stage_changes_for_commit(
|
|
@@ -533,7 +547,8 @@ def request_commit_message(
|
|
|
533
547
|
status: GitStatus,
|
|
534
548
|
model: str | None = None,
|
|
535
549
|
context: str = "",
|
|
536
|
-
|
|
550
|
+
provider_config: providers.ProviderConfig | None = None,
|
|
551
|
+
http_client_config: llm.HttpClientConfig | None = None,
|
|
537
552
|
) -> str:
|
|
538
553
|
"""Request a commit message for the provided staged state."""
|
|
539
554
|
try:
|
|
@@ -544,11 +559,12 @@ def request_commit_message(
|
|
|
544
559
|
status,
|
|
545
560
|
model=model,
|
|
546
561
|
context=context,
|
|
562
|
+
provider_config=provider_config,
|
|
547
563
|
http_client_config=http_client_config,
|
|
548
564
|
)
|
|
549
|
-
except
|
|
550
|
-
|
|
551
|
-
raise
|
|
565
|
+
except llm.LLMError as exc:
|
|
566
|
+
print_llm_error("Could not generate a commit message", exc)
|
|
567
|
+
raise SystemExit(1)
|
|
552
568
|
|
|
553
569
|
|
|
554
570
|
def request_split_commit_plan(
|
|
@@ -558,7 +574,8 @@ def request_split_commit_plan(
|
|
|
558
574
|
preferred_commits: int | None = None,
|
|
559
575
|
model: str | None = None,
|
|
560
576
|
context: str = "",
|
|
561
|
-
|
|
577
|
+
provider_config: providers.ProviderConfig | None = None,
|
|
578
|
+
http_client_config: llm.HttpClientConfig | None = None,
|
|
562
579
|
) -> SplitCommitPlan:
|
|
563
580
|
"""Request and validate a split-commit plan for the staged patch units."""
|
|
564
581
|
planner_system_prompt = load_named_prompt(SPLIT_COMMIT_PLANNER_PROMPT_FILENAME)
|
|
@@ -573,16 +590,17 @@ def request_split_commit_plan(
|
|
|
573
590
|
with console.status(
|
|
574
591
|
"[yellow]Planning split commits from [bold]staged hunks[/] ...[/yellow]"
|
|
575
592
|
):
|
|
576
|
-
response =
|
|
593
|
+
response = ask_llm_with_system_prompt(
|
|
577
594
|
planner_system_prompt,
|
|
578
595
|
planner_prompt,
|
|
579
596
|
model=model,
|
|
597
|
+
provider_config=provider_config,
|
|
580
598
|
http_client_config=http_client_config,
|
|
581
599
|
)
|
|
582
|
-
except
|
|
600
|
+
except llm.LLMError as exc:
|
|
583
601
|
if not should_retry_with_compact_prompt(exc):
|
|
584
|
-
|
|
585
|
-
raise
|
|
602
|
+
print_llm_error("Could not generate a split commit plan", exc)
|
|
603
|
+
raise SystemExit(1)
|
|
586
604
|
|
|
587
605
|
console.print(
|
|
588
606
|
"[yellow]Staged patch units exceeded the model context window; retrying split planning with summaries only.[/yellow]"
|
|
@@ -605,15 +623,16 @@ def request_split_commit_plan(
|
|
|
605
623
|
with console.status(
|
|
606
624
|
"[yellow]Planning split commits from [bold]patch summaries[/] ...[/yellow]"
|
|
607
625
|
):
|
|
608
|
-
response =
|
|
626
|
+
response = ask_llm_with_system_prompt(
|
|
609
627
|
planner_system_prompt,
|
|
610
628
|
compact_planner_prompt,
|
|
611
629
|
model=model,
|
|
630
|
+
provider_config=provider_config,
|
|
612
631
|
http_client_config=http_client_config,
|
|
613
632
|
)
|
|
614
|
-
except
|
|
615
|
-
|
|
616
|
-
raise
|
|
633
|
+
except llm.LLMError as exc:
|
|
634
|
+
print_llm_error("Could not generate a split commit plan", exc)
|
|
635
|
+
raise SystemExit(1)
|
|
617
636
|
|
|
618
637
|
return parse_split_plan_response(
|
|
619
638
|
response,
|
|
@@ -627,7 +646,8 @@ def request_split_commit_messages(
|
|
|
627
646
|
*,
|
|
628
647
|
model: str | None = None,
|
|
629
648
|
context: str = "",
|
|
630
|
-
|
|
649
|
+
provider_config: providers.ProviderConfig | None = None,
|
|
650
|
+
http_client_config: llm.HttpClientConfig | None = None,
|
|
631
651
|
) -> list[PreparedSplitCommit]:
|
|
632
652
|
"""Generate commit messages for each planned split-commit group."""
|
|
633
653
|
try:
|
|
@@ -643,6 +663,7 @@ def request_split_commit_messages(
|
|
|
643
663
|
build_status_for_patch_units(unit_group),
|
|
644
664
|
model=model,
|
|
645
665
|
context=context,
|
|
666
|
+
provider_config=provider_config,
|
|
646
667
|
http_client_config=http_client_config,
|
|
647
668
|
)
|
|
648
669
|
|
|
@@ -651,9 +672,9 @@ def request_split_commit_messages(
|
|
|
651
672
|
)
|
|
652
673
|
|
|
653
674
|
return prepared_commits
|
|
654
|
-
except
|
|
655
|
-
|
|
656
|
-
raise
|
|
675
|
+
except llm.LLMError as exc:
|
|
676
|
+
print_llm_error("Could not generate split commit messages", exc)
|
|
677
|
+
raise SystemExit(1)
|
|
657
678
|
|
|
658
679
|
|
|
659
680
|
def confirm_split_commit_count(
|
|
@@ -683,7 +704,7 @@ def confirm_split_commit_count(
|
|
|
683
704
|
return plan
|
|
684
705
|
|
|
685
706
|
console.print("Split commit plan cancelled.")
|
|
686
|
-
raise
|
|
707
|
+
raise SystemExit()
|
|
687
708
|
|
|
688
709
|
|
|
689
710
|
def display_commit_message(commit_message: str) -> None:
|
|
@@ -722,7 +743,7 @@ def execute_commit_action(
|
|
|
722
743
|
if yes:
|
|
723
744
|
return commit_with_retry_no_verify(repo, commit_message)
|
|
724
745
|
|
|
725
|
-
choice =
|
|
746
|
+
choice = Prompt.ask(
|
|
726
747
|
"Choose action: (c)ommit, (e)dit message, (q)uit",
|
|
727
748
|
default="c",
|
|
728
749
|
show_default=True,
|
|
@@ -730,7 +751,7 @@ def execute_commit_action(
|
|
|
730
751
|
|
|
731
752
|
if choice == "q":
|
|
732
753
|
console.print("Commit cancelled.")
|
|
733
|
-
raise
|
|
754
|
+
raise SystemExit()
|
|
734
755
|
if choice == "e":
|
|
735
756
|
console.print("[cyan]Opening git editor...[/cyan]")
|
|
736
757
|
return commit_with_retry_no_verify(repo, commit_message, use_editor=True)
|
|
@@ -738,7 +759,7 @@ def execute_commit_action(
|
|
|
738
759
|
return commit_with_retry_no_verify(repo, commit_message)
|
|
739
760
|
|
|
740
761
|
console.print("Invalid choice. Commit cancelled.")
|
|
741
|
-
raise
|
|
762
|
+
raise SystemExit()
|
|
742
763
|
|
|
743
764
|
|
|
744
765
|
def execute_split_commit_plan(
|
|
@@ -750,7 +771,7 @@ def execute_split_commit_plan(
|
|
|
750
771
|
"""Run the split-commit plan against temporary alternate indexes."""
|
|
751
772
|
use_editor = False
|
|
752
773
|
if not yes:
|
|
753
|
-
choice =
|
|
774
|
+
choice = Prompt.ask(
|
|
754
775
|
"Choose action: (c)ommit all, (e)dit each message, (q)uit",
|
|
755
776
|
default="c",
|
|
756
777
|
show_default=True,
|
|
@@ -758,12 +779,12 @@ def execute_split_commit_plan(
|
|
|
758
779
|
|
|
759
780
|
if choice == "q":
|
|
760
781
|
console.print("Commit cancelled.")
|
|
761
|
-
raise
|
|
782
|
+
raise SystemExit()
|
|
762
783
|
if choice == "e":
|
|
763
784
|
use_editor = True
|
|
764
785
|
elif choice != "c":
|
|
765
786
|
console.print("Invalid choice. Commit cancelled.")
|
|
766
|
-
raise
|
|
787
|
+
raise SystemExit()
|
|
767
788
|
|
|
768
789
|
execution_state = SplitCommitExecutionState(
|
|
769
790
|
original_head_sha=repo.get_head_sha() if repo.has_commit("HEAD") else None,
|
|
@@ -793,16 +814,19 @@ def execute_split_commit_plan(
|
|
|
793
814
|
console.print(
|
|
794
815
|
f"[red]Failed to apply the planned changes for commit {index}: {exc}[/red]"
|
|
795
816
|
)
|
|
796
|
-
raise
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
repo
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
817
|
+
raise SystemExit(1)
|
|
818
|
+
|
|
819
|
+
try:
|
|
820
|
+
commit_shas.append(
|
|
821
|
+
repo.create_commit_from_index(
|
|
822
|
+
prepared_commit.message,
|
|
823
|
+
index=alternate_index,
|
|
824
|
+
use_editor=use_editor,
|
|
825
|
+
)
|
|
804
826
|
)
|
|
805
|
-
|
|
827
|
+
except GitError as exc:
|
|
828
|
+
console.print(f"[red]Failed to create commit {index}: {exc}[/red]")
|
|
829
|
+
raise SystemExit(1)
|
|
806
830
|
except BaseException:
|
|
807
831
|
try:
|
|
808
832
|
if execution_state.original_head_sha is not None:
|
|
@@ -832,13 +856,15 @@ def handle_single_commit_flow(
|
|
|
832
856
|
model: str | None = None,
|
|
833
857
|
yes: bool = False,
|
|
834
858
|
context: str = "",
|
|
835
|
-
|
|
859
|
+
provider_config: providers.ProviderConfig | None = None,
|
|
860
|
+
http_client_config: llm.HttpClientConfig | None = None,
|
|
836
861
|
) -> None:
|
|
837
862
|
"""Generate, display, and execute the single-commit flow."""
|
|
838
863
|
commit_message = request_commit_message(
|
|
839
864
|
status,
|
|
840
865
|
model=model,
|
|
841
866
|
context=context,
|
|
867
|
+
provider_config=provider_config,
|
|
842
868
|
http_client_config=http_client_config,
|
|
843
869
|
)
|
|
844
870
|
display_commit_message(commit_message)
|
|
@@ -855,7 +881,8 @@ def handle_split_commit_flow(
|
|
|
855
881
|
model: str | None = None,
|
|
856
882
|
yes: bool = False,
|
|
857
883
|
context: str = "",
|
|
858
|
-
|
|
884
|
+
provider_config: providers.ProviderConfig | None = None,
|
|
885
|
+
http_client_config: llm.HttpClientConfig | None = None,
|
|
859
886
|
) -> None:
|
|
860
887
|
"""Generate, display, and execute the split-commit flow."""
|
|
861
888
|
patch_units = tuple(
|
|
@@ -872,6 +899,7 @@ def handle_split_commit_flow(
|
|
|
872
899
|
model=model,
|
|
873
900
|
yes=yes,
|
|
874
901
|
context=context,
|
|
902
|
+
provider_config=provider_config,
|
|
875
903
|
http_client_config=http_client_config,
|
|
876
904
|
)
|
|
877
905
|
return
|
|
@@ -886,6 +914,7 @@ def handle_split_commit_flow(
|
|
|
886
914
|
model=model,
|
|
887
915
|
yes=yes,
|
|
888
916
|
context=context,
|
|
917
|
+
provider_config=provider_config,
|
|
889
918
|
http_client_config=http_client_config,
|
|
890
919
|
)
|
|
891
920
|
return
|
|
@@ -907,6 +936,7 @@ def handle_split_commit_flow(
|
|
|
907
936
|
preferred_commits=preferred_commits,
|
|
908
937
|
model=model,
|
|
909
938
|
context=context,
|
|
939
|
+
provider_config=provider_config,
|
|
910
940
|
http_client_config=http_client_config,
|
|
911
941
|
)
|
|
912
942
|
except SplitPlanningError as exc:
|
|
@@ -920,6 +950,7 @@ def handle_split_commit_flow(
|
|
|
920
950
|
model=model,
|
|
921
951
|
yes=yes,
|
|
922
952
|
context=context,
|
|
953
|
+
provider_config=provider_config,
|
|
923
954
|
http_client_config=http_client_config,
|
|
924
955
|
)
|
|
925
956
|
return
|
|
@@ -936,6 +967,7 @@ def handle_split_commit_flow(
|
|
|
936
967
|
patch_units,
|
|
937
968
|
model=model,
|
|
938
969
|
context=context,
|
|
970
|
+
provider_config=provider_config,
|
|
939
971
|
http_client_config=http_client_config,
|
|
940
972
|
)
|
|
941
973
|
prepared_commits = order_prepared_split_commits(prepared_commits)
|
|
@@ -957,69 +989,94 @@ def handle_split_commit_flow(
|
|
|
957
989
|
console.print(f"[green]{commit_sha[:8]}[/green] {prepared_commit.message}")
|
|
958
990
|
|
|
959
991
|
|
|
960
|
-
@app.command("authenticate")
|
|
961
|
-
@app.command("login",
|
|
992
|
+
@app.command(name="authenticate")
|
|
993
|
+
@app.command(name="login", show=False)
|
|
962
994
|
def authenticate(
|
|
963
|
-
enterprise_domain:
|
|
964
|
-
None,
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
995
|
+
enterprise_domain: Annotated[
|
|
996
|
+
str | None,
|
|
997
|
+
cyclopts.Parameter(
|
|
998
|
+
name="--enterprise-domain",
|
|
999
|
+
help="GitHub Enterprise hostname. Omit for github.com.",
|
|
1000
|
+
),
|
|
1001
|
+
] = None,
|
|
1002
|
+
force: Annotated[
|
|
1003
|
+
bool,
|
|
1004
|
+
cyclopts.Parameter(
|
|
1005
|
+
name="--force",
|
|
1006
|
+
help="Replace cached GitHub Copilot credentials",
|
|
1007
|
+
),
|
|
1008
|
+
] = False,
|
|
971
1009
|
ca_bundle: CaBundleOption = None,
|
|
972
1010
|
insecure: InsecureOption = False,
|
|
973
1011
|
native_tls: NativeTlsOption = False,
|
|
974
1012
|
):
|
|
975
1013
|
"""Authenticate with GitHub Copilot and cache credentials locally."""
|
|
1014
|
+
print_cli_banner()
|
|
976
1015
|
http_client_config = build_http_client_config(
|
|
977
1016
|
ca_bundle=ca_bundle,
|
|
978
1017
|
insecure=insecure,
|
|
979
1018
|
native_tls=native_tls,
|
|
980
1019
|
)
|
|
981
1020
|
try:
|
|
982
|
-
|
|
1021
|
+
copilot.login(
|
|
983
1022
|
enterprise_domain=enterprise_domain,
|
|
984
1023
|
force=force,
|
|
985
1024
|
http_client_config=http_client_config,
|
|
986
1025
|
)
|
|
987
|
-
except
|
|
988
|
-
|
|
989
|
-
raise
|
|
1026
|
+
except copilot.LLMError as exc:
|
|
1027
|
+
print_llm_error("Authentication failed", exc)
|
|
1028
|
+
raise SystemExit(1)
|
|
990
1029
|
|
|
991
1030
|
|
|
992
|
-
@app.command("summary")
|
|
1031
|
+
@app.command(name="summary")
|
|
993
1032
|
def summary(
|
|
1033
|
+
provider: ProviderOption = None,
|
|
1034
|
+
base_url: BaseUrlOption = None,
|
|
1035
|
+
api_key: ApiKeyOption = None,
|
|
994
1036
|
ca_bundle: CaBundleOption = None,
|
|
995
1037
|
insecure: InsecureOption = False,
|
|
996
1038
|
native_tls: NativeTlsOption = False,
|
|
997
1039
|
):
|
|
998
|
-
"""Show the
|
|
1040
|
+
"""Show the configured LLM provider summary."""
|
|
1041
|
+
print_cli_banner()
|
|
999
1042
|
http_client_config = build_http_client_config(
|
|
1000
1043
|
ca_bundle=ca_bundle,
|
|
1001
1044
|
insecure=insecure,
|
|
1002
1045
|
native_tls=native_tls,
|
|
1003
1046
|
)
|
|
1004
1047
|
try:
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1048
|
+
provider_config = providers.resolve_provider_config(
|
|
1049
|
+
provider=provider,
|
|
1050
|
+
base_url=base_url,
|
|
1051
|
+
api_key=api_key,
|
|
1052
|
+
)
|
|
1053
|
+
providers.show_summary(
|
|
1054
|
+
provider_config=provider_config,
|
|
1055
|
+
http_client_config=http_client_config,
|
|
1056
|
+
)
|
|
1057
|
+
except llm.LLMError as exc:
|
|
1058
|
+
print_llm_error("Could not load provider summary", exc)
|
|
1059
|
+
raise SystemExit(1)
|
|
1009
1060
|
|
|
1010
1061
|
|
|
1011
|
-
@app.command("models")
|
|
1062
|
+
@app.command(name="models")
|
|
1012
1063
|
def models_command(
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1064
|
+
provider: ProviderOption = None,
|
|
1065
|
+
base_url: BaseUrlOption = None,
|
|
1066
|
+
api_key: ApiKeyOption = None,
|
|
1067
|
+
vendor: Annotated[
|
|
1068
|
+
str | None,
|
|
1069
|
+
cyclopts.Parameter(
|
|
1070
|
+
name="--vendor",
|
|
1071
|
+
help="Filter listed models by vendor: anthropic, gemini/google, or openai.",
|
|
1072
|
+
),
|
|
1073
|
+
] = None,
|
|
1018
1074
|
ca_bundle: CaBundleOption = None,
|
|
1019
1075
|
insecure: InsecureOption = False,
|
|
1020
1076
|
native_tls: NativeTlsOption = False,
|
|
1021
1077
|
):
|
|
1022
|
-
"""List available
|
|
1078
|
+
"""List available models for the configured LLM provider."""
|
|
1079
|
+
print_cli_banner()
|
|
1023
1080
|
http_client_config = build_http_client_config(
|
|
1024
1081
|
ca_bundle=ca_bundle,
|
|
1025
1082
|
insecure=insecure,
|
|
@@ -1027,42 +1084,64 @@ def models_command(
|
|
|
1027
1084
|
)
|
|
1028
1085
|
|
|
1029
1086
|
try:
|
|
1030
|
-
|
|
1087
|
+
provider_config = providers.resolve_provider_config(
|
|
1088
|
+
provider=provider,
|
|
1089
|
+
base_url=base_url,
|
|
1090
|
+
api_key=api_key,
|
|
1091
|
+
)
|
|
1092
|
+
inventory = providers.get_available_models(
|
|
1093
|
+
provider_config=provider_config,
|
|
1031
1094
|
vendor=vendor,
|
|
1032
1095
|
http_client_config=http_client_config,
|
|
1033
1096
|
)
|
|
1034
1097
|
|
|
1035
|
-
console.print(f"[green]
|
|
1036
|
-
console.print(f"[green]
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1098
|
+
console.print(f"[green]LLM provider:[/green] {provider_config.display_name}")
|
|
1099
|
+
console.print(f"[green]Base URL:[/green] {inventory.base_url}")
|
|
1100
|
+
console.print(f"[green]Model count:[/green] {len(inventory.models)}")
|
|
1101
|
+
llm.print_model_table(
|
|
1102
|
+
inventory.models,
|
|
1103
|
+
title=f"Available {provider_config.display_name} Models",
|
|
1104
|
+
)
|
|
1105
|
+
except llm.LLMError as exc:
|
|
1106
|
+
print_llm_error("Could not load models", exc)
|
|
1107
|
+
raise SystemExit(1)
|
|
1041
1108
|
|
|
1042
1109
|
|
|
1043
|
-
@app.command
|
|
1110
|
+
@app.command
|
|
1044
1111
|
def commit(
|
|
1045
|
-
all_files:
|
|
1046
|
-
|
|
1047
|
-
|
|
1112
|
+
all_files: Annotated[
|
|
1113
|
+
bool,
|
|
1114
|
+
cyclopts.Parameter(
|
|
1115
|
+
name=("--all", "-a"),
|
|
1116
|
+
help="Stage all files before committing",
|
|
1117
|
+
),
|
|
1118
|
+
] = False,
|
|
1048
1119
|
split: SplitOption = False,
|
|
1049
1120
|
split_count: SplitCountOption = None,
|
|
1050
|
-
model:
|
|
1051
|
-
None,
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
yes:
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1121
|
+
model: Annotated[
|
|
1122
|
+
str | None,
|
|
1123
|
+
cyclopts.Parameter(
|
|
1124
|
+
name=("--model", "-m"),
|
|
1125
|
+
help="Model to use for generating commit message",
|
|
1126
|
+
),
|
|
1127
|
+
] = None,
|
|
1128
|
+
yes: Annotated[
|
|
1129
|
+
bool,
|
|
1130
|
+
cyclopts.Parameter(
|
|
1131
|
+
name=("--yes", "-y"),
|
|
1132
|
+
help="Automatically accept the generated commit message",
|
|
1133
|
+
),
|
|
1134
|
+
] = False,
|
|
1135
|
+
context: Annotated[
|
|
1136
|
+
str,
|
|
1137
|
+
cyclopts.Parameter(
|
|
1138
|
+
name=("--context", "-c"),
|
|
1139
|
+
help="Optional user-provided context to guide commit message",
|
|
1140
|
+
),
|
|
1141
|
+
] = "",
|
|
1142
|
+
provider: ProviderOption = None,
|
|
1143
|
+
base_url: BaseUrlOption = None,
|
|
1144
|
+
api_key: ApiKeyOption = None,
|
|
1066
1145
|
ca_bundle: CaBundleOption = None,
|
|
1067
1146
|
insecure: InsecureOption = False,
|
|
1068
1147
|
native_tls: NativeTlsOption = False,
|
|
@@ -1070,25 +1149,37 @@ def commit(
|
|
|
1070
1149
|
"""
|
|
1071
1150
|
Generate commit message based on changes in the current git repository and commit them.
|
|
1072
1151
|
"""
|
|
1152
|
+
print_cli_banner()
|
|
1073
1153
|
try:
|
|
1074
1154
|
repo = GitRepository()
|
|
1075
1155
|
except NotAGitRepositoryError:
|
|
1076
1156
|
console.print("[red]Error: Not in a git repository[/red]")
|
|
1077
|
-
raise
|
|
1157
|
+
raise SystemExit(1)
|
|
1078
1158
|
|
|
1079
1159
|
http_client_config = build_http_client_config(
|
|
1080
1160
|
ca_bundle=ca_bundle,
|
|
1081
1161
|
insecure=insecure,
|
|
1082
1162
|
native_tls=native_tls,
|
|
1083
1163
|
)
|
|
1084
|
-
|
|
1164
|
+
try:
|
|
1165
|
+
provider_config = providers.resolve_provider_config(
|
|
1166
|
+
provider=provider,
|
|
1167
|
+
base_url=base_url,
|
|
1168
|
+
api_key=api_key,
|
|
1169
|
+
)
|
|
1170
|
+
except llm.LLMError as exc:
|
|
1171
|
+
print_llm_error("Could not resolve the LLM provider", exc)
|
|
1172
|
+
raise SystemExit(1)
|
|
1173
|
+
|
|
1174
|
+
if provider_config.provider == "copilot":
|
|
1175
|
+
ensure_copilot_authentication(http_client_config)
|
|
1085
1176
|
|
|
1086
1177
|
# Get initial status
|
|
1087
1178
|
status = repo.get_status()
|
|
1088
1179
|
|
|
1089
1180
|
if not status.files:
|
|
1090
1181
|
console.print("[yellow]No changes to commit.[/yellow]")
|
|
1091
|
-
raise
|
|
1182
|
+
raise SystemExit()
|
|
1092
1183
|
|
|
1093
1184
|
status = stage_changes_for_commit(repo, status, all_files=all_files)
|
|
1094
1185
|
|
|
@@ -1099,13 +1190,14 @@ def commit(
|
|
|
1099
1190
|
|
|
1100
1191
|
normalized_model = normalize_model_name(model)
|
|
1101
1192
|
try:
|
|
1102
|
-
selected_model =
|
|
1193
|
+
selected_model = providers.ensure_model_ready(
|
|
1194
|
+
provider_config=provider_config,
|
|
1103
1195
|
model=normalized_model,
|
|
1104
1196
|
http_client_config=http_client_config,
|
|
1105
1197
|
)
|
|
1106
|
-
except
|
|
1107
|
-
|
|
1108
|
-
raise
|
|
1198
|
+
except llm.LLMError as exc:
|
|
1199
|
+
print_llm_error("Could not select a model", exc)
|
|
1200
|
+
raise SystemExit(1)
|
|
1109
1201
|
|
|
1110
1202
|
display_selected_model(selected_model)
|
|
1111
1203
|
model = selected_model.id
|
|
@@ -1118,6 +1210,7 @@ def commit(
|
|
|
1118
1210
|
model=model,
|
|
1119
1211
|
yes=yes,
|
|
1120
1212
|
context=context,
|
|
1213
|
+
provider_config=provider_config,
|
|
1121
1214
|
http_client_config=http_client_config,
|
|
1122
1215
|
)
|
|
1123
1216
|
return
|
|
@@ -1128,6 +1221,7 @@ def commit(
|
|
|
1128
1221
|
model=model,
|
|
1129
1222
|
yes=yes,
|
|
1130
1223
|
context=context,
|
|
1224
|
+
provider_config=provider_config,
|
|
1131
1225
|
http_client_config=http_client_config,
|
|
1132
1226
|
)
|
|
1133
1227
|
|