git-copilot-commit 0.4.3__py3-none-any.whl → 0.4.5__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- git_copilot_commit/cli.py +32 -7
- git_copilot_commit/github_copilot.py +96 -33
- {git_copilot_commit-0.4.3.dist-info → git_copilot_commit-0.4.5.dist-info}/METADATA +7 -1
- {git_copilot_commit-0.4.3.dist-info → git_copilot_commit-0.4.5.dist-info}/RECORD +7 -7
- {git_copilot_commit-0.4.3.dist-info → git_copilot_commit-0.4.5.dist-info}/WHEEL +0 -0
- {git_copilot_commit-0.4.3.dist-info → git_copilot_commit-0.4.5.dist-info}/entry_points.txt +0 -0
- {git_copilot_commit-0.4.3.dist-info → git_copilot_commit-0.4.5.dist-info}/licenses/LICENSE +0 -0
git_copilot_commit/cli.py
CHANGED
|
@@ -151,6 +151,21 @@ def commit_with_retry_no_verify(
|
|
|
151
151
|
raise typer.Exit(1)
|
|
152
152
|
|
|
153
153
|
|
|
154
|
+
@app.command("authenticate")
|
|
155
|
+
@app.command("login", hidden=True)
|
|
156
|
+
def authenticate(
|
|
157
|
+
force: bool = typer.Option(
|
|
158
|
+
False, "--force", help="Replace cached GitHub Copilot credentials"
|
|
159
|
+
),
|
|
160
|
+
):
|
|
161
|
+
"""Authenticate with GitHub Copilot and cache credentials locally."""
|
|
162
|
+
try:
|
|
163
|
+
github_copilot.login(force=force)
|
|
164
|
+
except github_copilot.CopilotError as exc:
|
|
165
|
+
console.print(f"[red]Authentication failed: {exc}[/red]")
|
|
166
|
+
raise typer.Exit(1)
|
|
167
|
+
|
|
168
|
+
|
|
154
169
|
@app.command()
|
|
155
170
|
def commit(
|
|
156
171
|
all_files: bool = typer.Option(
|
|
@@ -180,11 +195,15 @@ def commit(
|
|
|
180
195
|
|
|
181
196
|
try:
|
|
182
197
|
existing_credentials = github_copilot.load_credentials()
|
|
183
|
-
except
|
|
198
|
+
except github_copilot.CopilotError:
|
|
184
199
|
existing_credentials = None
|
|
185
200
|
|
|
186
201
|
if existing_credentials is None:
|
|
187
|
-
|
|
202
|
+
try:
|
|
203
|
+
github_copilot.login(force=True)
|
|
204
|
+
except github_copilot.CopilotError as exc:
|
|
205
|
+
console.print(f"[red]Authentication failed: {exc}[/red]")
|
|
206
|
+
raise typer.Exit(1)
|
|
188
207
|
|
|
189
208
|
# Load settings and use default model if none provided
|
|
190
209
|
settings = Settings()
|
|
@@ -228,11 +247,17 @@ def commit(
|
|
|
228
247
|
Panel(context.strip(), title="User Context", border_style="magenta")
|
|
229
248
|
)
|
|
230
249
|
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
250
|
+
try:
|
|
251
|
+
github_copilot.ensure_auth_ready(model=model)
|
|
252
|
+
|
|
253
|
+
# Generate or use provided commit message
|
|
254
|
+
with console.status(
|
|
255
|
+
"[yellow]Generating commit message based on [bold]`git diff --staged`[/] ...[/yellow]"
|
|
256
|
+
):
|
|
257
|
+
commit_message = generate_commit_message(repo, model, context=context)
|
|
258
|
+
except github_copilot.CopilotError as exc:
|
|
259
|
+
console.print(f"[red]Could not generate a commit message: {exc}[/red]")
|
|
260
|
+
raise typer.Exit(1)
|
|
236
261
|
|
|
237
262
|
console.print("[yellow]Generated commit message.[/yellow]")
|
|
238
263
|
|
|
@@ -10,7 +10,7 @@ import uuid
|
|
|
10
10
|
from dataclasses import dataclass
|
|
11
11
|
from datetime import datetime
|
|
12
12
|
from pathlib import Path
|
|
13
|
-
from typing import Any
|
|
13
|
+
from typing import Any, Callable, TypeVar
|
|
14
14
|
|
|
15
15
|
import httpx
|
|
16
16
|
import typer
|
|
@@ -19,6 +19,7 @@ from rich.panel import Panel
|
|
|
19
19
|
from rich.table import Table
|
|
20
20
|
|
|
21
21
|
APP_NAME = "github-copilot-commit"
|
|
22
|
+
CLI_AUTH_COMMAND = "git-copilot-commit authenticate"
|
|
22
23
|
DEFAULT_GITHUB_DOMAIN = "github.com"
|
|
23
24
|
USER_AGENT = "GitHubCopilotChat/0.35.0"
|
|
24
25
|
EDITOR_VERSION = "vscode/1.107.0"
|
|
@@ -49,12 +50,24 @@ app = typer.Typer(
|
|
|
49
50
|
|
|
50
51
|
console = Console()
|
|
51
52
|
console_err = Console(stderr=True)
|
|
53
|
+
T = TypeVar("T")
|
|
52
54
|
|
|
53
55
|
|
|
54
56
|
class CopilotError(RuntimeError):
|
|
55
57
|
pass
|
|
56
58
|
|
|
57
59
|
|
|
60
|
+
class CopilotHttpError(CopilotError):
|
|
61
|
+
def __init__(
|
|
62
|
+
self, status_code: int, reason_phrase: str, detail: str | None = None
|
|
63
|
+
) -> None:
|
|
64
|
+
self.status_code = status_code
|
|
65
|
+
self.reason_phrase = reason_phrase
|
|
66
|
+
self.detail = detail
|
|
67
|
+
suffix = f": {detail}" if detail else ""
|
|
68
|
+
super().__init__(f"{status_code} {reason_phrase}{suffix}")
|
|
69
|
+
|
|
70
|
+
|
|
58
71
|
@dataclass(slots=True)
|
|
59
72
|
class DeviceCodeResponse:
|
|
60
73
|
device_code: str
|
|
@@ -290,8 +303,7 @@ def request_json(
|
|
|
290
303
|
detail = response.text.strip()
|
|
291
304
|
if len(detail) > 400:
|
|
292
305
|
detail = f"{detail[:397]}..."
|
|
293
|
-
|
|
294
|
-
raise CopilotError(f"{response.status_code} {response.reason_phrase}{suffix}")
|
|
306
|
+
raise CopilotHttpError(response.status_code, response.reason_phrase, detail)
|
|
295
307
|
|
|
296
308
|
content_type = response.headers.get("content-type", "")
|
|
297
309
|
if "application/json" not in content_type:
|
|
@@ -364,10 +376,9 @@ def iter_sse_events(response: httpx.Response, url: str):
|
|
|
364
376
|
yield payload
|
|
365
377
|
|
|
366
378
|
|
|
367
|
-
def
|
|
368
|
-
path = credentials_path()
|
|
379
|
+
def read_json_object(path: Path) -> dict[str, Any] | None:
|
|
369
380
|
if not path.exists():
|
|
370
|
-
return
|
|
381
|
+
return None
|
|
371
382
|
|
|
372
383
|
try:
|
|
373
384
|
raw = json.loads(path.read_text(encoding="utf-8"))
|
|
@@ -379,11 +390,23 @@ def load_credentials() -> CopilotCredentials | None:
|
|
|
379
390
|
if not isinstance(raw, dict):
|
|
380
391
|
raise CopilotError(f"Cached credentials in {path} are not a JSON object.")
|
|
381
392
|
|
|
393
|
+
return raw
|
|
394
|
+
|
|
395
|
+
|
|
396
|
+
def load_stored_credentials_from_path(path: Path) -> CopilotCredentials | None:
|
|
397
|
+
raw = read_json_object(path)
|
|
398
|
+
if raw is None:
|
|
399
|
+
return None
|
|
400
|
+
|
|
382
401
|
return CopilotCredentials.from_dict(raw)
|
|
383
402
|
|
|
384
403
|
|
|
404
|
+
def load_credentials() -> CopilotCredentials | None:
|
|
405
|
+
return load_stored_credentials_from_path(credentials_path())
|
|
406
|
+
|
|
407
|
+
|
|
385
408
|
def save_credentials(credentials: CopilotCredentials) -> Path:
|
|
386
|
-
path = credentials_path()
|
|
409
|
+
path = credentials_path().expanduser()
|
|
387
410
|
path.parent.mkdir(parents=True, exist_ok=True)
|
|
388
411
|
payload = json.dumps(credentials.to_dict(), indent=2, sort_keys=True)
|
|
389
412
|
path.write_text(f"{payload}\n", encoding="utf-8")
|
|
@@ -573,7 +596,7 @@ def ensure_fresh_credentials(client: httpx.Client) -> CopilotCredentials:
|
|
|
573
596
|
credentials = load_credentials()
|
|
574
597
|
if credentials is None:
|
|
575
598
|
raise CopilotError(
|
|
576
|
-
f"No cached Copilot credentials found. Run `{
|
|
599
|
+
f"No cached Copilot credentials found. Run `{CLI_AUTH_COMMAND}` first."
|
|
577
600
|
)
|
|
578
601
|
|
|
579
602
|
if not credentials.is_expired():
|
|
@@ -588,6 +611,23 @@ def ensure_fresh_credentials(client: httpx.Client) -> CopilotCredentials:
|
|
|
588
611
|
return refreshed
|
|
589
612
|
|
|
590
613
|
|
|
614
|
+
def should_reauthenticate(exc: CopilotError) -> bool:
|
|
615
|
+
if isinstance(exc, CopilotHttpError):
|
|
616
|
+
return exc.status_code == 401
|
|
617
|
+
|
|
618
|
+
message = str(exc)
|
|
619
|
+
retryable_prefixes = (
|
|
620
|
+
"No cached Copilot credentials found.",
|
|
621
|
+
"Cached GitHub access token is missing or invalid.",
|
|
622
|
+
"Cached Copilot token is missing or invalid.",
|
|
623
|
+
"Cached Copilot expiration timestamp is missing or invalid.",
|
|
624
|
+
"Cached enterprise domain is invalid.",
|
|
625
|
+
"Unable to read cached credentials from ",
|
|
626
|
+
"Cached credentials in ",
|
|
627
|
+
)
|
|
628
|
+
return any(message.startswith(prefix) for prefix in retryable_prefixes)
|
|
629
|
+
|
|
630
|
+
|
|
591
631
|
def copilot_request_headers(
|
|
592
632
|
access_token: str,
|
|
593
633
|
*,
|
|
@@ -886,9 +926,8 @@ def responses_completion(
|
|
|
886
926
|
detail = response.read().decode("utf-8", errors="replace").strip()
|
|
887
927
|
if len(detail) > 400:
|
|
888
928
|
detail = f"{detail[:397]}..."
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
f"{response.status_code} {response.reason_phrase}{suffix}"
|
|
929
|
+
raise CopilotHttpError(
|
|
930
|
+
response.status_code, response.reason_phrase, detail
|
|
892
931
|
)
|
|
893
932
|
|
|
894
933
|
content_type = response.headers.get("content-type", "")
|
|
@@ -1101,24 +1140,19 @@ def print_login_summary(
|
|
|
1101
1140
|
console.print(Panel.fit(table, title="Login Summary"))
|
|
1102
1141
|
|
|
1103
1142
|
|
|
1104
|
-
def login(
|
|
1105
|
-
enterprise_domain: str | None = typer.Option(
|
|
1106
|
-
None,
|
|
1107
|
-
"--enterprise-domain",
|
|
1108
|
-
help="GitHub Enterprise hostname. Omit for github.com.",
|
|
1109
|
-
),
|
|
1110
|
-
force: bool = typer.Option(
|
|
1111
|
-
False,
|
|
1112
|
-
"--force",
|
|
1113
|
-
help="Replace any cached Copilot credentials without prompting.",
|
|
1114
|
-
),
|
|
1115
|
-
) -> None:
|
|
1143
|
+
def login(enterprise_domain: str | None = None, force: bool = False) -> None:
|
|
1116
1144
|
"""Authenticate with GitHub and cache Copilot credentials locally."""
|
|
1117
1145
|
normalized_domain = normalize_domain(enterprise_domain)
|
|
1118
1146
|
if enterprise_domain and not normalized_domain:
|
|
1119
1147
|
raise CopilotError("Invalid GitHub Enterprise hostname.")
|
|
1120
1148
|
|
|
1121
|
-
existing =
|
|
1149
|
+
existing: CopilotCredentials | None = None
|
|
1150
|
+
try:
|
|
1151
|
+
existing = load_credentials()
|
|
1152
|
+
except CopilotError:
|
|
1153
|
+
if not force:
|
|
1154
|
+
raise
|
|
1155
|
+
|
|
1122
1156
|
if existing and not force:
|
|
1123
1157
|
raise CopilotError(
|
|
1124
1158
|
f"Cached credentials already exist at {credentials_path()}. Re-run with --force to replace them."
|
|
@@ -1173,16 +1207,45 @@ def login(
|
|
|
1173
1207
|
console.print(f"[yellow]Warning:[/yellow] {warning}")
|
|
1174
1208
|
|
|
1175
1209
|
|
|
1176
|
-
def
|
|
1177
|
-
|
|
1210
|
+
def _ask_once(client: httpx.Client, prompt: str, model: str | None = None) -> str:
|
|
1211
|
+
credentials = ensure_fresh_credentials(client)
|
|
1212
|
+
all_models = list_models(client, credentials)
|
|
1213
|
+
|
|
1214
|
+
selected_model = pick_model(all_models, model)
|
|
1215
|
+
return complete_text_prompt(
|
|
1216
|
+
client,
|
|
1217
|
+
credentials,
|
|
1218
|
+
model=selected_model,
|
|
1219
|
+
prompt=prompt,
|
|
1220
|
+
)
|
|
1221
|
+
|
|
1222
|
+
|
|
1223
|
+
def _with_reauthentication(action: Callable[[httpx.Client], T]) -> T:
|
|
1224
|
+
try:
|
|
1225
|
+
with make_http_client() as client:
|
|
1226
|
+
return action(client)
|
|
1227
|
+
except CopilotError as exc:
|
|
1228
|
+
if not should_reauthenticate(exc):
|
|
1229
|
+
raise
|
|
1230
|
+
|
|
1231
|
+
console.print(
|
|
1232
|
+
"[yellow]Cached GitHub Copilot credentials are missing or no longer valid. Starting authentication...[/yellow]"
|
|
1233
|
+
)
|
|
1234
|
+
login(force=True)
|
|
1235
|
+
|
|
1178
1236
|
with make_http_client() as client:
|
|
1237
|
+
return action(client)
|
|
1238
|
+
|
|
1239
|
+
|
|
1240
|
+
def ensure_auth_ready(model: str | None = None) -> None:
|
|
1241
|
+
def validate(client: httpx.Client) -> None:
|
|
1179
1242
|
credentials = ensure_fresh_credentials(client)
|
|
1180
1243
|
all_models = list_models(client, credentials)
|
|
1244
|
+
pick_model(all_models, model)
|
|
1181
1245
|
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
)
|
|
1246
|
+
_with_reauthentication(validate)
|
|
1247
|
+
|
|
1248
|
+
|
|
1249
|
+
def ask(prompt: str, model: str | None = None) -> str:
|
|
1250
|
+
"""Send a prompt to GitHub Copilot and print the reply."""
|
|
1251
|
+
return _with_reauthentication(lambda client: _ask_once(client, prompt, model))
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: git-copilot-commit
|
|
3
|
-
Version: 0.4.
|
|
3
|
+
Version: 0.4.5
|
|
4
4
|
Summary: Automatically generate and commit changes using copilot
|
|
5
5
|
Author-email: Dheepak Krishnamurthy <1813121+kdheepak@users.noreply.github.com>
|
|
6
6
|
License-File: LICENSE
|
|
@@ -77,6 +77,12 @@ pipx install git-copilot-commit
|
|
|
77
77
|
uvx git-copilot-commit authenticate
|
|
78
78
|
```
|
|
79
79
|
|
|
80
|
+
If your cached GitHub token is revoked or expires, refresh it with:
|
|
81
|
+
|
|
82
|
+
```bash
|
|
83
|
+
uvx git-copilot-commit authenticate --force
|
|
84
|
+
```
|
|
85
|
+
|
|
80
86
|
2. Make changes in your repository.
|
|
81
87
|
|
|
82
88
|
3. Generate and commit:
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
git_copilot_commit/__init__.py,sha256=v3x5oBkxwKJEZLv62QqSmP3iqNKLtZgrWZfH8eFzlQg,60
|
|
2
|
-
git_copilot_commit/cli.py,sha256=
|
|
2
|
+
git_copilot_commit/cli.py,sha256=gXRoJDhpW0Ck7-sBDpMmulYztUH1IExuF-SczbgQqF0,9434
|
|
3
3
|
git_copilot_commit/git.py,sha256=f42GawgkyrsFkl127XvDrdg2xVEf87lb-5QO04nuRoU,9459
|
|
4
|
-
git_copilot_commit/github_copilot.py,sha256=
|
|
4
|
+
git_copilot_commit/github_copilot.py,sha256=lP-LLk4mOC4D9-rCtxIBs7XN_oFImZHpoKyHNBQFf-c,39484
|
|
5
5
|
git_copilot_commit/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
6
6
|
git_copilot_commit/settings.py,sha256=asaCxX_TAr5lCRkoSLtHSk1eUrT-y4bJbLUNQZSzAs0,2793
|
|
7
7
|
git_copilot_commit/version.py,sha256=AieHOUX52g6N67HL0iLWtDKrgOYyulxwHWViu26Jrd4,105
|
|
8
8
|
git_copilot_commit/prompts/commit-message-generator-prompt.md,sha256=ZvllyqtsLRwj6NmvygNGFajdLKNkO67hUnLsR_P1WOs,2370
|
|
9
|
-
git_copilot_commit-0.4.
|
|
10
|
-
git_copilot_commit-0.4.
|
|
11
|
-
git_copilot_commit-0.4.
|
|
12
|
-
git_copilot_commit-0.4.
|
|
13
|
-
git_copilot_commit-0.4.
|
|
9
|
+
git_copilot_commit-0.4.5.dist-info/METADATA,sha256=BFvZy64MhMB6GGeAiBS6L7N5EcPqNS_9miCE8roZ9E8,4059
|
|
10
|
+
git_copilot_commit-0.4.5.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
|
|
11
|
+
git_copilot_commit-0.4.5.dist-info/entry_points.txt,sha256=Imboc0oJa4Oq1O3C-wWcy7ZxfsVSkkC-OC-iPbTn3Fg,66
|
|
12
|
+
git_copilot_commit-0.4.5.dist-info/licenses/LICENSE,sha256=14lNZAoKJPI1U7eGpletjN_PFm1JwP1vT_0jFKY6eWg,1065
|
|
13
|
+
git_copilot_commit-0.4.5.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|