git-copilot-commit 0.4.2__tar.gz → 0.4.4__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (18) hide show
  1. {git_copilot_commit-0.4.2 → git_copilot_commit-0.4.4}/PKG-INFO +13 -51
  2. {git_copilot_commit-0.4.2 → git_copilot_commit-0.4.4}/README.md +12 -50
  3. {git_copilot_commit-0.4.2 → git_copilot_commit-0.4.4}/src/git_copilot_commit/github_copilot.py +192 -7
  4. {git_copilot_commit-0.4.2 → git_copilot_commit-0.4.4}/.github/workflows/ci.yml +0 -0
  5. {git_copilot_commit-0.4.2 → git_copilot_commit-0.4.4}/.gitignore +0 -0
  6. {git_copilot_commit-0.4.2 → git_copilot_commit-0.4.4}/.justfile +0 -0
  7. {git_copilot_commit-0.4.2 → git_copilot_commit-0.4.4}/.python-version +0 -0
  8. {git_copilot_commit-0.4.2 → git_copilot_commit-0.4.4}/LICENSE +0 -0
  9. {git_copilot_commit-0.4.2 → git_copilot_commit-0.4.4}/pyproject.toml +0 -0
  10. {git_copilot_commit-0.4.2 → git_copilot_commit-0.4.4}/src/git_copilot_commit/__init__.py +0 -0
  11. {git_copilot_commit-0.4.2 → git_copilot_commit-0.4.4}/src/git_copilot_commit/cli.py +0 -0
  12. {git_copilot_commit-0.4.2 → git_copilot_commit-0.4.4}/src/git_copilot_commit/git.py +0 -0
  13. {git_copilot_commit-0.4.2 → git_copilot_commit-0.4.4}/src/git_copilot_commit/prompts/commit-message-generator-prompt.md +0 -0
  14. {git_copilot_commit-0.4.2 → git_copilot_commit-0.4.4}/src/git_copilot_commit/py.typed +0 -0
  15. {git_copilot_commit-0.4.2 → git_copilot_commit-0.4.4}/src/git_copilot_commit/settings.py +0 -0
  16. {git_copilot_commit-0.4.2 → git_copilot_commit-0.4.4}/src/git_copilot_commit/version.py +0 -0
  17. {git_copilot_commit-0.4.2 → git_copilot_commit-0.4.4}/uv.lock +0 -0
  18. {git_copilot_commit-0.4.2 → git_copilot_commit-0.4.4}/vhs/demo.vhs +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: git-copilot-commit
3
- Version: 0.4.2
3
+ Version: 0.4.4
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
@@ -104,44 +104,6 @@ Options:
104
104
  --help Show this message and exit.
105
105
  ```
106
106
 
107
- ### Authenticate
108
-
109
- ```bash
110
- $ uvx git-copilot-commit authenticate --help
111
- Usage: git-copilot-commit authenticate [OPTIONS]
112
-
113
- Autheticate with GitHub Copilot.
114
-
115
- Options:
116
- --help Show this message and exit.
117
- ```
118
-
119
- ### List models
120
-
121
- ```bash
122
- $ uvx git-copilot-commit models --help
123
- Usage: git-copilot-commit models [OPTIONS]
124
-
125
- List models available for chat in a table.
126
-
127
- Options:
128
- --help Show this message and exit.
129
- ```
130
-
131
- ### Configure
132
-
133
- ```bash
134
- $ uvx git-copilot-commit config --help
135
- Usage: git-copilot-commit config [OPTIONS]
136
-
137
- Manage application configuration.
138
-
139
- Options:
140
- --set-default-model TEXT Set default model for commit messages
141
- --show Show current configuration
142
- --help Show this message and exit.
143
- ```
144
-
145
107
  ## Examples
146
108
 
147
109
  Commit all changes:
@@ -162,14 +124,6 @@ Use a specific model:
162
124
  uvx git-copilot-commit commit --model claude-3.5-sonnet
163
125
  ```
