git-copilot-commit 0.4.6__py3-none-any.whl → 0.5.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 +734 -138
- git_copilot_commit/git.py +187 -34
- git_copilot_commit/github_copilot.py +410 -157
- git_copilot_commit/prompts/commit-message-generator-prompt.md +2 -0
- git_copilot_commit/prompts/split-commit-planner-prompt.md +41 -0
- git_copilot_commit/settings.py +30 -3
- git_copilot_commit/split_commits.py +474 -0
- git_copilot_commit-0.5.1.dist-info/METADATA +205 -0
- git_copilot_commit-0.5.1.dist-info/RECORD +15 -0
- git_copilot_commit-0.5.1.dist-info/entry_points.txt +2 -0
- git_copilot_commit-0.4.6.dist-info/METADATA +0 -182
- git_copilot_commit-0.4.6.dist-info/RECORD +0 -13
- git_copilot_commit-0.4.6.dist-info/entry_points.txt +0 -2
- {git_copilot_commit-0.4.6.dist-info → git_copilot_commit-0.5.1.dist-info}/WHEEL +0 -0
- {git_copilot_commit-0.4.6.dist-info → git_copilot_commit-0.5.1.dist-info}/licenses/LICENSE +0 -0
git_copilot_commit/cli.py
CHANGED
|
@@ -2,16 +2,30 @@
|
|
|
2
2
|
git-copilot-commit - AI-powered Git commit assistant
|
|
3
3
|
"""
|
|
4
4
|
|
|
5
|
+
from dataclasses import dataclass
|
|
5
6
|
from pathlib import Path
|
|
6
|
-
|
|
7
|
+
import os
|
|
8
|
+
import sys
|
|
9
|
+
from typing import Annotated, Sequence
|
|
7
10
|
|
|
8
11
|
import rich
|
|
9
12
|
import typer
|
|
10
13
|
from rich.console import Console
|
|
11
14
|
from rich.panel import Panel
|
|
12
15
|
from rich.prompt import Confirm
|
|
13
|
-
|
|
14
|
-
|
|
16
|
+
from typer.main import get_command
|
|
17
|
+
|
|
18
|
+
from .git import GitRepository, GitError, GitStatus, NotAGitRepositoryError
|
|
19
|
+
from .split_commits import (
|
|
20
|
+
PatchUnit,
|
|
21
|
+
SplitCommitPlan,
|
|
22
|
+
SplitPlanningError,
|
|
23
|
+
build_split_plan_prompt,
|
|
24
|
+
build_status_for_patch_units,
|
|
25
|
+
extract_patch_units,
|
|
26
|
+
group_patch_units,
|
|
27
|
+
parse_split_plan_response,
|
|
28
|
+
)
|
|
15
29
|
from .settings import Settings
|
|
16
30
|
from .version import __version__
|
|
17
31
|
from . import github_copilot
|
|
@@ -19,9 +33,20 @@ from . import github_copilot
|
|
|
19
33
|
console = Console()
|
|
20
34
|
app = typer.Typer(help=__doc__, add_completion=False)
|
|
21
35
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
36
|
+
COMMIT_MESSAGE_PROMPT_FILENAME = "commit-message-generator-prompt.md"
|
|
37
|
+
SPLIT_COMMIT_PLANNER_PROMPT_FILENAME = "split-commit-planner-prompt.md"
|
|
38
|
+
SPLIT_DIFF_ARGS = [
|
|
39
|
+
"--binary",
|
|
40
|
+
"--full-index",
|
|
41
|
+
"--find-renames",
|
|
42
|
+
"--no-color",
|
|
43
|
+
"--no-ext-diff",
|
|
44
|
+
"--src-prefix=a/",
|
|
45
|
+
"--dst-prefix=b/",
|
|
46
|
+
"--unified=3",
|
|
47
|
+
]
|
|
48
|
+
|
|
49
|
+
CA_BUNDLE_HELP = "Path to a custom CA bundle (PEM)"
|
|
25
50
|
NATIVE_TLS_HELP = (
|
|
26
51
|
"Use the OS's native certificate store via 'truststore' for httpx instead of "
|
|
27
52
|
"the Python bundle. Ignored if --ca-bundle or --insecure is used."
|
|
@@ -41,6 +66,92 @@ NativeTlsOption = Annotated[
|
|
|
41
66
|
]
|
|
42
67
|
|
|
43
68
|
|
|
69
|
+
SplitOption = Annotated[
|
|
70
|
+
bool,
|
|
71
|
+
typer.Option(
|
|
72
|
+
"--split",
|
|
73
|
+
help=(
|
|
74
|
+
"Split staged hunks into multiple commits automatically. Pass "
|
|
75
|
+
"`--split=N` to express a preference for N commits."
|
|
76
|
+
),
|
|
77
|
+
),
|
|
78
|
+
]
|
|
79
|
+
SplitCountOption = Annotated[
|
|
80
|
+
int | None,
|
|
81
|
+
typer.Option(
|
|
82
|
+
"--split-count",
|
|
83
|
+
hidden=True,
|
|
84
|
+
min=1,
|
|
85
|
+
),
|
|
86
|
+
]
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
@dataclass(frozen=True, slots=True)
|
|
90
|
+
class PreparedSplitCommit:
|
|
91
|
+
"""A split commit with its generated message and assigned patch units."""
|
|
92
|
+
|
|
93
|
+
message: str
|
|
94
|
+
patch_units: tuple[PatchUnit, ...]
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def preprocess_cli_args(args: Sequence[str]) -> list[str]:
|
|
98
|
+
"""Normalize CLI arguments before Click parses them."""
|
|
99
|
+
processed_args: list[str] = []
|
|
100
|
+
in_commit_command = False
|
|
101
|
+
index = 0
|
|
102
|
+
|
|
103
|
+
while index < len(args):
|
|
104
|
+
arg = args[index]
|
|
105
|
+
|
|
106
|
+
if not in_commit_command and not arg.startswith("-"):
|
|
107
|
+
processed_args.append(arg)
|
|
108
|
+
if arg == "commit":
|
|
109
|
+
in_commit_command = True
|
|
110
|
+
index += 1
|
|
111
|
+
continue
|
|
112
|
+
|
|
113
|
+
if in_commit_command and arg.startswith("--split="):
|
|
114
|
+
split_value = arg.split("=", 1)[1].strip().lower()
|
|
115
|
+
if split_value == "auto":
|
|
116
|
+
processed_args.append("--split")
|
|
117
|
+
index += 1
|
|
118
|
+
continue
|
|
119
|
+
if split_value.isdigit():
|
|
120
|
+
processed_args.extend(["--split-count", split_value])
|
|
121
|
+
index += 1
|
|
122
|
+
continue
|
|
123
|
+
|
|
124
|
+
processed_args.append(arg)
|
|
125
|
+
index += 1
|
|
126
|
+
continue
|
|
127
|
+
|
|
128
|
+
if in_commit_command and arg == "--split" and index + 1 < len(args):
|
|
129
|
+
split_value = args[index + 1].strip().lower()
|
|
130
|
+
if split_value == "auto":
|
|
131
|
+
processed_args.append("--split")
|
|
132
|
+
index += 2
|
|
133
|
+
continue
|
|
134
|
+
if split_value.isdigit():
|
|
135
|
+
processed_args.extend(["--split-count", split_value])
|
|
136
|
+
index += 2
|
|
137
|
+
continue
|
|
138
|
+
|
|
139
|
+
processed_args.append(arg)
|
|
140
|
+
index += 1
|
|
141
|
+
|
|
142
|
+
return processed_args
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def run(args: Sequence[str] | None = None) -> None:
|
|
146
|
+
"""Run the CLI entrypoint with argument normalization."""
|
|
147
|
+
raw_args = list(args) if args is not None else sys.argv[1:]
|
|
148
|
+
command = get_command(app)
|
|
149
|
+
command.main(
|
|
150
|
+
args=preprocess_cli_args(raw_args),
|
|
151
|
+
prog_name=Path(sys.argv[0]).name,
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
|
|
44
155
|
def version_callback(value: bool):
|
|
45
156
|
if value:
|
|
46
157
|
rich.print(f"git-copilot-commit [bold yellow]{__version__}[/]")
|
|
@@ -69,12 +180,10 @@ def main(
|
|
|
69
180
|
)
|
|
70
181
|
|
|
71
182
|
|
|
72
|
-
def get_prompt_locations():
|
|
183
|
+
def get_prompt_locations(filename: str):
|
|
73
184
|
"""Get potential prompt file locations in order of preference."""
|
|
74
185
|
import importlib.resources
|
|
75
186
|
|
|
76
|
-
filename = "commit-message-generator-prompt.md"
|
|
77
|
-
|
|
78
187
|
return [
|
|
79
188
|
Path(Settings().data_dir) / "prompts" / filename, # User customizable
|
|
80
189
|
importlib.resources.files("git_copilot_commit")
|
|
@@ -83,26 +192,46 @@ def get_prompt_locations():
|
|
|
83
192
|
]
|
|
84
193
|
|
|
85
194
|
|
|
86
|
-
def
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
195
|
+
def resolve_prompt_file() -> Path | None:
|
|
196
|
+
settings = Settings()
|
|
197
|
+
try:
|
|
198
|
+
configured_prompt_file = settings.default_prompt_file
|
|
199
|
+
except ValueError:
|
|
200
|
+
console.print(
|
|
201
|
+
f"[red]Configured default prompt file in {settings.config_file} is invalid.[/red]"
|
|
202
|
+
)
|
|
203
|
+
raise typer.Exit(1)
|
|
204
|
+
|
|
205
|
+
if configured_prompt_file is None:
|
|
206
|
+
return None
|
|
207
|
+
|
|
208
|
+
return Path(configured_prompt_file).expanduser()
|
|
95
209
|
|
|
96
210
|
|
|
97
211
|
def load_system_prompt() -> str:
|
|
98
212
|
"""Load the system prompt from the markdown file."""
|
|
99
|
-
|
|
213
|
+
resolved_prompt_file = resolve_prompt_file()
|
|
214
|
+
if resolved_prompt_file is not None:
|
|
215
|
+
try:
|
|
216
|
+
return resolved_prompt_file.read_text(encoding="utf-8")
|
|
217
|
+
except OSError as exc:
|
|
218
|
+
console.print(
|
|
219
|
+
f"[red]Error reading prompt file {resolved_prompt_file}: {exc}[/red]"
|
|
220
|
+
)
|
|
221
|
+
raise typer.Exit(1)
|
|
222
|
+
|
|
223
|
+
return load_named_prompt(COMMIT_MESSAGE_PROMPT_FILENAME)
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
def load_named_prompt(filename: str) -> str:
|
|
227
|
+
"""Load a packaged prompt by filename, optionally overridden via user data."""
|
|
228
|
+
for path in get_prompt_locations(filename):
|
|
100
229
|
try:
|
|
101
230
|
return path.read_text(encoding="utf-8")
|
|
102
231
|
except (FileNotFoundError, AttributeError):
|
|
103
232
|
continue
|
|
104
233
|
|
|
105
|
-
console.print("[red]Error: Prompt file not found in any location[/red]")
|
|
234
|
+
console.print(f"[red]Error: Prompt file {filename} not found in any location[/red]")
|
|
106
235
|
raise typer.Exit(1)
|
|
107
236
|
|
|
108
237
|
|
|
@@ -112,6 +241,8 @@ def build_http_client_config(
|
|
|
112
241
|
insecure: bool,
|
|
113
242
|
native_tls: bool,
|
|
114
243
|
) -> github_copilot.HttpClientConfig:
|
|
244
|
+
if ca_bundle is not None:
|
|
245
|
+
ca_bundle = os.path.expanduser(ca_bundle)
|
|
115
246
|
return github_copilot.HttpClientConfig(
|
|
116
247
|
native_tls=native_tls,
|
|
117
248
|
insecure=insecure,
|
|
@@ -119,17 +250,26 @@ def build_http_client_config(
|
|
|
119
250
|
)
|
|
120
251
|
|
|
121
252
|
|
|
122
|
-
def
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
253
|
+
def print_copilot_error(message: str, exc: github_copilot.CopilotError) -> None:
|
|
254
|
+
"""Render Copilot errors, with rich formatting for model selection issues."""
|
|
255
|
+
if isinstance(exc, github_copilot.ModelSelectionError):
|
|
256
|
+
console.print(f"[red]{message}[/red]")
|
|
257
|
+
github_copilot.print_model_selection_error(exc)
|
|
258
|
+
return
|
|
259
|
+
|
|
260
|
+
console.print(f"[red]{message}: {exc}[/red]")
|
|
129
261
|
|
|
130
|
-
# Refresh status after staging
|
|
131
|
-
status = repo.get_status()
|
|
132
262
|
|
|
263
|
+
def display_selected_model(model: github_copilot.CopilotModel) -> None:
|
|
264
|
+
"""Show the resolved Copilot model for the current command."""
|
|
265
|
+
details = [github_copilot.infer_api_surface(model)]
|
|
266
|
+
if model.vendor:
|
|
267
|
+
details.insert(0, model.vendor)
|
|
268
|
+
console.print(f"[green]Using model:[/green] {model.id} ({', '.join(details)})")
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
def build_commit_message_prompt(status: GitStatus, context: str = "") -> str:
|
|
272
|
+
"""Build the prompt used to generate a commit message."""
|
|
133
273
|
if not status.has_staged_changes:
|
|
134
274
|
console.print("[red]No staged changes to commit.[/red]")
|
|
135
275
|
raise typer.Exit()
|
|
@@ -144,37 +284,76 @@ def generate_commit_message(
|
|
|
144
284
|
if context.strip():
|
|
145
285
|
prompt_parts.insert(0, f"User-provided context:\n\n{context.strip()}\n\n")
|
|
146
286
|
|
|
147
|
-
|
|
287
|
+
return "\n".join(prompt_parts)
|
|
148
288
|
|
|
149
|
-
prompt = "\n".join(prompt_parts)
|
|
150
289
|
|
|
151
|
-
|
|
152
|
-
|
|
290
|
+
def normalize_model_name(model: str | None) -> str | None:
|
|
291
|
+
"""Normalize model names accepted by the CLI to Copilot API model ids."""
|
|
292
|
+
if model is not None and model.startswith("github_copilot/"):
|
|
293
|
+
return model.replace("github_copilot/", "", 1)
|
|
294
|
+
return model
|
|
153
295
|
|
|
154
|
-
if model.startswith("github_copilot/"):
|
|
155
|
-
model = model.replace("github_copilot/", "")
|
|
156
296
|
|
|
297
|
+
def ask_copilot_with_system_prompt(
|
|
298
|
+
system_prompt: str,
|
|
299
|
+
prompt: str,
|
|
300
|
+
model: str | None = None,
|
|
301
|
+
http_client_config: github_copilot.HttpClientConfig | None = None,
|
|
302
|
+
) -> str:
|
|
303
|
+
"""Send a prepared prompt to Copilot using the provided system prompt."""
|
|
157
304
|
return github_copilot.ask(
|
|
158
305
|
f"""
|
|
159
306
|
# System Prompt
|
|
160
307
|
|
|
161
|
-
{
|
|
308
|
+
{system_prompt}
|
|
162
309
|
|
|
163
310
|
# Prompt
|
|
164
311
|
|
|
165
312
|
{prompt}
|
|
166
313
|
""",
|
|
314
|
+
model=normalize_model_name(model),
|
|
315
|
+
http_client_config=http_client_config,
|
|
316
|
+
)
|
|
317
|
+
|
|
318
|
+
|
|
319
|
+
def generate_commit_message_for_prompt(
|
|
320
|
+
prompt: str,
|
|
321
|
+
model: str | None = None,
|
|
322
|
+
http_client_config: github_copilot.HttpClientConfig | None = None,
|
|
323
|
+
) -> str:
|
|
324
|
+
"""Generate a conventional commit message from a prepared prompt."""
|
|
325
|
+
return ask_copilot_with_system_prompt(
|
|
326
|
+
load_system_prompt(),
|
|
327
|
+
prompt,
|
|
328
|
+
model=model,
|
|
329
|
+
http_client_config=http_client_config,
|
|
330
|
+
)
|
|
331
|
+
|
|
332
|
+
|
|
333
|
+
def generate_commit_message_for_status(
|
|
334
|
+
status: GitStatus,
|
|
335
|
+
model: str | None = None,
|
|
336
|
+
context: str = "",
|
|
337
|
+
http_client_config: github_copilot.HttpClientConfig | None = None,
|
|
338
|
+
) -> str:
|
|
339
|
+
"""Generate a commit message for a staged status snapshot."""
|
|
340
|
+
prompt = build_commit_message_prompt(status, context=context)
|
|
341
|
+
return generate_commit_message_for_prompt(
|
|
342
|
+
prompt,
|
|
167
343
|
model=model,
|
|
168
344
|
http_client_config=http_client_config,
|
|
169
345
|
)
|
|
170
346
|
|
|
171
347
|
|
|
172
348
|
def commit_with_retry_no_verify(
|
|
173
|
-
repo: GitRepository,
|
|
349
|
+
repo: GitRepository,
|
|
350
|
+
message: str,
|
|
351
|
+
use_editor: bool = False,
|
|
352
|
+
env: dict[str, str] | None = None,
|
|
174
353
|
) -> str:
|
|
175
354
|
"""Run commit and offer one retry with -n on failure."""
|
|
176
355
|
try:
|
|
177
|
-
return repo.commit(message, use_editor=use_editor)
|
|
356
|
+
return repo.commit(message, use_editor=use_editor, env=env)
|
|
178
357
|
except GitError as e:
|
|
179
358
|
console.print(f"[red]Commit failed: {e}[/red]")
|
|
180
359
|
if not Confirm.ask(
|
|
@@ -184,15 +363,444 @@ def commit_with_retry_no_verify(
|
|
|
184
363
|
raise typer.Exit(1)
|
|
185
364
|
|
|
186
365
|
try:
|
|
187
|
-
return repo.commit(message, use_editor=use_editor, no_verify=True)
|
|
366
|
+
return repo.commit(message, use_editor=use_editor, no_verify=True, env=env)
|
|
188
367
|
except GitError as retry_error:
|
|
189
368
|
console.print(f"[red]Commit with -n failed: {retry_error}[/red]")
|
|
190
369
|
raise typer.Exit(1)
|
|
191
370
|
|
|
192
371
|
|
|
372
|
+
def ensure_copilot_authentication(
|
|
373
|
+
http_client_config: github_copilot.HttpClientConfig,
|
|
374
|
+
) -> None:
|
|
375
|
+
"""Authenticate if no cached Copilot credentials are available."""
|
|
376
|
+
try:
|
|
377
|
+
existing_credentials = github_copilot.load_credentials()
|
|
378
|
+
except github_copilot.CopilotError:
|
|
379
|
+
existing_credentials = None
|
|
380
|
+
|
|
381
|
+
if existing_credentials is not None:
|
|
382
|
+
return
|
|
383
|
+
|
|
384
|
+
try:
|
|
385
|
+
github_copilot.login(
|
|
386
|
+
force=True,
|
|
387
|
+
http_client_config=http_client_config,
|
|
388
|
+
)
|
|
389
|
+
except github_copilot.CopilotError as exc:
|
|
390
|
+
print_copilot_error("Authentication failed", exc)
|
|
391
|
+
raise typer.Exit(1)
|
|
392
|
+
|
|
393
|
+
|
|
394
|
+
def stage_changes_for_commit(
|
|
395
|
+
repo: GitRepository, status: GitStatus, all_files: bool
|
|
396
|
+
) -> GitStatus:
|
|
397
|
+
"""Stage changes according to the command options and return refreshed status."""
|
|
398
|
+
if all_files:
|
|
399
|
+
repo.stage_files()
|
|
400
|
+
console.print("[green]Staged all files.[/green]")
|
|
401
|
+
return repo.get_status()
|
|
402
|
+
|
|
403
|
+
if status.has_unstaged_changes or status.has_untracked_files:
|
|
404
|
+
git_status_output = repo._run_git_command(["status"])
|
|
405
|
+
console.print(git_status_output.stdout)
|
|
406
|
+
|
|
407
|
+
if status.has_unstaged_changes:
|
|
408
|
+
if Confirm.ask(
|
|
409
|
+
"Modified files found. Add [bold yellow]all unstaged changes[/] to staging?",
|
|
410
|
+
default=True,
|
|
411
|
+
):
|
|
412
|
+
repo.stage_modified()
|
|
413
|
+
console.print("[green]Staged modified files.[/green]")
|
|
414
|
+
|
|
415
|
+
if status.has_untracked_files:
|
|
416
|
+
if Confirm.ask(
|
|
417
|
+
"Untracked files found. Add [bold yellow]all untracked files and unstaged changes[/] to staging?",
|
|
418
|
+
default=True,
|
|
419
|
+
):
|
|
420
|
+
repo.stage_files()
|
|
421
|
+
console.print("[green]Staged untracked files.[/green]")
|
|
422
|
+
|
|
423
|
+
return repo.get_status()
|
|
424
|
+
|
|
425
|
+
|
|
426
|
+
def request_commit_message(
|
|
427
|
+
status: GitStatus,
|
|
428
|
+
model: str | None = None,
|
|
429
|
+
context: str = "",
|
|
430
|
+
http_client_config: github_copilot.HttpClientConfig | None = None,
|
|
431
|
+
) -> str:
|
|
432
|
+
"""Request a commit message for the provided staged state."""
|
|
433
|
+
try:
|
|
434
|
+
with console.status(
|
|
435
|
+
"[yellow]Generating commit message based on [bold]`git diff --staged`[/] ...[/yellow]"
|
|
436
|
+
):
|
|
437
|
+
return generate_commit_message_for_status(
|
|
438
|
+
status,
|
|
439
|
+
model=model,
|
|
440
|
+
context=context,
|
|
441
|
+
http_client_config=http_client_config,
|
|
442
|
+
)
|
|
443
|
+
except github_copilot.CopilotError as exc:
|
|
444
|
+
print_copilot_error("Could not generate a commit message", exc)
|
|
445
|
+
raise typer.Exit(1)
|
|
446
|
+
|
|
447
|
+
|
|
448
|
+
def request_split_commit_plan(
|
|
449
|
+
status: GitStatus,
|
|
450
|
+
patch_units: tuple[PatchUnit, ...],
|
|
451
|
+
*,
|
|
452
|
+
preferred_commits: int | None = None,
|
|
453
|
+
model: str | None = None,
|
|
454
|
+
context: str = "",
|
|
455
|
+
http_client_config: github_copilot.HttpClientConfig | None = None,
|
|
456
|
+
) -> SplitCommitPlan:
|
|
457
|
+
"""Request and validate a split-commit plan for the staged patch units."""
|
|
458
|
+
try:
|
|
459
|
+
planner_prompt = build_split_plan_prompt(
|
|
460
|
+
status,
|
|
461
|
+
patch_units,
|
|
462
|
+
preferred_commits=preferred_commits,
|
|
463
|
+
context=context,
|
|
464
|
+
)
|
|
465
|
+
|
|
466
|
+
with console.status(
|
|
467
|
+
"[yellow]Planning split commits from [bold]staged hunks[/] ...[/yellow]"
|
|
468
|
+
):
|
|
469
|
+
response = ask_copilot_with_system_prompt(
|
|
470
|
+
load_named_prompt(SPLIT_COMMIT_PLANNER_PROMPT_FILENAME),
|
|
471
|
+
planner_prompt,
|
|
472
|
+
model=model,
|
|
473
|
+
http_client_config=http_client_config,
|
|
474
|
+
)
|
|
475
|
+
return parse_split_plan_response(
|
|
476
|
+
response,
|
|
477
|
+
patch_units,
|
|
478
|
+
)
|
|
479
|
+
except github_copilot.CopilotError as exc:
|
|
480
|
+
print_copilot_error("Could not generate a split commit plan", exc)
|
|
481
|
+
raise typer.Exit(1)
|
|
482
|
+
|
|
483
|
+
|
|
484
|
+
def request_split_commit_messages(
|
|
485
|
+
plan: SplitCommitPlan,
|
|
486
|
+
patch_units: tuple[PatchUnit, ...],
|
|
487
|
+
*,
|
|
488
|
+
model: str | None = None,
|
|
489
|
+
context: str = "",
|
|
490
|
+
http_client_config: github_copilot.HttpClientConfig | None = None,
|
|
491
|
+
) -> list[PreparedSplitCommit]:
|
|
492
|
+
"""Generate commit messages for each planned split-commit group."""
|
|
493
|
+
try:
|
|
494
|
+
prepared_commits: list[PreparedSplitCommit] = []
|
|
495
|
+
grouped_units = group_patch_units(patch_units, plan)
|
|
496
|
+
total_commits = len(grouped_units)
|
|
497
|
+
|
|
498
|
+
for index, unit_group in enumerate(grouped_units, start=1):
|
|
499
|
+
with console.status(
|
|
500
|
+
f"[yellow]Generating commit message {index}/{total_commits} based on [bold]planned staged diff[/] ...[/yellow]"
|
|
501
|
+
):
|
|
502
|
+
message = generate_commit_message_for_status(
|
|
503
|
+
build_status_for_patch_units(unit_group),
|
|
504
|
+
model=model,
|
|
505
|
+
context=context,
|
|
506
|
+
http_client_config=http_client_config,
|
|
507
|
+
)
|
|
508
|
+
|
|
509
|
+
prepared_commits.append(
|
|
510
|
+
PreparedSplitCommit(message=message, patch_units=tuple(unit_group))
|
|
511
|
+
)
|
|
512
|
+
|
|
513
|
+
return prepared_commits
|
|
514
|
+
except github_copilot.CopilotError as exc:
|
|
515
|
+
print_copilot_error("Could not generate split commit messages", exc)
|
|
516
|
+
raise typer.Exit(1)
|
|
517
|
+
|
|
518
|
+
|
|
519
|
+
def confirm_split_commit_count(
|
|
520
|
+
plan: SplitCommitPlan,
|
|
521
|
+
*,
|
|
522
|
+
preferred_commits: int,
|
|
523
|
+
yes: bool = False,
|
|
524
|
+
) -> SplitCommitPlan:
|
|
525
|
+
"""Ask whether to proceed when the planner exceeds the preferred count."""
|
|
526
|
+
actual_commits = len(plan.commits)
|
|
527
|
+
if actual_commits <= preferred_commits:
|
|
528
|
+
return plan
|
|
529
|
+
|
|
530
|
+
console.print(
|
|
531
|
+
"[yellow]Split planning produced "
|
|
532
|
+
f"{actual_commits} commits, exceeding the preferred count of "
|
|
533
|
+
f"{preferred_commits}.[/yellow]"
|
|
534
|
+
)
|
|
535
|
+
|
|
536
|
+
if yes:
|
|
537
|
+
return plan
|
|
538
|
+
|
|
539
|
+
if Confirm.ask(
|
|
540
|
+
f"Proceed with [bold]{actual_commits} commits[/] anyway?",
|
|
541
|
+
default=False,
|
|
542
|
+
):
|
|
543
|
+
return plan
|
|
544
|
+
|
|
545
|
+
console.print("Split commit plan cancelled.")
|
|
546
|
+
raise typer.Exit()
|
|
547
|
+
|
|
548
|
+
|
|
549
|
+
def display_commit_message(commit_message: str) -> None:
|
|
550
|
+
"""Render the generated commit message."""
|
|
551
|
+
console.print("[yellow]Generated commit message.[/yellow]")
|
|
552
|
+
console.print(
|
|
553
|
+
Panel(
|
|
554
|
+
f"[bold]{commit_message}[/]",
|
|
555
|
+
title="Commit Message",
|
|
556
|
+
border_style="cyan",
|
|
557
|
+
width=len(commit_message) + 5,
|
|
558
|
+
)
|
|
559
|
+
)
|
|
560
|
+
|
|
561
|
+
|
|
562
|
+
def display_split_commit_plan(prepared_commits: list[PreparedSplitCommit]) -> None:
|
|
563
|
+
"""Render the split-commit plan preview."""
|
|
564
|
+
console.print("[yellow]Generated split commit plan.[/yellow]")
|
|
565
|
+
|
|
566
|
+
for index, prepared_commit in enumerate(prepared_commits, start=1):
|
|
567
|
+
paths = list(dict.fromkeys(unit.path for unit in prepared_commit.patch_units))
|
|
568
|
+
file_lines = "\n".join(f"- {path}" for path in paths)
|
|
569
|
+
console.print(
|
|
570
|
+
Panel(
|
|
571
|
+
f"[bold]{prepared_commit.message}[/]\n\nFiles:\n{file_lines}",
|
|
572
|
+
title=f"Commit {index}",
|
|
573
|
+
border_style="cyan",
|
|
574
|
+
)
|
|
575
|
+
)
|
|
576
|
+
|
|
577
|
+
|
|
578
|
+
def execute_commit_action(
|
|
579
|
+
repo: GitRepository, commit_message: str, yes: bool = False
|
|
580
|
+
) -> str:
|
|
581
|
+
"""Run the chosen commit action using the provided message."""
|
|
582
|
+
if yes:
|
|
583
|
+
return commit_with_retry_no_verify(repo, commit_message)
|
|
584
|
+
|
|
585
|
+
choice = typer.prompt(
|
|
586
|
+
"Choose action: (c)ommit, (e)dit message, (q)uit",
|
|
587
|
+
default="c",
|
|
588
|
+
show_default=True,
|
|
589
|
+
).lower()
|
|
590
|
+
|
|
591
|
+
if choice == "q":
|
|
592
|
+
console.print("Commit cancelled.")
|
|
593
|
+
raise typer.Exit()
|
|
594
|
+
if choice == "e":
|
|
595
|
+
console.print("[cyan]Opening git editor...[/cyan]")
|
|
596
|
+
return commit_with_retry_no_verify(repo, commit_message, use_editor=True)
|
|
597
|
+
if choice == "c":
|
|
598
|
+
return commit_with_retry_no_verify(repo, commit_message)
|
|
599
|
+
|
|
600
|
+
console.print("Invalid choice. Commit cancelled.")
|
|
601
|
+
raise typer.Exit()
|
|
602
|
+
|
|
603
|
+
|
|
604
|
+
def execute_split_commit_plan(
|
|
605
|
+
repo: GitRepository,
|
|
606
|
+
prepared_commits: list[PreparedSplitCommit],
|
|
607
|
+
*,
|
|
608
|
+
yes: bool = False,
|
|
609
|
+
) -> list[str]:
|
|
610
|
+
"""Run the split-commit plan against temporary alternate indexes."""
|
|
611
|
+
use_editor = False
|
|
612
|
+
if not yes:
|
|
613
|
+
choice = typer.prompt(
|
|
614
|
+
"Choose action: (c)ommit all, (e)dit each message, (q)uit",
|
|
615
|
+
default="c",
|
|
616
|
+
show_default=True,
|
|
617
|
+
).lower()
|
|
618
|
+
|
|
619
|
+
if choice == "q":
|
|
620
|
+
console.print("Commit cancelled.")
|
|
621
|
+
raise typer.Exit()
|
|
622
|
+
if choice == "e":
|
|
623
|
+
use_editor = True
|
|
624
|
+
elif choice != "c":
|
|
625
|
+
console.print("Invalid choice. Commit cancelled.")
|
|
626
|
+
raise typer.Exit()
|
|
627
|
+
|
|
628
|
+
commit_shas: list[str] = []
|
|
629
|
+
total_commits = len(prepared_commits)
|
|
630
|
+
|
|
631
|
+
for index, prepared_commit in enumerate(prepared_commits, start=1):
|
|
632
|
+
console.print(
|
|
633
|
+
f"[cyan]Creating commit {index}/{total_commits}:[/cyan] {prepared_commit.message}"
|
|
634
|
+
)
|
|
635
|
+
|
|
636
|
+
with repo.temporary_alternate_index() as alternate_index:
|
|
637
|
+
try:
|
|
638
|
+
for patch_unit in prepared_commit.patch_units:
|
|
639
|
+
repo.check_patch_for_alternate_index(
|
|
640
|
+
patch_unit.patch,
|
|
641
|
+
index=alternate_index,
|
|
642
|
+
)
|
|
643
|
+
repo.apply_patch_to_alternate_index(
|
|
644
|
+
patch_unit.patch,
|
|
645
|
+
index=alternate_index,
|
|
646
|
+
)
|
|
647
|
+
except GitError as exc:
|
|
648
|
+
console.print(
|
|
649
|
+
f"[red]Failed to apply the planned changes for commit {index}: {exc}[/red]"
|
|
650
|
+
)
|
|
651
|
+
raise typer.Exit(1)
|
|
652
|
+
|
|
653
|
+
commit_shas.append(
|
|
654
|
+
commit_with_retry_no_verify(
|
|
655
|
+
repo,
|
|
656
|
+
prepared_commit.message,
|
|
657
|
+
use_editor=use_editor,
|
|
658
|
+
env=alternate_index.env,
|
|
659
|
+
)
|
|
660
|
+
)
|
|
661
|
+
|
|
662
|
+
return commit_shas
|
|
663
|
+
|
|
664
|
+
|
|
665
|
+
def handle_single_commit_flow(
|
|
666
|
+
repo: GitRepository,
|
|
667
|
+
status: GitStatus,
|
|
668
|
+
*,
|
|
669
|
+
model: str | None = None,
|
|
670
|
+
yes: bool = False,
|
|
671
|
+
context: str = "",
|
|
672
|
+
http_client_config: github_copilot.HttpClientConfig | None = None,
|
|
673
|
+
) -> None:
|
|
674
|
+
"""Generate, display, and execute the single-commit flow."""
|
|
675
|
+
commit_message = request_commit_message(
|
|
676
|
+
status,
|
|
677
|
+
model=model,
|
|
678
|
+
context=context,
|
|
679
|
+
http_client_config=http_client_config,
|
|
680
|
+
)
|
|
681
|
+
display_commit_message(commit_message)
|
|
682
|
+
|
|
683
|
+
commit_sha = execute_commit_action(repo, commit_message, yes=yes)
|
|
684
|
+
console.print(f"[green]✓ Successfully committed: {commit_sha[:8]}[/green]")
|
|
685
|
+
|
|
686
|
+
|
|
687
|
+
def handle_split_commit_flow(
|
|
688
|
+
repo: GitRepository,
|
|
689
|
+
status: GitStatus,
|
|
690
|
+
*,
|
|
691
|
+
preferred_commits: int | None = None,
|
|
692
|
+
model: str | None = None,
|
|
693
|
+
yes: bool = False,
|
|
694
|
+
context: str = "",
|
|
695
|
+
http_client_config: github_copilot.HttpClientConfig | None = None,
|
|
696
|
+
) -> None:
|
|
697
|
+
"""Generate, display, and execute the split-commit flow."""
|
|
698
|
+
patch_units = tuple(
|
|
699
|
+
extract_patch_units(repo.get_staged_diff(extra_args=SPLIT_DIFF_ARGS))
|
|
700
|
+
)
|
|
701
|
+
|
|
702
|
+
if not patch_units:
|
|
703
|
+
console.print(
|
|
704
|
+
"[yellow]No split patch units were extracted; falling back to a single commit.[/yellow]"
|
|
705
|
+
)
|
|
706
|
+
handle_single_commit_flow(
|
|
707
|
+
repo,
|
|
708
|
+
status,
|
|
709
|
+
model=model,
|
|
710
|
+
yes=yes,
|
|
711
|
+
context=context,
|
|
712
|
+
http_client_config=http_client_config,
|
|
713
|
+
)
|
|
714
|
+
return
|
|
715
|
+
|
|
716
|
+
if len(patch_units) == 1:
|
|
717
|
+
console.print(
|
|
718
|
+
"[yellow]Only one staged patch unit was found; creating a single commit.[/yellow]"
|
|
719
|
+
)
|
|
720
|
+
handle_single_commit_flow(
|
|
721
|
+
repo,
|
|
722
|
+
status,
|
|
723
|
+
model=model,
|
|
724
|
+
yes=yes,
|
|
725
|
+
context=context,
|
|
726
|
+
http_client_config=http_client_config,
|
|
727
|
+
)
|
|
728
|
+
return
|
|
729
|
+
|
|
730
|
+
if preferred_commits is None:
|
|
731
|
+
console.print(
|
|
732
|
+
"[yellow]Planning split commits from the staged patch units.[/yellow]"
|
|
733
|
+
)
|
|
734
|
+
else:
|
|
735
|
+
console.print(
|
|
736
|
+
"[yellow]Planning split commits with a preference for "
|
|
737
|
+
f"{preferred_commits} commits.[/yellow]"
|
|
738
|
+
)
|
|
739
|
+
|
|
740
|
+
try:
|
|
741
|
+
split_plan = request_split_commit_plan(
|
|
742
|
+
status,
|
|
743
|
+
patch_units,
|
|
744
|
+
preferred_commits=preferred_commits,
|
|
745
|
+
model=model,
|
|
746
|
+
context=context,
|
|
747
|
+
http_client_config=http_client_config,
|
|
748
|
+
)
|
|
749
|
+
except SplitPlanningError as exc:
|
|
750
|
+
console.print(
|
|
751
|
+
"[yellow]Split planning returned an invalid plan; falling back to a single commit.[/yellow]"
|
|
752
|
+
)
|
|
753
|
+
console.print(f"[yellow]Reason:[/yellow] {exc}")
|
|
754
|
+
handle_single_commit_flow(
|
|
755
|
+
repo,
|
|
756
|
+
status,
|
|
757
|
+
model=model,
|
|
758
|
+
yes=yes,
|
|
759
|
+
context=context,
|
|
760
|
+
http_client_config=http_client_config,
|
|
761
|
+
)
|
|
762
|
+
return
|
|
763
|
+
|
|
764
|
+
if preferred_commits is not None:
|
|
765
|
+
split_plan = confirm_split_commit_count(
|
|
766
|
+
split_plan,
|
|
767
|
+
preferred_commits=preferred_commits,
|
|
768
|
+
yes=yes,
|
|
769
|
+
)
|
|
770
|
+
|
|
771
|
+
prepared_commits = request_split_commit_messages(
|
|
772
|
+
split_plan,
|
|
773
|
+
patch_units,
|
|
774
|
+
model=model,
|
|
775
|
+
context=context,
|
|
776
|
+
http_client_config=http_client_config,
|
|
777
|
+
)
|
|
778
|
+
|
|
779
|
+
if len(prepared_commits) == 1:
|
|
780
|
+
console.print(
|
|
781
|
+
"[yellow]Split planning resulted in a single commit; using the standard commit flow.[/yellow]"
|
|
782
|
+
)
|
|
783
|
+
display_commit_message(prepared_commits[0].message)
|
|
784
|
+
commit_sha = execute_commit_action(repo, prepared_commits[0].message, yes=yes)
|
|
785
|
+
console.print(f"[green]✓ Successfully committed: {commit_sha[:8]}[/green]")
|
|
786
|
+
return
|
|
787
|
+
|
|
788
|
+
display_split_commit_plan(prepared_commits)
|
|
789
|
+
commit_shas = execute_split_commit_plan(repo, prepared_commits, yes=yes)
|
|
790
|
+
|
|
791
|
+
console.print(f"[green]✓ Successfully created {len(commit_shas)} commits.[/green]")
|
|
792
|
+
for commit_sha, prepared_commit in zip(commit_shas, prepared_commits, strict=True):
|
|
793
|
+
console.print(f"[green]{commit_sha[:8]}[/green] {prepared_commit.message}")
|
|
794
|
+
|
|
795
|
+
|
|
193
796
|
@app.command("authenticate")
|
|
194
797
|
@app.command("login", hidden=True)
|
|
195
798
|
def authenticate(
|
|
799
|
+
enterprise_domain: str | None = typer.Option(
|
|
800
|
+
None,
|
|
801
|
+
"--enterprise-domain",
|
|
802
|
+
help="GitHub Enterprise hostname. Omit for github.com.",
|
|
803
|
+
),
|
|
196
804
|
force: bool = typer.Option(
|
|
197
805
|
False, "--force", help="Replace cached GitHub Copilot credentials"
|
|
198
806
|
),
|
|
@@ -208,11 +816,63 @@ def authenticate(
|
|
|
208
816
|
)
|
|
209
817
|
try:
|
|
210
818
|
github_copilot.login(
|
|
819
|
+
enterprise_domain=enterprise_domain,
|
|
211
820
|
force=force,
|
|
212
821
|
http_client_config=http_client_config,
|
|
213
822
|
)
|
|
214
823
|
except github_copilot.CopilotError as exc:
|
|
215
|
-
|
|
824
|
+
print_copilot_error("Authentication failed", exc)
|
|
825
|
+
raise typer.Exit(1)
|
|
826
|
+
|
|
827
|
+
|
|
828
|
+
@app.command("summary")
|
|
829
|
+
def summary(
|
|
830
|
+
ca_bundle: CaBundleOption = None,
|
|
831
|
+
insecure: InsecureOption = False,
|
|
832
|
+
native_tls: NativeTlsOption = False,
|
|
833
|
+
):
|
|
834
|
+
"""Show the current cached GitHub Copilot login summary."""
|
|
835
|
+
http_client_config = build_http_client_config(
|
|
836
|
+
ca_bundle=ca_bundle,
|
|
837
|
+
insecure=insecure,
|
|
838
|
+
native_tls=native_tls,
|
|
839
|
+
)
|
|
840
|
+
try:
|
|
841
|
+
github_copilot.show_login_summary(http_client_config=http_client_config)
|
|
842
|
+
except github_copilot.CopilotError as exc:
|
|
843
|
+
print_copilot_error("Could not load login summary", exc)
|
|
844
|
+
raise typer.Exit(1)
|
|
845
|
+
|
|
846
|
+
|
|
847
|
+
@app.command("models")
|
|
848
|
+
def models_command(
|
|
849
|
+
vendor: str | None = typer.Option(
|
|
850
|
+
None,
|
|
851
|
+
"--vendor",
|
|
852
|
+
help="Filter listed models by vendor: anthropic, gemini/google, or openai.",
|
|
853
|
+
),
|
|
854
|
+
ca_bundle: CaBundleOption = None,
|
|
855
|
+
insecure: InsecureOption = False,
|
|
856
|
+
native_tls: NativeTlsOption = False,
|
|
857
|
+
):
|
|
858
|
+
"""List available Copilot models for the current account."""
|
|
859
|
+
http_client_config = build_http_client_config(
|
|
860
|
+
ca_bundle=ca_bundle,
|
|
861
|
+
insecure=insecure,
|
|
862
|
+
native_tls=native_tls,
|
|
863
|
+
)
|
|
864
|
+
|
|
865
|
+
try:
|
|
866
|
+
credentials, models = github_copilot.get_available_models(
|
|
867
|
+
vendor=vendor,
|
|
868
|
+
http_client_config=http_client_config,
|
|
869
|
+
)
|
|
870
|
+
|
|
871
|
+
console.print(f"[green]Copilot base URL:[/green] {credentials.base_url()}")
|
|
872
|
+
console.print(f"[green]Model count:[/green] {len(models)}")
|
|
873
|
+
github_copilot.print_model_table(models)
|
|
874
|
+
except github_copilot.CopilotError as exc:
|
|
875
|
+
print_copilot_error("Could not load models", exc)
|
|
216
876
|
raise typer.Exit(1)
|
|
217
877
|
|
|
218
878
|
|
|
@@ -221,8 +881,14 @@ def commit(
|
|
|
221
881
|
all_files: bool = typer.Option(
|
|
222
882
|
False, "--all", "-a", help="Stage all files before committing"
|
|
223
883
|
),
|
|
884
|
+
split: SplitOption = False,
|
|
885
|
+
split_count: SplitCountOption = None,
|
|
224
886
|
model: str | None = typer.Option(
|
|
225
|
-
None,
|
|
887
|
+
None,
|
|
888
|
+
"--model",
|
|
889
|
+
"-m",
|
|
890
|
+
metavar="MODEL_ID",
|
|
891
|
+
help="Model to use for generating commit message",
|
|
226
892
|
),
|
|
227
893
|
yes: bool = typer.Option(
|
|
228
894
|
False, "--yes", "-y", help="Automatically accept the generated commit message"
|
|
@@ -246,31 +912,12 @@ def commit(
|
|
|
246
912
|
console.print("[red]Error: Not in a git repository[/red]")
|
|
247
913
|
raise typer.Exit(1)
|
|
248
914
|
|
|
249
|
-
try:
|
|
250
|
-
existing_credentials = github_copilot.load_credentials()
|
|
251
|
-
except github_copilot.CopilotError:
|
|
252
|
-
existing_credentials = None
|
|
253
|
-
|
|
254
915
|
http_client_config = build_http_client_config(
|
|
255
916
|
ca_bundle=ca_bundle,
|
|
256
917
|
insecure=insecure,
|
|
257
918
|
native_tls=native_tls,
|
|
258
919
|
)
|
|
259
|
-
|
|
260
|
-
if existing_credentials is None:
|
|
261
|
-
try:
|
|
262
|
-
github_copilot.login(
|
|
263
|
-
force=True,
|
|
264
|
-
http_client_config=http_client_config,
|
|
265
|
-
)
|
|
266
|
-
except github_copilot.CopilotError as exc:
|
|
267
|
-
console.print(f"[red]Authentication failed: {exc}[/red]")
|
|
268
|
-
raise typer.Exit(1)
|
|
269
|
-
|
|
270
|
-
# Load settings and use default model if none provided
|
|
271
|
-
settings = Settings()
|
|
272
|
-
if model is None:
|
|
273
|
-
model = settings.default_model
|
|
920
|
+
ensure_copilot_authentication(http_client_config)
|
|
274
921
|
|
|
275
922
|
# Get initial status
|
|
276
923
|
status = repo.get_status()
|
|
@@ -279,98 +926,47 @@ def commit(
|
|
|
279
926
|
console.print("[yellow]No changes to commit.[/yellow]")
|
|
280
927
|
raise typer.Exit()
|
|
281
928
|
|
|
282
|
-
|
|
283
|
-
if all_files:
|
|
284
|
-
repo.stage_files() # Stage all files
|
|
285
|
-
console.print("[green]Staged all files.[/green]")
|
|
286
|
-
else:
|
|
287
|
-
# Show git status once if there are unstaged or untracked files to prompt about
|
|
288
|
-
if status.has_unstaged_changes or status.has_untracked_files:
|
|
289
|
-
git_status_output = repo._run_git_command(["status"])
|
|
290
|
-
console.print(git_status_output.stdout)
|
|
291
|
-
|
|
292
|
-
if status.has_unstaged_changes:
|
|
293
|
-
if Confirm.ask(
|
|
294
|
-
"Modified files found. Add [bold yellow]all unstaged changes[/] to staging?",
|
|
295
|
-
default=True,
|
|
296
|
-
):
|
|
297
|
-
repo.stage_modified()
|
|
298
|
-
console.print("[green]Staged modified files.[/green]")
|
|
299
|
-
if status.has_untracked_files:
|
|
300
|
-
if Confirm.ask(
|
|
301
|
-
"Untracked files found. Add [bold yellow]all untracked files and unstaged changes[/] to staging?",
|
|
302
|
-
default=True,
|
|
303
|
-
):
|
|
304
|
-
repo.stage_files()
|
|
305
|
-
console.print("[green]Staged untracked files.[/green]")
|
|
929
|
+
status = stage_changes_for_commit(repo, status, all_files=all_files)
|
|
306
930
|
|
|
307
931
|
if context:
|
|
308
932
|
console.print(
|
|
309
933
|
Panel(context.strip(), title="User Context", border_style="magenta")
|
|
310
934
|
)
|
|
311
935
|
|
|
936
|
+
normalized_model = normalize_model_name(model)
|
|
312
937
|
try:
|
|
313
|
-
github_copilot.ensure_auth_ready(
|
|
314
|
-
model=
|
|
938
|
+
selected_model = github_copilot.ensure_auth_ready(
|
|
939
|
+
model=normalized_model,
|
|
315
940
|
http_client_config=http_client_config,
|
|
316
941
|
)
|
|
317
|
-
|
|
318
|
-
# Generate or use provided commit message
|
|
319
|
-
with console.status(
|
|
320
|
-
"[yellow]Generating commit message based on [bold]`git diff --staged`[/] ...[/yellow]"
|
|
321
|
-
):
|
|
322
|
-
commit_message = generate_commit_message(
|
|
323
|
-
repo,
|
|
324
|
-
model,
|
|
325
|
-
context=context,
|
|
326
|
-
http_client_config=http_client_config,
|
|
327
|
-
)
|
|
328
942
|
except github_copilot.CopilotError as exc:
|
|
329
|
-
|
|
943
|
+
print_copilot_error("Could not select a model", exc)
|
|
330
944
|
raise typer.Exit(1)
|
|
331
945
|
|
|
332
|
-
|
|
946
|
+
display_selected_model(selected_model)
|
|
947
|
+
model = selected_model.id
|
|
333
948
|
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
949
|
+
if split or split_count is not None:
|
|
950
|
+
handle_split_commit_flow(
|
|
951
|
+
repo,
|
|
952
|
+
status,
|
|
953
|
+
preferred_commits=split_count,
|
|
954
|
+
model=model,
|
|
955
|
+
yes=yes,
|
|
956
|
+
context=context,
|
|
957
|
+
http_client_config=http_client_config,
|
|
341
958
|
)
|
|
342
|
-
|
|
959
|
+
return
|
|
343
960
|
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
show_default=True,
|
|
353
|
-
).lower()
|
|
354
|
-
|
|
355
|
-
if choice == "q":
|
|
356
|
-
console.print("Commit cancelled.")
|
|
357
|
-
raise typer.Exit()
|
|
358
|
-
elif choice == "e":
|
|
359
|
-
# Use git's built-in editor with generated message as template
|
|
360
|
-
console.print("[cyan]Opening git editor...[/cyan]")
|
|
361
|
-
commit_sha = commit_with_retry_no_verify(
|
|
362
|
-
repo, commit_message, use_editor=True
|
|
363
|
-
)
|
|
364
|
-
elif choice == "c":
|
|
365
|
-
# Commit with generated message
|
|
366
|
-
commit_sha = commit_with_retry_no_verify(repo, commit_message)
|
|
367
|
-
else:
|
|
368
|
-
console.print("Invalid choice. Commit cancelled.")
|
|
369
|
-
raise typer.Exit()
|
|
370
|
-
|
|
371
|
-
# Show success message
|
|
372
|
-
console.print(f"[green]✓ Successfully committed: {commit_sha[:8]}[/green]")
|
|
961
|
+
handle_single_commit_flow(
|
|
962
|
+
repo,
|
|
963
|
+
status,
|
|
964
|
+
model=model,
|
|
965
|
+
yes=yes,
|
|
966
|
+
context=context,
|
|
967
|
+
http_client_config=http_client_config,
|
|
968
|
+
)
|
|
373
969
|
|
|
374
970
|
|
|
375
971
|
if __name__ == "__main__":
|
|
376
|
-
|
|
972
|
+
run()
|