git-copilot-commit 0.6.0__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 +122 -124
- git_copilot_commit/git.py +123 -0
- {git_copilot_commit-0.6.0.dist-info → git_copilot_commit-0.6.1.dist-info}/METADATA +39 -5
- {git_copilot_commit-0.6.0.dist-info → git_copilot_commit-0.6.1.dist-info}/RECORD +7 -7
- {git_copilot_commit-0.6.0.dist-info → git_copilot_commit-0.6.1.dist-info}/WHEEL +0 -0
- {git_copilot_commit-0.6.0.dist-info → git_copilot_commit-0.6.1.dist-info}/entry_points.txt +0 -0
- {git_copilot_commit-0.6.0.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 (
|
|
@@ -34,7 +32,7 @@ from .llms import core as llm
|
|
|
34
32
|
from .llms import providers
|
|
35
33
|
|
|
36
34
|
console = Console()
|
|
37
|
-
app =
|
|
35
|
+
app = cyclopts.App(help=__doc__, version=lambda: f"git-copilot-commit {__version__}")
|
|
38
36
|
|
|
39
37
|
COMMIT_MESSAGE_PROMPT_FILENAME = "commit-message-generator-prompt.md"
|
|
40
38
|
SPLIT_COMMIT_PLANNER_PROMPT_FILENAME = "split-commit-planner-prompt.md"
|
|
@@ -57,28 +55,31 @@ NATIVE_TLS_HELP = (
|
|
|
57
55
|
|
|
58
56
|
CaBundleOption = Annotated[
|
|
59
57
|
str | None,
|
|
60
|
-
|
|
58
|
+
cyclopts.Parameter(name="--ca-bundle", help=CA_BUNDLE_HELP),
|
|
61
59
|
]
|
|
62
60
|
InsecureOption = Annotated[
|
|
63
61
|
bool,
|
|
64
|
-
|
|
62
|
+
cyclopts.Parameter(name="--insecure", help="Disable SSL certificate verification."),
|
|
65
63
|
]
|
|
66
64
|
NativeTlsOption = Annotated[
|
|
67
65
|
bool,
|
|
68
|
-
|
|
66
|
+
cyclopts.Parameter(
|
|
67
|
+
name="--native-tls",
|
|
68
|
+
negative="--no-native-tls",
|
|
69
|
+
help=NATIVE_TLS_HELP,
|
|
70
|
+
),
|
|
69
71
|
]
|
|
70
72
|
ProviderOption = Annotated[
|
|
71
73
|
str | None,
|
|
72
|
-
|
|
73
|
-
"--provider",
|
|
74
|
+
cyclopts.Parameter(
|
|
75
|
+
name="--provider",
|
|
74
76
|
help="LLM provider to use: copilot or openai.",
|
|
75
77
|
),
|
|
76
78
|
]
|
|
77
79
|
BaseUrlOption = Annotated[
|
|
78
80
|
str | None,
|
|
79
|
-
|
|
80
|
-
"--base-url",
|
|
81
|
-
metavar="URL",
|
|
81
|
+
cyclopts.Parameter(
|
|
82
|
+
name="--base-url",
|
|
82
83
|
help=(
|
|
83
84
|
"Base URL for an OpenAI-compatible provider, for example "
|
|
84
85
|
"http://127.0.0.1:11434/v1."
|
|
@@ -87,8 +88,8 @@ BaseUrlOption = Annotated[
|
|
|
87
88
|
]
|
|
88
89
|
ApiKeyOption = Annotated[
|
|
89
90
|
str | None,
|
|
90
|
-
|
|
91
|
-
"--api-key",
|
|
91
|
+
cyclopts.Parameter(
|
|
92
|
+
name="--api-key",
|
|
92
93
|
help="API key for an OpenAI-compatible provider. Omit when the server does not require one.",
|
|
93
94
|
),
|
|
94
95
|
]
|
|
@@ -96,8 +97,8 @@ ApiKeyOption = Annotated[
|
|
|
96
97
|
|
|
97
98
|
SplitOption = Annotated[
|
|
98
99
|
bool,
|
|
99
|
-
|
|
100
|
-
"--split",
|
|
100
|
+
cyclopts.Parameter(
|
|
101
|
+
name="--split",
|
|
101
102
|
help=(
|
|
102
103
|
"Split staged hunks into multiple commits automatically. Pass "
|
|
103
104
|
"`--split=N` to express a preference for N commits."
|
|
@@ -106,10 +107,10 @@ SplitOption = Annotated[
|
|
|
106
107
|
]
|
|
107
108
|
SplitCountOption = Annotated[
|
|
108
109
|
int | None,
|
|
109
|
-
|
|
110
|
-
"--split-count",
|
|
111
|
-
|
|
112
|
-
|
|
110
|
+
cyclopts.Parameter(
|
|
111
|
+
name="--split-count",
|
|
112
|
+
show=False,
|
|
113
|
+
validator=cyclopts.validators.Number(gte=1),
|
|
113
114
|
),
|
|
114
115
|
]
|
|
115
116
|
|
|
@@ -225,39 +226,13 @@ def order_prepared_split_commits(
|
|
|
225
226
|
def run(args: Sequence[str] | None = None) -> None:
|
|
226
227
|
"""Run the CLI entrypoint with argument normalization."""
|
|
227
228
|
raw_args = list(args) if args is not None else sys.argv[1:]
|
|
228
|
-
|
|
229
|
-
command.main(
|
|
230
|
-
args=preprocess_cli_args(raw_args),
|
|
231
|
-
prog_name=Path(sys.argv[0]).name,
|
|
232
|
-
)
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
def version_callback(value: bool):
|
|
236
|
-
if value:
|
|
237
|
-
rich.print(f"git-copilot-commit [bold yellow]{__version__}[/]")
|
|
238
|
-
raise typer.Exit()
|
|
229
|
+
app(preprocess_cli_args(raw_args))
|
|
239
230
|
|
|
240
231
|
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
False, "--version", callback=version_callback, help="Show version and exit"
|
|
246
|
-
),
|
|
247
|
-
):
|
|
248
|
-
"""
|
|
249
|
-
Automatically commit changes in the current git repository.
|
|
250
|
-
"""
|
|
251
|
-
if ctx.invoked_subcommand is None:
|
|
252
|
-
# Show help when no command is provided
|
|
253
|
-
console.print(ctx.get_help())
|
|
254
|
-
raise typer.Exit()
|
|
255
|
-
else:
|
|
256
|
-
# Don't show version for print command to avoid interfering with pipes
|
|
257
|
-
if ctx.invoked_subcommand != "echo":
|
|
258
|
-
console.print(
|
|
259
|
-
f"[bold]{(__package__ or 'git_copilot_commit').replace('_', '-')}[/] - [bold green]v{__version__}[/]\n"
|
|
260
|
-
)
|
|
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")
|
|
261
236
|
|
|
262
237
|
|
|
263
238
|
def get_prompt_locations(filename: str):
|
|
@@ -280,7 +255,7 @@ def resolve_prompt_file() -> Path | None:
|
|
|
280
255
|
console.print(
|
|
281
256
|
f"[red]Configured default prompt file in {settings.config_file} is invalid.[/red]"
|
|
282
257
|
)
|
|
283
|
-
raise
|
|
258
|
+
raise SystemExit(1)
|
|
284
259
|
|
|
285
260
|
if configured_prompt_file is None:
|
|
286
261
|
return None
|
|
@@ -298,7 +273,7 @@ def load_system_prompt() -> str:
|
|
|
298
273
|
console.print(
|
|
299
274
|
f"[red]Error reading prompt file {resolved_prompt_file}: {exc}[/red]"
|
|
300
275
|
)
|
|
301
|
-
raise
|
|
276
|
+
raise SystemExit(1)
|
|
302
277
|
|
|
303
278
|
return load_named_prompt(COMMIT_MESSAGE_PROMPT_FILENAME)
|
|
304
279
|
|
|
@@ -312,7 +287,7 @@ def load_named_prompt(filename: str) -> str:
|
|
|
312
287
|
continue
|
|
313
288
|
|
|
314
289
|
console.print(f"[red]Error: Prompt file {filename} not found in any location[/red]")
|
|
315
|
-
raise
|
|
290
|
+
raise SystemExit(1)
|
|
316
291
|
|
|
317
292
|
|
|
318
293
|
def build_http_client_config(
|
|
@@ -357,7 +332,7 @@ def build_commit_message_prompt(
|
|
|
357
332
|
"""Build the prompt used to generate a commit message."""
|
|
358
333
|
if not status.has_staged_changes:
|
|
359
334
|
console.print("[red]No staged changes to commit.[/red]")
|
|
360
|
-
raise
|
|
335
|
+
raise SystemExit()
|
|
361
336
|
|
|
362
337
|
prompt_parts = [
|
|
363
338
|
"`git status`:\n",
|
|
@@ -383,7 +358,6 @@ def normalize_model_name(model: str | None) -> str | None:
|
|
|
383
358
|
if model is not None:
|
|
384
359
|
for prefix in (
|
|
385
360
|
"copilot/",
|
|
386
|
-
"openai/",
|
|
387
361
|
"openai-compatible/",
|
|
388
362
|
):
|
|
389
363
|
if model.startswith(prefix):
|
|
@@ -506,13 +480,13 @@ def commit_with_retry_no_verify(
|
|
|
506
480
|
"Retry commit with [bold]`-n`[/] (skip hooks) using the same commit message?",
|
|
507
481
|
default=True,
|
|
508
482
|
):
|
|
509
|
-
raise
|
|
483
|
+
raise SystemExit(1)
|
|
510
484
|
|
|
511
485
|
try:
|
|
512
486
|
return repo.commit(message, use_editor=use_editor, no_verify=True, env=env)
|
|
513
487
|
except GitError as retry_error:
|
|
514
488
|
console.print(f"[red]Commit with -n failed: {retry_error}[/red]")
|
|
515
|
-
raise
|
|
489
|
+
raise SystemExit(1)
|
|
516
490
|
|
|
517
491
|
|
|
518
492
|
def ensure_copilot_authentication(
|
|
@@ -534,7 +508,7 @@ def ensure_copilot_authentication(
|
|
|
534
508
|
)
|
|
535
509
|
except copilot.LLMError as exc:
|
|
536
510
|
print_llm_error("Authentication failed", exc)
|
|
537
|
-
raise
|
|
511
|
+
raise SystemExit(1)
|
|
538
512
|
|
|
539
513
|
|
|
540
514
|
def stage_changes_for_commit(
|
|
@@ -590,7 +564,7 @@ def request_commit_message(
|
|
|
590
564
|
)
|
|
591
565
|
except llm.LLMError as exc:
|
|
592
566
|
print_llm_error("Could not generate a commit message", exc)
|
|
593
|
-
raise
|
|
567
|
+
raise SystemExit(1)
|
|
594
568
|
|
|
595
569
|
|
|
596
570
|
def request_split_commit_plan(
|
|
@@ -626,7 +600,7 @@ def request_split_commit_plan(
|
|
|
626
600
|
except llm.LLMError as exc:
|
|
627
601
|
if not should_retry_with_compact_prompt(exc):
|
|
628
602
|
print_llm_error("Could not generate a split commit plan", exc)
|
|
629
|
-
raise
|
|
603
|
+
raise SystemExit(1)
|
|
630
604
|
|
|
631
605
|
console.print(
|
|
632
606
|
"[yellow]Staged patch units exceeded the model context window; retrying split planning with summaries only.[/yellow]"
|
|
@@ -658,7 +632,7 @@ def request_split_commit_plan(
|
|
|
658
632
|
)
|
|
659
633
|
except llm.LLMError as exc:
|
|
660
634
|
print_llm_error("Could not generate a split commit plan", exc)
|
|
661
|
-
raise
|
|
635
|
+
raise SystemExit(1)
|
|
662
636
|
|
|
663
637
|
return parse_split_plan_response(
|
|
664
638
|
response,
|
|
@@ -700,7 +674,7 @@ def request_split_commit_messages(
|
|
|
700
674
|
return prepared_commits
|
|
701
675
|
except llm.LLMError as exc:
|
|
702
676
|
print_llm_error("Could not generate split commit messages", exc)
|
|
703
|
-
raise
|
|
677
|
+
raise SystemExit(1)
|
|
704
678
|
|
|
705
679
|
|
|
706
680
|
def confirm_split_commit_count(
|
|
@@ -730,7 +704,7 @@ def confirm_split_commit_count(
|
|
|
730
704
|
return plan
|
|
731
705
|
|
|
732
706
|
console.print("Split commit plan cancelled.")
|
|
733
|
-
raise
|
|
707
|
+
raise SystemExit()
|
|
734
708
|
|
|
735
709
|
|
|
736
710
|
def display_commit_message(commit_message: str) -> None:
|
|
@@ -769,7 +743,7 @@ def execute_commit_action(
|
|
|
769
743
|
if yes:
|
|
770
744
|
return commit_with_retry_no_verify(repo, commit_message)
|
|
771
745
|
|
|
772
|
-
choice =
|
|
746
|
+
choice = Prompt.ask(
|
|
773
747
|
"Choose action: (c)ommit, (e)dit message, (q)uit",
|
|
774
748
|
default="c",
|
|
775
749
|
show_default=True,
|
|
@@ -777,7 +751,7 @@ def execute_commit_action(
|
|
|
777
751
|
|
|
778
752
|
if choice == "q":
|
|
779
753
|
console.print("Commit cancelled.")
|
|
780
|
-
raise
|
|
754
|
+
raise SystemExit()
|
|
781
755
|
if choice == "e":
|
|
782
756
|
console.print("[cyan]Opening git editor...[/cyan]")
|
|
783
757
|
return commit_with_retry_no_verify(repo, commit_message, use_editor=True)
|
|
@@ -785,7 +759,7 @@ def execute_commit_action(
|
|
|
785
759
|
return commit_with_retry_no_verify(repo, commit_message)
|
|
786
760
|
|
|
787
761
|
console.print("Invalid choice. Commit cancelled.")
|
|
788
|
-
raise
|
|
762
|
+
raise SystemExit()
|
|
789
763
|
|
|
790
764
|
|
|
791
765
|
def execute_split_commit_plan(
|
|
@@ -797,7 +771,7 @@ def execute_split_commit_plan(
|
|
|
797
771
|
"""Run the split-commit plan against temporary alternate indexes."""
|
|
798
772
|
use_editor = False
|
|
799
773
|
if not yes:
|
|
800
|
-
choice =
|
|
774
|
+
choice = Prompt.ask(
|
|
801
775
|
"Choose action: (c)ommit all, (e)dit each message, (q)uit",
|
|
802
776
|
default="c",
|
|
803
777
|
show_default=True,
|
|
@@ -805,12 +779,12 @@ def execute_split_commit_plan(
|
|
|
805
779
|
|
|
806
780
|
if choice == "q":
|
|
807
781
|
console.print("Commit cancelled.")
|
|
808
|
-
raise
|
|
782
|
+
raise SystemExit()
|
|
809
783
|
if choice == "e":
|
|
810
784
|
use_editor = True
|
|
811
785
|
elif choice != "c":
|
|
812
786
|
console.print("Invalid choice. Commit cancelled.")
|
|
813
|
-
raise
|
|
787
|
+
raise SystemExit()
|
|
814
788
|
|
|
815
789
|
execution_state = SplitCommitExecutionState(
|
|
816
790
|
original_head_sha=repo.get_head_sha() if repo.has_commit("HEAD") else None,
|
|
@@ -840,16 +814,19 @@ def execute_split_commit_plan(
|
|
|
840
814
|
console.print(
|
|
841
815
|
f"[red]Failed to apply the planned changes for commit {index}: {exc}[/red]"
|
|
842
816
|
)
|
|
843
|
-
raise
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
repo
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
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
|
+
)
|
|
851
826
|
)
|
|
852
|
-
|
|
827
|
+
except GitError as exc:
|
|
828
|
+
console.print(f"[red]Failed to create commit {index}: {exc}[/red]")
|
|
829
|
+
raise SystemExit(1)
|
|
853
830
|
except BaseException:
|
|
854
831
|
try:
|
|
855
832
|
if execution_state.original_head_sha is not None:
|
|
@@ -1012,22 +989,29 @@ def handle_split_commit_flow(
|
|
|
1012
989
|
console.print(f"[green]{commit_sha[:8]}[/green] {prepared_commit.message}")
|
|
1013
990
|
|
|
1014
991
|
|
|
1015
|
-
@app.command("authenticate")
|
|
1016
|
-
@app.command("login",
|
|
992
|
+
@app.command(name="authenticate")
|
|
993
|
+
@app.command(name="login", show=False)
|
|
1017
994
|
def authenticate(
|
|
1018
|
-
enterprise_domain:
|
|
1019
|
-
None,
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
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,
|
|
1026
1009
|
ca_bundle: CaBundleOption = None,
|
|
1027
1010
|
insecure: InsecureOption = False,
|
|
1028
1011
|
native_tls: NativeTlsOption = False,
|
|
1029
1012
|
):
|
|
1030
1013
|
"""Authenticate with GitHub Copilot and cache credentials locally."""
|
|
1014
|
+
print_cli_banner()
|
|
1031
1015
|
http_client_config = build_http_client_config(
|
|
1032
1016
|
ca_bundle=ca_bundle,
|
|
1033
1017
|
insecure=insecure,
|
|
@@ -1041,10 +1025,10 @@ def authenticate(
|
|
|
1041
1025
|
)
|
|
1042
1026
|
except copilot.LLMError as exc:
|
|
1043
1027
|
print_llm_error("Authentication failed", exc)
|
|
1044
|
-
raise
|
|
1028
|
+
raise SystemExit(1)
|
|
1045
1029
|
|
|
1046
1030
|
|
|
1047
|
-
@app.command("summary")
|
|
1031
|
+
@app.command(name="summary")
|
|
1048
1032
|
def summary(
|
|
1049
1033
|
provider: ProviderOption = None,
|
|
1050
1034
|
base_url: BaseUrlOption = None,
|
|
@@ -1054,6 +1038,7 @@ def summary(
|
|
|
1054
1038
|
native_tls: NativeTlsOption = False,
|
|
1055
1039
|
):
|
|
1056
1040
|
"""Show the configured LLM provider summary."""
|
|
1041
|
+
print_cli_banner()
|
|
1057
1042
|
http_client_config = build_http_client_config(
|
|
1058
1043
|
ca_bundle=ca_bundle,
|
|
1059
1044
|
insecure=insecure,
|
|
@@ -1071,24 +1056,27 @@ def summary(
|
|
|
1071
1056
|
)
|
|
1072
1057
|
except llm.LLMError as exc:
|
|
1073
1058
|
print_llm_error("Could not load provider summary", exc)
|
|
1074
|
-
raise
|
|
1059
|
+
raise SystemExit(1)
|
|
1075
1060
|
|
|
1076
1061
|
|
|
1077
|
-
@app.command("models")
|
|
1062
|
+
@app.command(name="models")
|
|
1078
1063
|
def models_command(
|
|
1079
1064
|
provider: ProviderOption = None,
|
|
1080
1065
|
base_url: BaseUrlOption = None,
|
|
1081
1066
|
api_key: ApiKeyOption = None,
|
|
1082
|
-
vendor:
|
|
1083
|
-
None,
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
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,
|
|
1087
1074
|
ca_bundle: CaBundleOption = None,
|
|
1088
1075
|
insecure: InsecureOption = False,
|
|
1089
1076
|
native_tls: NativeTlsOption = False,
|
|
1090
1077
|
):
|
|
1091
1078
|
"""List available models for the configured LLM provider."""
|
|
1079
|
+
print_cli_banner()
|
|
1092
1080
|
http_client_config = build_http_client_config(
|
|
1093
1081
|
ca_bundle=ca_bundle,
|
|
1094
1082
|
insecure=insecure,
|
|
@@ -1116,32 +1104,41 @@ def models_command(
|
|
|
1116
1104
|
)
|
|
1117
1105
|
except llm.LLMError as exc:
|
|
1118
1106
|
print_llm_error("Could not load models", exc)
|
|
1119
|
-
raise
|
|
1107
|
+
raise SystemExit(1)
|
|
1120
1108
|
|
|
1121
1109
|
|
|
1122
|
-
@app.command
|
|
1110
|
+
@app.command
|
|
1123
1111
|
def commit(
|
|
1124
|
-
all_files:
|
|
1125
|
-
|
|
1126
|
-
|
|
1112
|
+
all_files: Annotated[
|
|
1113
|
+
bool,
|
|
1114
|
+
cyclopts.Parameter(
|
|
1115
|
+
name=("--all", "-a"),
|
|
1116
|
+
help="Stage all files before committing",
|
|
1117
|
+
),
|
|
1118
|
+
] = False,
|
|
1127
1119
|
split: SplitOption = False,
|
|
1128
1120
|
split_count: SplitCountOption = None,
|
|
1129
|
-
model:
|
|
1130
|
-
None,
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
yes:
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
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
|
+
] = "",
|
|
1145
1142
|
provider: ProviderOption = None,
|
|
1146
1143
|
base_url: BaseUrlOption = None,
|
|
1147
1144
|
api_key: ApiKeyOption = None,
|
|
@@ -1152,11 +1149,12 @@ def commit(
|
|
|
1152
1149
|
"""
|
|
1153
1150
|
Generate commit message based on changes in the current git repository and commit them.
|
|
1154
1151
|
"""
|
|
1152
|
+
print_cli_banner()
|
|
1155
1153
|
try:
|
|
1156
1154
|
repo = GitRepository()
|
|
1157
1155
|
except NotAGitRepositoryError:
|
|
1158
1156
|
console.print("[red]Error: Not in a git repository[/red]")
|
|
1159
|
-
raise
|
|
1157
|
+
raise SystemExit(1)
|
|
1160
1158
|
|
|
1161
1159
|
http_client_config = build_http_client_config(
|
|
1162
1160
|
ca_bundle=ca_bundle,
|
|
@@ -1171,7 +1169,7 @@ def commit(
|
|
|
1171
1169
|
)
|
|
1172
1170
|
except llm.LLMError as exc:
|
|
1173
1171
|
print_llm_error("Could not resolve the LLM provider", exc)
|
|
1174
|
-
raise
|
|
1172
|
+
raise SystemExit(1)
|
|
1175
1173
|
|
|
1176
1174
|
if provider_config.provider == "copilot":
|
|
1177
1175
|
ensure_copilot_authentication(http_client_config)
|
|
@@ -1181,7 +1179,7 @@ def commit(
|
|
|
1181
1179
|
|
|
1182
1180
|
if not status.files:
|
|
1183
1181
|
console.print("[yellow]No changes to commit.[/yellow]")
|
|
1184
|
-
raise
|
|
1182
|
+
raise SystemExit()
|
|
1185
1183
|
|
|
1186
1184
|
status = stage_changes_for_commit(repo, status, all_files=all_files)
|
|
1187
1185
|
|
|
@@ -1199,7 +1197,7 @@ def commit(
|
|
|
1199
1197
|
)
|
|
1200
1198
|
except llm.LLMError as exc:
|
|
1201
1199
|
print_llm_error("Could not select a model", exc)
|
|
1202
|
-
raise
|
|
1200
|
+
raise SystemExit(1)
|
|
1203
1201
|
|
|
1204
1202
|
display_selected_model(selected_model)
|
|
1205
1203
|
model = selected_model.id
|
git_copilot_commit/git.py
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import os
|
|
2
|
+
import shlex
|
|
2
3
|
import subprocess
|
|
3
4
|
import tempfile
|
|
4
5
|
from contextlib import contextmanager
|
|
@@ -352,6 +353,15 @@ class GitRepository:
|
|
|
352
353
|
)
|
|
353
354
|
raise GitCommandError(f"Git command failed: git update-ref -d {ref}")
|
|
354
355
|
|
|
356
|
+
def update_ref(
|
|
357
|
+
self, ref: str, new_value: str, *, old_value: str | None = None
|
|
358
|
+
) -> None:
|
|
359
|
+
"""Update a ref to point at a new object id."""
|
|
360
|
+
args = ["update-ref", ref, new_value]
|
|
361
|
+
if old_value is not None:
|
|
362
|
+
args.append(old_value)
|
|
363
|
+
self._run_git_command(args)
|
|
364
|
+
|
|
355
365
|
def create_alternate_index(self, from_ref: str = "HEAD") -> AlternateGitIndex:
|
|
356
366
|
"""Create a temporary git index initialized from the provided ref."""
|
|
357
367
|
fd, index_path = tempfile.mkstemp(prefix="git-copilot-commit-", suffix=".index")
|
|
@@ -382,6 +392,15 @@ class GitRepository:
|
|
|
382
392
|
"""Initialize an alternate index with an empty tree."""
|
|
383
393
|
self._run_git_command(["read-tree", "--empty"], env=index.env)
|
|
384
394
|
|
|
395
|
+
def write_tree(self, *, env: Mapping[str, str] | None = None) -> str:
|
|
396
|
+
"""Write the current index as a tree object and return its SHA."""
|
|
397
|
+
result = self._run_git_command(["write-tree"], env=env)
|
|
398
|
+
tree_sha = result.stdout.strip()
|
|
399
|
+
if tree_sha:
|
|
400
|
+
return tree_sha
|
|
401
|
+
|
|
402
|
+
raise GitCommandError("Git command failed: git write-tree")
|
|
403
|
+
|
|
385
404
|
def apply_patch(
|
|
386
405
|
self,
|
|
387
406
|
patch: str,
|
|
@@ -422,6 +441,110 @@ class GitRepository:
|
|
|
422
441
|
"""Validate whether a cached patch can be applied to an alternate index."""
|
|
423
442
|
self.check_patch(patch, cached=True, env=index.env)
|
|
424
443
|
|
|
444
|
+
def edit_commit_message(
|
|
445
|
+
self,
|
|
446
|
+
message: str,
|
|
447
|
+
*,
|
|
448
|
+
env: Mapping[str, str] | None = None,
|
|
449
|
+
) -> str:
|
|
450
|
+
"""Open git's configured editor with a starting message."""
|
|
451
|
+
with tempfile.NamedTemporaryFile(
|
|
452
|
+
mode="w",
|
|
453
|
+
suffix=".txt",
|
|
454
|
+
delete=False,
|
|
455
|
+
encoding="utf-8",
|
|
456
|
+
) as f:
|
|
457
|
+
f.write(message)
|
|
458
|
+
temp_path = Path(f.name)
|
|
459
|
+
|
|
460
|
+
try:
|
|
461
|
+
editor = self._run_git_command(
|
|
462
|
+
["var", "GIT_EDITOR"], env=env
|
|
463
|
+
).stdout.strip()
|
|
464
|
+
if not editor:
|
|
465
|
+
raise GitCommandError("Git command failed: git var GIT_EDITOR")
|
|
466
|
+
|
|
467
|
+
editor_command = f"{editor} {shlex.quote(str(temp_path))}"
|
|
468
|
+
try:
|
|
469
|
+
# Git editor configuration is shell syntax, so execute it the same way.
|
|
470
|
+
subprocess.run(
|
|
471
|
+
editor_command,
|
|
472
|
+
cwd=self.repo_path,
|
|
473
|
+
timeout=self.timeout,
|
|
474
|
+
check=True,
|
|
475
|
+
env=self._build_env(env),
|
|
476
|
+
shell=True,
|
|
477
|
+
)
|
|
478
|
+
except subprocess.CalledProcessError:
|
|
479
|
+
raise GitCommandError(f"Git editor failed: {editor_command}")
|
|
480
|
+
except subprocess.TimeoutExpired:
|
|
481
|
+
raise GitCommandError(f"Git editor timed out: {editor_command}")
|
|
482
|
+
|
|
483
|
+
edited_message = temp_path.read_text(encoding="utf-8").strip()
|
|
484
|
+
if edited_message:
|
|
485
|
+
return edited_message
|
|
486
|
+
|
|
487
|
+
raise GitCommandError("Commit message cannot be empty")
|
|
488
|
+
finally:
|
|
489
|
+
temp_path.unlink(missing_ok=True)
|
|
490
|
+
|
|
491
|
+
def create_commit_object(
|
|
492
|
+
self,
|
|
493
|
+
tree_sha: str,
|
|
494
|
+
*,
|
|
495
|
+
message: str | None = None,
|
|
496
|
+
message_file: Path | None = None,
|
|
497
|
+
parent_refs: tuple[str, ...] = (),
|
|
498
|
+
env: Mapping[str, str] | None = None,
|
|
499
|
+
) -> str:
|
|
500
|
+
"""Create a commit object without updating refs."""
|
|
501
|
+
if (message is None) == (message_file is None):
|
|
502
|
+
raise ValueError("Exactly one of message or message_file is required")
|
|
503
|
+
|
|
504
|
+
args = ["commit-tree", tree_sha]
|
|
505
|
+
for parent_ref in parent_refs:
|
|
506
|
+
args.extend(["-p", parent_ref])
|
|
507
|
+
|
|
508
|
+
if message_file is not None:
|
|
509
|
+
args.extend(["-F", str(message_file)])
|
|
510
|
+
result = self._run_git_command(args, env=env)
|
|
511
|
+
else:
|
|
512
|
+
result = self._run_git_command(args, env=env, input_text=message)
|
|
513
|
+
|
|
514
|
+
commit_sha = result.stdout.strip()
|
|
515
|
+
if commit_sha:
|
|
516
|
+
return commit_sha
|
|
517
|
+
|
|
518
|
+
raise GitCommandError(f"Git command failed: git {' '.join(args)}")
|
|
519
|
+
|
|
520
|
+
def advance_head_to_commit(self, commit_sha: str) -> None:
|
|
521
|
+
"""Move HEAD or the current branch to a commit without touching the real index."""
|
|
522
|
+
ref = self.get_symbolic_head_ref() or "HEAD"
|
|
523
|
+
self.update_ref(ref, commit_sha)
|
|
524
|
+
|
|
525
|
+
def create_commit_from_index(
|
|
526
|
+
self,
|
|
527
|
+
message: str,
|
|
528
|
+
*,
|
|
529
|
+
index: AlternateGitIndex,
|
|
530
|
+
use_editor: bool = False,
|
|
531
|
+
) -> str:
|
|
532
|
+
"""Create a commit from an alternate index using plumbing commands only."""
|
|
533
|
+
commit_message = message
|
|
534
|
+
if use_editor:
|
|
535
|
+
commit_message = self.edit_commit_message(message, env=index.env)
|
|
536
|
+
|
|
537
|
+
parent_refs = ("HEAD",) if self.has_commit("HEAD") else ()
|
|
538
|
+
tree_sha = self.write_tree(env=index.env)
|
|
539
|
+
commit_sha = self.create_commit_object(
|
|
540
|
+
tree_sha,
|
|
541
|
+
message=commit_message,
|
|
542
|
+
parent_refs=parent_refs,
|
|
543
|
+
env=index.env,
|
|
544
|
+
)
|
|
545
|
+
self.advance_head_to_commit(commit_sha)
|
|
546
|
+
return commit_sha
|
|
547
|
+
|
|
425
548
|
def commit(
|
|
426
549
|
self,
|
|
427
550
|
message: str | None = None,
|
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: git-copilot-commit
|
|
3
|
-
Version: 0.6.
|
|
3
|
+
Version: 0.6.1
|
|
4
4
|
Summary: Automatically generate and commit changes using GitHub Copilot or OpenAI-compatible LLMs
|
|
5
5
|
Author-email: Dheepak Krishnamurthy <1813121+kdheepak@users.noreply.github.com>
|
|
6
6
|
License-File: LICENSE
|
|
7
7
|
Requires-Python: >=3.12
|
|
8
|
+
Requires-Dist: cyclopts>=4.11.0
|
|
8
9
|
Requires-Dist: httpx>=0.28.0
|
|
9
10
|
Requires-Dist: platformdirs>=4.0.0
|
|
10
11
|
Requires-Dist: rich>=14.0.0
|
|
11
12
|
Requires-Dist: truststore>=0.10.4
|
|
12
|
-
Requires-Dist: typer>=0.16.0
|
|
13
13
|
Description-Content-Type: text/markdown
|
|
14
14
|
|
|
15
15
|
# `git-copilot-commit`
|
|
@@ -96,7 +96,11 @@ git-copilot-commit --help
|
|
|
96
96
|
|
|
97
97
|
### OpenAI-compatible provider
|
|
98
98
|
|
|
99
|
-
1. Point the CLI at your server
|
|
99
|
+
1. Point the CLI at your server.
|
|
100
|
+
|
|
101
|
+
The base URL can be either the provider root such as `http://127.0.0.1:11434/v1`
|
|
102
|
+
or the full chat completions endpoint such as
|
|
103
|
+
`http://127.0.0.1:11434/v1/chat/completions`.
|
|
100
104
|
|
|
101
105
|
```bash
|
|
102
106
|
uvx git-copilot-commit models \
|
|
@@ -104,7 +108,7 @@ git-copilot-commit --help
|
|
|
104
108
|
--base-url http://127.0.0.1:11434/v1
|
|
105
109
|
```
|
|
106
110
|
|
|
107
|
-
2. Generate and commit
|
|
111
|
+
2. Generate and commit.
|
|
108
112
|
|
|
109
113
|
```bash
|
|
110
114
|
uvx git-copilot-commit commit \
|
|
@@ -115,6 +119,17 @@ git-copilot-commit --help
|
|
|
115
119
|
|
|
116
120
|
If your server requires an API key, also pass `--api-key ...` or set `OPENAI_API_KEY`.
|
|
117
121
|
|
|
122
|
+
3. Example: use a self-hosted GPT-OSS model:
|
|
123
|
+
|
|
124
|
+
```bash
|
|
125
|
+
uvx git-copilot-commit commit \
|
|
126
|
+
--provider openai \
|
|
127
|
+
--base-url http://example.com:8001/v1/chat/completions \
|
|
128
|
+
--model openai/gpt-oss-120b
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
Model ids with slashes such as `openai/gpt-oss-120b` are supported.
|
|
132
|
+
|
|
118
133
|
## Usage
|
|
119
134
|
|
|
120
135
|
### Commit changes
|
|
@@ -175,6 +190,15 @@ uvx git-copilot-commit commit \
|
|
|
175
190
|
--model your-model-id
|
|
176
191
|
```
|
|
177
192
|
|
|
193
|
+
Use a self-hosted GPT-OSS endpoint:
|
|
194
|
+
|
|
195
|
+
```bash
|
|
196
|
+
uvx git-copilot-commit commit \
|
|
197
|
+
--provider openai \
|
|
198
|
+
--base-url http://example.com:8001/v1/chat/completions \
|
|
199
|
+
--model openai/gpt-oss-120b
|
|
200
|
+
```
|
|
201
|
+
|
|
178
202
|
Split staged hunks into separate commits:
|
|
179
203
|
|
|
180
204
|
```bash
|
|
@@ -232,11 +256,21 @@ git ai-commit --all --yes --model claude-3.5-sonnet
|
|
|
232
256
|
You can also set provider defaults with environment variables:
|
|
233
257
|
|
|
234
258
|
```bash
|
|
235
|
-
export
|
|
259
|
+
export GIT_COPILOT_COMMIT_PROVIDER=openai
|
|
260
|
+
export GIT_COPILOT_COMMIT_BASE_URL=http://127.0.0.1:11434/v1
|
|
261
|
+
export GIT_COPILOT_COMMIT_API_KEY=...
|
|
236
262
|
export OPENAI_API_KEY=...
|
|
237
263
|
git ai-commit --provider openai --model your-model-id
|
|
238
264
|
```
|
|
239
265
|
|
|
266
|
+
For example:
|
|
267
|
+
|
|
268
|
+
```bash
|
|
269
|
+
export GIT_COPILOT_COMMIT_PROVIDER=openai
|
|
270
|
+
export GIT_COPILOT_COMMIT_BASE_URL=http://example.com:8001/v1
|
|
271
|
+
git ai-commit --model openai/gpt-oss-120b
|
|
272
|
+
```
|
|
273
|
+
|
|
240
274
|
> [!TIP]
|
|
241
275
|
>
|
|
242
276
|
> Show more context in diffs by running the following command:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
git_copilot_commit/__init__.py,sha256=v3x5oBkxwKJEZLv62QqSmP3iqNKLtZgrWZfH8eFzlQg,60
|
|
2
|
-
git_copilot_commit/cli.py,sha256=
|
|
3
|
-
git_copilot_commit/git.py,sha256=
|
|
2
|
+
git_copilot_commit/cli.py,sha256=x5p_f71DhnYEED8-9rvJKqkjTbcwfXlN3zIvfxmYQfU,38011
|
|
3
|
+
git_copilot_commit/git.py,sha256=EbXiicWygSlMM-F6rY4LCkchwCvsFTziJcdUZM-1vnw,21059
|
|
4
4
|
git_copilot_commit/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
5
5
|
git_copilot_commit/settings.py,sha256=WrM10_J3F7QBfOVmPDWpNZrNHhmZSeN-9FqQZxgdWvQ,3730
|
|
6
6
|
git_copilot_commit/split_commits.py,sha256=rHyuVJggjmYjbva7BVqsM3aZRxUgOKkuZtxxvFRcu6Q,15060
|
|
@@ -12,8 +12,8 @@ git_copilot_commit/llms/openai_api.py,sha256=wkadrdSDadbLRaLWEpOhsYYjrbtEYg14CFb
|
|
|
12
12
|
git_copilot_commit/llms/providers.py,sha256=rA2mdCQR8pfVDhwV5mqpdHlT1nxkWtowQ1Smt0zXCa0,11565
|
|
13
13
|
git_copilot_commit/prompts/commit-message-generator-prompt.md,sha256=3Dz8GCdumFNAtXOdTlpRtgBnmX0WyrPL6tdfMgNyYiE,2411
|
|
14
14
|
git_copilot_commit/prompts/split-commit-planner-prompt.md,sha256=tDI0v1udOhkRQM31M892FMzcPMYHExnU0fjTGia1V2k,1510
|
|
15
|
-
git_copilot_commit-0.6.
|
|
16
|
-
git_copilot_commit-0.6.
|
|
17
|
-
git_copilot_commit-0.6.
|
|
18
|
-
git_copilot_commit-0.6.
|
|
19
|
-
git_copilot_commit-0.6.
|
|
15
|
+
git_copilot_commit-0.6.1.dist-info/METADATA,sha256=3pBOX2m_9i-UKx-dQUu5fwr2gA638RePMzEBe8rf4E4,8986
|
|
16
|
+
git_copilot_commit-0.6.1.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
|
|
17
|
+
git_copilot_commit-0.6.1.dist-info/entry_points.txt,sha256=-D4bQqiuSPwQJG2zx--vJbZD1iqB5coUfoJ_gmC3rSg,66
|
|
18
|
+
git_copilot_commit-0.6.1.dist-info/licenses/LICENSE,sha256=14lNZAoKJPI1U7eGpletjN_PFm1JwP1vT_0jFKY6eWg,1065
|
|
19
|
+
git_copilot_commit-0.6.1.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|