164
126
 
165
- Set and use a default model:
166
-
167
- ```bash
168
- uvx git-copilot-commit config --set-default-model gpt-4o
169
- uvx git-copilot-commit commit
170
- uvx git-copilot-commit commit --model claude-3.5-sonnet
171
- ```
172
-
173
127
  ## Commit Message Format
174
128
 
175
129
  Follows [Conventional Commits](https://www.conventionalcommits.org/):
@@ -199,15 +153,23 @@ Add a git alias by adding the following to your `~/.gitconfig`:
199
153
  ai-commit = "!f() { uvx git-copilot-commit commit $@; }; f"
200
154
  ```
201
155
 
202
- Now you can run:
156
+ Now you can run to review the message before committing:
203
157
 
204
158
  ```bash
205
159
  git ai-commit
206
- git ai-commit --all --yes --model claude-3.5-sonnet
207
160
  ```
208
161
 
209
- Additionally, show more context in diffs by running the following command:
162
+ Alternatively, you can stage all files and auto accept the commit message and
163
+ specify which model should be used to generate the commit in one CLI invocation.
210
164
 
211
165
  ```bash
212
- git config --global diff.context 3
166
+ git ai-commit --all --yes --model claude-3.5-sonnet
213
167
  ```
168
+
169
+ > [!TIP]
170
+ >
171
+ > Show more context in diffs by running the following command:
172
+ >
173
+ > ```bash
174
+ > git config --global diff.context 3
175
+ > ```
@@ -91,44 +91,6 @@ Options:
91
91
  --help Show this message and exit.
92
92
  ```
93
93
 
94
- ### Authenticate
95
-
96
- ```bash
97
- $ uvx git-copilot-commit authenticate --help
98
- Usage: git-copilot-commit authenticate [OPTIONS]
99
-
100
- Autheticate with GitHub Copilot.
101
-
102
- Options:
103
- --help Show this message and exit.
104
- ```
105
-
106
- ### List models
107
-
108
- ```bash
109
- $ uvx git-copilot-commit models --help
110
- Usage: git-copilot-commit models [OPTIONS]
111
-
112
- List models available for chat in a table.
113
-
114
- Options:
115
- --help Show this message and exit.
116
- ```
117
-
118
- ### Configure
119
-
120
- ```bash
121
- $ uvx git-copilot-commit config --help
122
- Usage: git-copilot-commit config [OPTIONS]
123
-
124
- Manage application configuration.
125
-
126
- Options:
127
- --set-default-model TEXT Set default model for commit messages
128
- --show Show current configuration
129
- --help Show this message and exit.
130
- ```
131
-
132
94
  ## Examples
133
95
 
134
96
  Commit all changes:
@@ -149,14 +111,6 @@ Use a specific model:
149
111
  uvx git-copilot-commit commit --model claude-3.5-sonnet
150
112
  ```
151
113
 
152
- Set and use a default model:
153
-
154
- ```bash
155
- uvx git-copilot-commit config --set-default-model gpt-4o
156
- uvx git-copilot-commit commit
157
- uvx git-copilot-commit commit --model claude-3.5-sonnet
158
- ```
159
-
160
114
  ## Commit Message Format
161
115
 
162
116
  Follows [Conventional Commits](https://www.conventionalcommits.org/):
@@ -186,15 +140,23 @@ Add a git alias by adding the following to your `~/.gitconfig`:
186
140
  ai-commit = "!f() { uvx git-copilot-commit commit $@; }; f"
187
141
  ```
188
142
 
189
- Now you can run:
143
+ Now you can run to review the message before committing:
190
144
 
191
145
  ```bash
192
146
  git ai-commit
193
- git ai-commit --all --yes --model claude-3.5-sonnet
194
147
  ```
195
148
 
196
- Additionally, show more context in diffs by running the following command:
149
+ Alternatively, you can stage all files and auto accept the commit message and
150
+ specify which model should be used to generate the commit in one CLI invocation.
197
151
 
198
152
  ```bash
199
- git config --global diff.context 3
153
+ git ai-commit --all --yes --model claude-3.5-sonnet
200
154
  ```
155
+
156
+ > [!TIP]
157
+ >
158
+ > Show more context in diffs by running the following command:
159
+ >
160
+ > ```bash
161
+ > git config --global diff.context 3
162
+ > ```
@@ -8,6 +8,7 @@ import secrets
8
8
  import time
9
9
  import uuid
10
10
  from dataclasses import dataclass
11
+ from datetime import datetime
11
12
  from pathlib import Path
12
13
  from typing import Any
13
14
 
@@ -17,7 +18,7 @@ from rich.console import Console
17
18
  from rich.panel import Panel
18
19
  from rich.table import Table
19
20
 
20
- APP_NAME = "git-copilot-commit"
21
+ APP_NAME = "github-copilot-commit"
21
22
  DEFAULT_GITHUB_DOMAIN = "github.com"
22
23
  USER_AGENT = "GitHubCopilotChat/0.35.0"
23
24
  EDITOR_VERSION = "vscode/1.107.0"
@@ -28,6 +29,23 @@ CLIENT_ID = base64.b64decode("SXYxLmI1MDdhMDhjODdlY2ZlOTg=").decode()
28
29
  INITIAL_POLL_INTERVAL_MULTIPLIER = 1.2
29
30
  SLOW_DOWN_POLL_INTERVAL_MULTIPLIER = 1.4
30
31
  DEFAULT_MODEL_ID = "gpt-5.3-codex"
32
+ DEFAULT_MODEL_PREFERENCES = (
33
+ "gpt-5.3-codex",
34
+ "gpt-5.4",
35
+ "claude-opus-4.6",
36
+ "claude-opus-4.5",
37
+ "claude-sonnet-4.6",
38
+ "claude-sonnet-4.5",
39
+ "gemini-2.5-pro",
40
+ "gpt-4.1",
41
+ "gpt-4o",
42
+ )
43
+
44
+ app = typer.Typer(
45
+ add_completion=False,
46
+ no_args_is_help=True,
47
+ help="General-purpose GitHub Copilot CLI.",
48
+ )
31
49
 
32
50
  console = Console()
33
51
  console_err = Console(stderr=True)
@@ -133,6 +151,42 @@ class CopilotModel:
133
151
  )
134
152
 
135
153
 
154
+ @dataclass(slots=True)
155
+ class GitHubViewer:
156
+ login: str
157
+ name: str | None = None
158
+ html_url: str | None = None
159
+ account_type: str | None = None
160
+ plan_name: str | None = None
161
+
162
+ @classmethod
163
+ def from_payload(cls, payload: dict[str, Any]) -> "GitHubViewer":
164
+ login = payload.get("login")
165
+ name = payload.get("name")
166
+ html_url = payload.get("html_url")
167
+ account_type = payload.get("type")
168
+ plan = payload.get("plan")
169
+
170
+ if not isinstance(login, str) or not login:
171
+ raise CopilotError("GitHub user endpoint did not return a login.")
172
+
173
+ plan_name: str | None = None
174
+ if isinstance(plan, dict):
175
+ raw_plan_name = plan.get("name")
176
+ if isinstance(raw_plan_name, str) and raw_plan_name:
177
+ plan_name = raw_plan_name
178
+
179
+ return cls(
180
+ login=login,
181
+ name=name if isinstance(name, str) and name else None,
182
+ html_url=html_url if isinstance(html_url, str) and html_url else None,
183
+ account_type=(
184
+ account_type if isinstance(account_type, str) and account_type else None
185
+ ),
186
+ plan_name=plan_name,
187
+ )
188
+
189
+
136
190
  def xdg_data_home() -> Path:
137
191
  value = os.environ.get("XDG_DATA_HOME")
138
192
  if value:
@@ -175,6 +229,12 @@ def get_urls(domain: str) -> dict[str, str]:
175
229
  }
176
230
 
177
231
 
232
+ def get_github_api_base_url(domain: str) -> str:
233
+ if domain == DEFAULT_GITHUB_DOMAIN:
234
+ return "https://api.github.com"
235
+ return f"https://api.{domain}"
236
+
237
+
178
238
  def get_base_url_from_token(token: str) -> str | None:
179
239
  match = re.search(r"proxy-ep=([^;]+)", token)
180
240
  if not match:
@@ -307,7 +367,7 @@ def iter_sse_events(response: httpx.Response, url: str):
307
367
  def load_credentials() -> CopilotCredentials | None:
308
368
  path = credentials_path()
309
369
  if not path.exists():
310
- return None
370
+ return
311
371
 
312
372
  try:
313
373
  raw = json.loads(path.read_text(encoding="utf-8"))
@@ -487,10 +547,34 @@ def refresh_copilot_token(
487
547
  )
488
548
 
489
549
 
550
+ def fetch_github_viewer(
551
+ client: httpx.Client,
552
+ github_access_token: str,
553
+ domain: str,
554
+ ) -> GitHubViewer:
555
+ payload = request_json(
556
+ client,
557
+ "GET",
558
+ f"{get_github_api_base_url(domain)}/user",
559
+ headers={
560
+ "Accept": "application/vnd.github+json",
561
+ "Authorization": f"Bearer {github_access_token}",
562
+ "User-Agent": USER_AGENT,
563
+ },
564
+ )
565
+
566
+ if not isinstance(payload, dict):
567
+ raise CopilotError("GitHub user endpoint returned an invalid response.")
568
+
569
+ return GitHubViewer.from_payload(payload)
570
+
571
+
490
572
  def ensure_fresh_credentials(client: httpx.Client) -> CopilotCredentials:
491
573
  credentials = load_credentials()
492
574
  if credentials is None:
493
- raise CopilotError("No cached Copilot credentials found.")
575
+ raise CopilotError(
576
+ f"No cached Copilot credentials found. Run `{Path(__file__).name} auth login` first."
577
+ )
494
578
 
495
579
  if not credentials.is_expired():
496
580
  return credentials
@@ -572,6 +656,10 @@ def pick_model(
572
656
  )
573
657
  return by_id[requested_model]
574
658
 
659
+ for preferred_model in DEFAULT_MODEL_PREFERENCES:
660
+ if preferred_model in by_id:
661
+ return by_id[preferred_model]
662
+
575
663
  return models[0]
576
664
 
577
665
 
@@ -931,9 +1019,86 @@ def print_model_table(models: list[CopilotModel]) -> None:
931
1019
  console.print(table)
932
1020
 
933
1021
 
934
- def fail(message: str) -> None:
935
- console_err.print(f"[red]Error:[/red] {message}")
936
- raise typer.Exit(1)
1022
+ def format_relative_duration(delta_seconds: int) -> str:
1023
+ remaining = abs(delta_seconds)
1024
+ units = (
1025
+ ("d", 86_400),
1026
+ ("h", 3_600),
1027
+ ("m", 60),
1028
+ ("s", 1),
1029
+ )
1030
+ parts: list[str] = []
1031
+ for suffix, width in units:
1032
+ if remaining < width and suffix != "s":
1033
+ continue
1034
+ value, remaining = divmod(remaining, width)
1035
+ if value == 0 and suffix != "s":
1036
+ continue
1037
+ parts.append(f"{value}{suffix}")
1038
+ if len(parts) == 2:
1039
+ break
1040
+
1041
+ if not parts:
1042
+ parts.append("0s")
1043
+
1044
+ text = " ".join(parts)
1045
+ if delta_seconds < 0:
1046
+ return f"{text} ago"
1047
+ return f"in {text}"
1048
+
1049
+
1050
+ def format_unix_timestamp(timestamp: int) -> str:
1051
+ try:
1052
+ formatted = datetime.fromtimestamp(timestamp).astimezone()
1053
+ except (OSError, OverflowError, ValueError):
1054
+ return str(timestamp)
1055
+
1056
+ delta_seconds = int(timestamp - time.time())
1057
+ return (
1058
+ f"{formatted.strftime('%Y-%m-%d %H:%M:%S %Z')} "
1059
+ f"({format_relative_duration(delta_seconds)})"
1060
+ )
1061
+
1062
+
1063
+ def print_login_summary(
1064
+ domain: str,
1065
+ credentials: CopilotCredentials,
1066
+ github_viewer: GitHubViewer | None = None,
1067
+ models: list[CopilotModel] | None = None,
1068
+ ) -> None:
1069
+ table = Table.grid(padding=(0, 1))
1070
+ table.add_column(style="cyan", no_wrap=True)
1071
+ table.add_column(style="white")
1072
+
1073
+ table.add_row("GitHub host", domain)
1074
+
1075
+ if github_viewer is not None:
1076
+ identity = github_viewer.login
1077
+ if github_viewer.name and github_viewer.name != github_viewer.login:
1078
+ identity = f"{identity} ({github_viewer.name})"
1079
+ table.add_row("GitHub user", identity)
1080
+ if github_viewer.account_type:
1081
+ table.add_row("Account type", github_viewer.account_type)
1082
+ if github_viewer.plan_name:
1083
+ table.add_row("GitHub plan", github_viewer.plan_name)
1084
+ if github_viewer.html_url:
1085
+ table.add_row("GitHub profile", github_viewer.html_url)
1086
+
1087
+ table.add_row("Copilot base URL", credentials.base_url())
1088
+ table.add_row(
1089
+ "Copilot token expires",
1090
+ format_unix_timestamp(credentials.copilot_expires_at),
1091
+ )
1092
+
1093
+ if models is not None:
1094
+ default_model = pick_model(models)
1095
+ table.add_row("Available models", str(len(models)))
1096
+ table.add_row(
1097
+ "Default model",
1098
+ f"{default_model.id} ({infer_api_surface(default_model)})",
1099
+ )
1100
+
1101
+ console.print(Panel.fit(table, title="Login Summary"))
937
1102
 
938
1103
 
939
1104
  def login(enterprise_domain: str | None = None, force: bool = False) -> None:
@@ -972,9 +1137,29 @@ def login(enterprise_domain: str | None = None, force: bool = False) -> None:
972
1137
  client, github_access_token, normalized_domain
973
1138
  )
974
1139
  path = save_credentials(credentials)
1140
+ github_viewer: GitHubViewer | None = None
1141
+ available_models: list[CopilotModel] | None = None
1142
+ warnings: list[str] = []
1143
+
1144
+ try:
1145
+ github_viewer = fetch_github_viewer(client, github_access_token, domain)
1146
+ except CopilotError as exc:
1147
+ warnings.append(f"Could not fetch GitHub account details: {exc}")
1148
+
1149
+ try:
1150
+ available_models = list_models(client, credentials)
1151
+ except CopilotError as exc:
1152
+ warnings.append(f"Could not fetch Copilot model summary: {exc}")
975
1153
 
976
1154
  console.print(f"[green]Saved Copilot credentials to[/green] {path}")
977
- console.print(f"[green]Resolved Copilot base URL:[/green] {credentials.base_url()}")
1155
+ print_login_summary(
1156
+ domain,
1157
+ credentials,
1158
+ github_viewer=github_viewer,
1159
+ models=available_models,
1160
+ )
1161
+ for warning in warnings:
1162
+ console.print(f"[yellow]Warning:[/yellow] {warning}")
978
1163
 
979
1164
 
980
1165
  def ask(prompt: str, model: str | None = None) -> str: