pygitcode 0.1.1__tar.gz → 0.1.2a0__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 (29) hide show
  1. {pygitcode-0.1.1 → pygitcode-0.1.2a0}/PKG-INFO +20 -6
  2. {pygitcode-0.1.1 → pygitcode-0.1.2a0}/README.md +19 -5
  3. {pygitcode-0.1.1 → pygitcode-0.1.2a0}/pyproject.toml +4 -1
  4. {pygitcode-0.1.1 → pygitcode-0.1.2a0}/src/gitcode_cli/__init__.py +1 -1
  5. {pygitcode-0.1.1 → pygitcode-0.1.2a0}/src/gitcode_cli/cli.py +24 -2
  6. pygitcode-0.1.2a0/src/gitcode_cli/commands/auth.py +54 -0
  7. {pygitcode-0.1.1 → pygitcode-0.1.2a0}/src/gitcode_cli/commands/issue.py +48 -21
  8. {pygitcode-0.1.1 → pygitcode-0.1.2a0}/src/gitcode_cli/commands/pr.py +99 -25
  9. {pygitcode-0.1.1 → pygitcode-0.1.2a0}/src/gitcode_cli/formatters.py +6 -4
  10. {pygitcode-0.1.1 → pygitcode-0.1.2a0}/src/gitcode_cli/services/pulls.py +3 -0
  11. {pygitcode-0.1.1 → pygitcode-0.1.2a0}/src/gitcode_cli/utils.py +21 -1
  12. {pygitcode-0.1.1 → pygitcode-0.1.2a0}/src/pygitcode.egg-info/PKG-INFO +20 -6
  13. pygitcode-0.1.1/src/gitcode_cli/commands/auth.py +0 -23
  14. {pygitcode-0.1.1 → pygitcode-0.1.2a0}/LICENSE +0 -0
  15. {pygitcode-0.1.1 → pygitcode-0.1.2a0}/setup.cfg +0 -0
  16. {pygitcode-0.1.1 → pygitcode-0.1.2a0}/src/gitcode_cli/cli_compat.py +0 -0
  17. {pygitcode-0.1.1 → pygitcode-0.1.2a0}/src/gitcode_cli/client.py +0 -0
  18. {pygitcode-0.1.1 → pygitcode-0.1.2a0}/src/gitcode_cli/commands/__init__.py +0 -0
  19. {pygitcode-0.1.1 → pygitcode-0.1.2a0}/src/gitcode_cli/config.py +0 -0
  20. {pygitcode-0.1.1 → pygitcode-0.1.2a0}/src/gitcode_cli/context.py +0 -0
  21. {pygitcode-0.1.1 → pygitcode-0.1.2a0}/src/gitcode_cli/errors.py +0 -0
  22. {pygitcode-0.1.1 → pygitcode-0.1.2a0}/src/gitcode_cli/repo.py +0 -0
  23. {pygitcode-0.1.1 → pygitcode-0.1.2a0}/src/gitcode_cli/services/__init__.py +0 -0
  24. {pygitcode-0.1.1 → pygitcode-0.1.2a0}/src/gitcode_cli/services/issues.py +0 -0
  25. {pygitcode-0.1.1 → pygitcode-0.1.2a0}/src/pygitcode.egg-info/SOURCES.txt +0 -0
  26. {pygitcode-0.1.1 → pygitcode-0.1.2a0}/src/pygitcode.egg-info/dependency_links.txt +0 -0
  27. {pygitcode-0.1.1 → pygitcode-0.1.2a0}/src/pygitcode.egg-info/entry_points.txt +0 -0
  28. {pygitcode-0.1.1 → pygitcode-0.1.2a0}/src/pygitcode.egg-info/requires.txt +0 -0
  29. {pygitcode-0.1.1 → pygitcode-0.1.2a0}/src/pygitcode.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pygitcode
3
- Version: 0.1.1
3
+ Version: 0.1.2a0
4
4
  Summary: A CLI tool for GitCode (api.gitcode.com), modeled after GitHub CLI.
5
5
  Author-email: codeasier <825770651@qq.com>
6
6
  License-Expression: MIT
@@ -45,7 +45,6 @@ Dynamic: license-file
45
45
  [![PyPI](https://img.shields.io/pypi/v/pygitcode)](https://pypi.org/project/pygitcode/)
46
46
  [![Python](https://img.shields.io/pypi/pyversions/pygitcode)](https://pypi.org/project/pygitcode/)
47
47
  [![License](https://img.shields.io/pypi/l/pygitcode)](https://github.com/yourusername/pygitcode/blob/main/LICENSE)
48
- [![Tests](https://github.com/codeasier/gitcode-cli/workflows/Tests/badge.svg)](https://github.com/yourusername/pygitcode/actions)
49
48
 
50
49
  ## Installation
51
50
 
@@ -83,6 +82,15 @@ Set-Alias -Name gc -Value 'C:\Users\<user>\AppData\Roaming\Python\Python313\Scri
83
82
  # Login with your GitCode personal access token
84
83
  gc auth login
85
84
 
85
+ # Check auth status
86
+ gc auth status
87
+
88
+ # Print the current auth token
89
+ gc auth token
90
+
91
+ # Logout
92
+ gc auth logout
93
+
86
94
  # Or set environment variable
87
95
  export GC_TOKEN=your_token_here
88
96
  ```
@@ -103,12 +111,14 @@ gc issue view 42 --web
103
111
 
104
112
  # Create an issue
105
113
  gc issue create -t "Bug report" -b "Something is broken"
114
+ gc issue create --label bug --label "help wanted" # Multiple labels
106
115
  gc issue create --web # Create in browser
107
116
 
108
- # Edit an issue (add/remove labels, assignees)
117
+ # Edit an issue (add/remove labels, assignees, milestones)
109
118
  gc issue edit 42 -t "Updated title"
110
119
  gc issue edit 42 --add-label bug --remove-label "in progress"
111
120
  gc issue edit 42 --add-assignee @me --remove-assignee otheruser
121
+ gc issue edit 42 --milestone v1.0 --remove-milestone
112
122
 
113
123
  # Comment on an issue
114
124
  gc issue comment 42 -b "Thanks for the report!"
@@ -116,8 +126,7 @@ gc issue comment 42 --editor # Use system editor
116
126
 
117
127
  # Close / reopen / delete
118
128
  gc issue close 42
119
- gc issue reopen 42
120
- gc issue delete 42
129
+ gc issue close 42 -c "Fixed in #50" --reason completed
121
130
  ```
122
131
 
123
132
  ### Pull Requests
@@ -132,6 +141,7 @@ gc pr list --web # Open in browser
132
141
  # View a PR (identifier optional - infers from current branch)
133
142
  gc pr view # View PR for current branch
134
143
  gc pr view 42 # By number
144
+ gc pr view 42 --comments # Include comments
135
145
  gc pr view https://gitcode.com/owner/repo/pulls/42
136
146
  gc pr view feature-branch
137
147
 
@@ -160,11 +170,13 @@ gc pr close --delete-branch # Delete remote branch too
160
170
  gc pr merge # Merge PR for current branch
161
171
  gc pr merge 42 -s # Squash merge
162
172
  gc pr merge --rebase
173
+ gc pr merge --delete-branch # Delete remote branch after merge
163
174
 
164
- # Edit a PR (add/remove labels, assignees, reviewers)
175
+ # Edit a PR (add/remove labels, assignees, reviewers, milestones)
165
176
  gc pr edit 42 -t "New title"
166
177
  gc pr edit --add-label bug --remove-label "needs review"
167
178
  gc pr edit --add-reviewer @me --remove-reviewer otheruser
179
+ gc pr edit --milestone v1.0 --remove-milestone
168
180
 
169
181
  # Mark as ready or convert to draft
170
182
  gc pr ready # Mark current branch's PR as ready
@@ -173,6 +185,8 @@ gc pr ready --undo # Convert back to draft
173
185
  # Comment / review / diff (identifier optional)
174
186
  gc pr comment -b "LGTM"
175
187
  gc pr comment --path src/file.py --position 5 -b "Suggestion"
188
+ gc pr comment --body-file comment.txt
189
+ gc pr comment --editor
176
190
 
177
191
  gc pr review --approve
178
192
  gc pr review --comment -b "Looks good but needs tests"
@@ -5,7 +5,6 @@
5
5
  [![PyPI](https://img.shields.io/pypi/v/pygitcode)](https://pypi.org/project/pygitcode/)
6
6
  [![Python](https://img.shields.io/pypi/pyversions/pygitcode)](https://pypi.org/project/pygitcode/)
7
7
  [![License](https://img.shields.io/pypi/l/pygitcode)](https://github.com/yourusername/pygitcode/blob/main/LICENSE)
8
- [![Tests](https://github.com/codeasier/gitcode-cli/workflows/Tests/badge.svg)](https://github.com/yourusername/pygitcode/actions)
9
8
 
10
9
  ## Installation
11
10
 
@@ -43,6 +42,15 @@ Set-Alias -Name gc -Value 'C:\Users\<user>\AppData\Roaming\Python\Python313\Scri
43
42
  # Login with your GitCode personal access token
44
43
  gc auth login
45
44
 
45
+ # Check auth status
46
+ gc auth status
47
+
48
+ # Print the current auth token
49
+ gc auth token
50
+
51
+ # Logout
52
+ gc auth logout
53
+
46
54
  # Or set environment variable
47
55
  export GC_TOKEN=your_token_here
48
56
  ```
@@ -63,12 +71,14 @@ gc issue view 42 --web
63
71
 
64
72
  # Create an issue
65
73
  gc issue create -t "Bug report" -b "Something is broken"
74
+ gc issue create --label bug --label "help wanted" # Multiple labels
66
75
  gc issue create --web # Create in browser
67
76
 
68
- # Edit an issue (add/remove labels, assignees)
77
+ # Edit an issue (add/remove labels, assignees, milestones)
69
78
  gc issue edit 42 -t "Updated title"
70
79
  gc issue edit 42 --add-label bug --remove-label "in progress"
71
80
  gc issue edit 42 --add-assignee @me --remove-assignee otheruser
81
+ gc issue edit 42 --milestone v1.0 --remove-milestone
72
82
 
73
83
  # Comment on an issue
74
84
  gc issue comment 42 -b "Thanks for the report!"
@@ -76,8 +86,7 @@ gc issue comment 42 --editor # Use system editor
76
86
 
77
87
  # Close / reopen / delete
78
88
  gc issue close 42
79
- gc issue reopen 42
80
- gc issue delete 42
89
+ gc issue close 42 -c "Fixed in #50" --reason completed
81
90
  ```
82
91
 
83
92
  ### Pull Requests
@@ -92,6 +101,7 @@ gc pr list --web # Open in browser
92
101
  # View a PR (identifier optional - infers from current branch)
93
102
  gc pr view # View PR for current branch
94
103
  gc pr view 42 # By number
104
+ gc pr view 42 --comments # Include comments
95
105
  gc pr view https://gitcode.com/owner/repo/pulls/42
96
106
  gc pr view feature-branch
97
107
 
@@ -120,11 +130,13 @@ gc pr close --delete-branch # Delete remote branch too
120
130
  gc pr merge # Merge PR for current branch
121
131
  gc pr merge 42 -s # Squash merge
122
132
  gc pr merge --rebase
133
+ gc pr merge --delete-branch # Delete remote branch after merge
123
134
 
124
- # Edit a PR (add/remove labels, assignees, reviewers)
135
+ # Edit a PR (add/remove labels, assignees, reviewers, milestones)
125
136
  gc pr edit 42 -t "New title"
126
137
  gc pr edit --add-label bug --remove-label "needs review"
127
138
  gc pr edit --add-reviewer @me --remove-reviewer otheruser
139
+ gc pr edit --milestone v1.0 --remove-milestone
128
140
 
129
141
  # Mark as ready or convert to draft
130
142
  gc pr ready # Mark current branch's PR as ready
@@ -133,6 +145,8 @@ gc pr ready --undo # Convert back to draft
133
145
  # Comment / review / diff (identifier optional)
134
146
  gc pr comment -b "LGTM"
135
147
  gc pr comment --path src/file.py --position 5 -b "Suggestion"
148
+ gc pr comment --body-file comment.txt
149
+ gc pr comment --editor
136
150
 
137
151
  gc pr review --approve
138
152
  gc pr review --comment -b "Looks good but needs tests"
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "pygitcode"
7
- version = "0.1.1"
7
+ dynamic = ["version"]
8
8
  description = "A CLI tool for GitCode (api.gitcode.com), modeled after GitHub CLI."
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -58,6 +58,9 @@ gitcode = "gitcode_cli.cli:main"
58
58
  [tool.setuptools.packages.find]
59
59
  where = ["src"]
60
60
 
61
+ [tool.setuptools.dynamic]
62
+ version = {attr = "gitcode_cli.__version__"}
63
+
61
64
  # --- pytest ---
62
65
  [tool.pytest.ini_options]
63
66
  testpaths = ["tests"]
@@ -1,3 +1,3 @@
1
1
  __all__ = ["__version__"]
2
2
 
3
- __version__ = "0.1.0"
3
+ __version__ = "0.1.2a0"
@@ -1,5 +1,9 @@
1
1
  from __future__ import annotations
2
2
 
3
+ import contextlib
4
+ import importlib.metadata
5
+ import sys
6
+
3
7
  import click
4
8
 
5
9
  from . import __version__
@@ -9,14 +13,32 @@ from .commands.pr import pr_group
9
13
  from .config import get_token
10
14
  from .context import AppContext
11
15
  from .errors import GCError
16
+ from .utils import safe_echo
17
+
18
+
19
+ def _configure_stdout_encoding() -> None:
20
+ if sys.platform != "win32":
21
+ return
22
+ for stream in (sys.stdout, sys.stderr):
23
+ if hasattr(stream, "reconfigure") and stream.isatty():
24
+ with contextlib.suppress(Exception):
25
+ stream.reconfigure(encoding="utf-8", errors="replace") # type: ignore[attr-defined]
26
+
27
+
28
+ def _get_version() -> str:
29
+ try:
30
+ return importlib.metadata.version("pygitcode")
31
+ except importlib.metadata.PackageNotFoundError:
32
+ return __version__
12
33
 
13
34
 
14
35
  @click.group(context_settings={"help_option_names": ["-h", "--help"]})
15
- @click.version_option(version=__version__, prog_name="gitcode")
36
+ @click.version_option(version=_get_version(), prog_name="gitcode")
16
37
  @click.option("--repo", "repo_name", "-R", help="Repository in OWNER/REPO format (default: gitcode.com).")
17
38
  @click.option("--token", hidden=True, help="Override authentication token.")
18
39
  @click.pass_context
19
40
  def main(ctx: click.Context, repo_name: str | None, token: str | None) -> None:
41
+ _configure_stdout_encoding()
20
42
  ctx.ensure_object(dict)
21
43
  try:
22
44
  resolved_token = token or get_token()
@@ -32,7 +54,7 @@ def process_result(*_args: object, **_kwargs: object) -> None:
32
54
 
33
55
  @main.command("version")
34
56
  def version_command() -> None:
35
- click.echo(f"gitcode version {__version__}")
57
+ safe_echo(f"gitcode version {_get_version()}")
36
58
 
37
59
 
38
60
  main.add_command(auth_group)
@@ -0,0 +1,54 @@
1
+ from __future__ import annotations
2
+
3
+ import click
4
+
5
+ from ..config import get_token, load_config, save_config
6
+ from ..utils import safe_echo
7
+
8
+
9
+ @click.group("auth")
10
+ def auth_group() -> None:
11
+ pass
12
+
13
+
14
+ @auth_group.command("login")
15
+ @click.option("--with-token", is_flag=True, help="Read token from stdin.")
16
+ def auth_login(with_token: bool) -> None:
17
+ if with_token:
18
+ token = click.get_text_stream("stdin").read().strip()
19
+ if not token:
20
+ raise click.ClickException("No token provided on stdin.")
21
+ else:
22
+ token = click.prompt("GitCode token", hide_input=True)
23
+ save_config({"token": token})
24
+ safe_echo("Authentication saved.")
25
+
26
+
27
+ @auth_group.command("logout")
28
+ def auth_logout() -> None:
29
+ config = load_config()
30
+ if "token" not in config:
31
+ raise click.ClickException("Not logged in.")
32
+ del config["token"]
33
+ save_config(config)
34
+ safe_echo("Logged out.")
35
+
36
+
37
+ @auth_group.command("status")
38
+ def auth_status() -> None:
39
+ try:
40
+ token = get_token()
41
+ except Exception:
42
+ safe_echo("Not logged in. Run `gc auth login` to authenticate.")
43
+ return
44
+ masked = token[:4] + "****" if len(token) > 4 else "****"
45
+ safe_echo(f"Logged in to GitCode (token: {masked})")
46
+
47
+
48
+ @auth_group.command("token")
49
+ def auth_token() -> None:
50
+ try:
51
+ token = get_token()
52
+ except Exception as exc:
53
+ raise click.ClickException(str(exc)) from exc
54
+ safe_echo(token)
@@ -6,13 +6,13 @@ from ..cli_compat import get_body_from_options, normalize_multi_values
6
6
  from ..formatters import format_issue_detail, format_issue_list, output_result
7
7
  from ..repo import resolve_repo
8
8
  from ..services import IssueService
9
- from ..utils import open_in_browser, prompt_if_missing, read_body_file, resolve_issue_arg
9
+ from ..utils import open_in_browser, prompt_if_missing, read_body_file, resolve_issue_arg, safe_echo, safe_number
10
10
 
11
11
 
12
12
  def _echo_issue_summary(items: list[dict]) -> None:
13
13
  output = format_issue_list(items)
14
14
  if output:
15
- click.echo(output)
15
+ safe_echo(output)
16
16
 
17
17
 
18
18
  @click.group("issue")
@@ -118,11 +118,11 @@ def issue_view(
118
118
  data["comments"] = comment_items
119
119
 
120
120
  def default_formatter(data: dict) -> None:
121
- click.echo(format_issue_detail(data))
121
+ safe_echo(format_issue_detail(data))
122
122
  if comment_items:
123
- click.echo("\nComments:")
123
+ safe_echo("\nComments:")
124
124
  for comment in comment_items:
125
- click.echo(f"- {comment.get('body') or ''}")
125
+ safe_echo(f"- {comment.get('body') or ''}")
126
126
 
127
127
  output_result(
128
128
  data,
@@ -137,7 +137,7 @@ def issue_view(
137
137
  json_fields,
138
138
  jq_query,
139
139
  template,
140
- default_formatter=lambda data: click.echo(format_issue_detail(data)),
140
+ default_formatter=lambda data: safe_echo(format_issue_detail(data)),
141
141
  )
142
142
 
143
143
 
@@ -146,7 +146,7 @@ def issue_view(
146
146
  @click.option("-t", "--title")
147
147
  @click.option("-b", "--body")
148
148
  @click.option("-a", "--assignee")
149
- @click.option("-l", "--label", "labels")
149
+ @click.option("-l", "--label", "labels", multiple=True)
150
150
  @click.option("-m", "--milestone")
151
151
  @click.option("-F", "--body-file")
152
152
  @click.option("-w", "--web", is_flag=True, help="Open the issue in the web browser.")
@@ -160,7 +160,7 @@ def issue_create(
160
160
  title: str | None,
161
161
  body: str | None,
162
162
  assignee: str | None,
163
- labels: str | None,
163
+ labels: tuple[str, ...] | None,
164
164
  milestone: str | None,
165
165
  body_file: str | None,
166
166
  web: bool,
@@ -176,22 +176,33 @@ def issue_create(
176
176
  title = prompt_if_missing(title, "Title")
177
177
  if body_file:
178
178
  body = read_body_file(body_file)
179
+ labels_str = normalize_multi_values(labels)
179
180
  service = IssueService(app.client())
180
- item = service.create(owner, repo, title=title, body=body, assignee=assignee, labels=labels, milestone=milestone)
181
+ item = service.create(
182
+ owner, repo, title=title, body=body, assignee=assignee, labels=labels_str, milestone=milestone
183
+ )
181
184
  output_result(
182
185
  item,
183
186
  json_fields,
184
187
  jq_query,
185
188
  template,
186
- default_formatter=lambda data: click.echo(data["html_url"]),
189
+ default_formatter=lambda data: safe_echo(data["html_url"]),
187
190
  )
188
191
 
189
192
 
190
193
  @issue_group.command("close")
191
194
  @click.option("-R", "--repo", "repo_name", help="Repository in OWNER/REPO format (default: gitcode.com).")
192
195
  @click.argument("identifier")
196
+ @click.option("-c", "--comment", help="Leave a closing comment.")
197
+ @click.option("-r", "--reason", type=click.Choice(["completed", "not_planned"]), help="Reason for closing.")
193
198
  @click.pass_context
194
- def issue_close(ctx: click.Context, repo_name: str | None, identifier: str) -> None:
199
+ def issue_close(
200
+ ctx: click.Context,
201
+ repo_name: str | None,
202
+ identifier: str,
203
+ comment: str | None,
204
+ reason: str | None,
205
+ ) -> None:
195
206
  app = ctx.obj["app"]
196
207
  url_owner, url_repo, number = resolve_issue_arg(identifier)
197
208
  if url_owner:
@@ -200,8 +211,13 @@ def issue_close(ctx: click.Context, repo_name: str | None, identifier: str) -> N
200
211
  else:
201
212
  owner, repo = resolve_repo(repo_name or app.repo)
202
213
  service = IssueService(app.client())
203
- item = service.update(owner, repo, number, state="closed")
204
- click.echo(f"Closed issue #{item['number']}")
214
+ if comment:
215
+ service.comment(owner, repo, number, comment)
216
+ update_data: dict = {"state": "closed"}
217
+ if reason:
218
+ update_data["state_reason"] = reason
219
+ item = service.update(owner, repo, number, **update_data)
220
+ safe_echo(f"Closed issue #{safe_number(item, number)}")
205
221
 
206
222
 
207
223
  @issue_group.command("comment")
@@ -235,7 +251,7 @@ def issue_comment(
235
251
  body = prompt_if_missing(body, "Body")
236
252
  service = IssueService(app.client())
237
253
  item = service.comment(owner, repo, number, body)
238
- click.echo(str(item["id"]))
254
+ safe_echo(str(item["id"]))
239
255
 
240
256
 
241
257
  @issue_group.command("reopen")
@@ -252,7 +268,7 @@ def issue_reopen(ctx: click.Context, repo_name: str | None, identifier: str) ->
252
268
  owner, repo = resolve_repo(repo_name or app.repo)
253
269
  service = IssueService(app.client())
254
270
  item = service.update(owner, repo, number, state="open")
255
- click.echo(f"Reopened issue #{item['number']}")
271
+ safe_echo(f"Reopened issue #{safe_number(item, number)}")
256
272
 
257
273
 
258
274
  @issue_group.command("edit")
@@ -260,10 +276,13 @@ def issue_reopen(ctx: click.Context, repo_name: str | None, identifier: str) ->
260
276
  @click.argument("identifier")
261
277
  @click.option("-t", "--title")
262
278
  @click.option("-b", "--body")
279
+ @click.option("-F", "--body-file")
263
280
  @click.option("-a", "--add-assignee")
264
281
  @click.option("-l", "--add-label")
265
282
  @click.option("--remove-assignee")
266
283
  @click.option("--remove-label")
284
+ @click.option("-m", "--milestone")
285
+ @click.option("--remove-milestone", is_flag=True, help="Remove the milestone from the issue.")
267
286
  @click.pass_context
268
287
  def issue_edit(
269
288
  ctx: click.Context,
@@ -271,10 +290,13 @@ def issue_edit(
271
290
  identifier: str,
272
291
  title: str | None,
273
292
  body: str | None,
293
+ body_file: str | None,
274
294
  add_assignee: str | None,
275
295
  add_label: str | None,
276
296
  remove_assignee: str | None,
277
297
  remove_label: str | None,
298
+ milestone: str | None,
299
+ remove_milestone: bool,
278
300
  ) -> None:
279
301
  app = ctx.obj["app"]
280
302
  url_owner, url_repo, number = resolve_issue_arg(identifier)
@@ -283,7 +305,8 @@ def issue_edit(
283
305
  owner, repo = url_owner, url_repo
284
306
  else:
285
307
  owner, repo = resolve_repo(repo_name or app.repo)
286
- service = IssueService(app.client())
308
+ if body_file:
309
+ body = read_body_file(body_file)
287
310
  data = {
288
311
  k: v
289
312
  for k, v in {
@@ -293,13 +316,17 @@ def issue_edit(
293
316
  "labels": add_label,
294
317
  "unassignee": remove_assignee,
295
318
  "unset_labels": remove_label,
319
+ "milestone": milestone,
296
320
  }.items()
297
321
  if v is not None
298
322
  }
323
+ if remove_milestone:
324
+ data["milestone"] = ""
299
325
  if not data:
300
326
  raise click.UsageError("must specify at least one field to edit")
327
+ service = IssueService(app.client())
301
328
  item = service.update(owner, repo, number, **data)
302
- click.echo(f"Edited issue #{item['number']}")
329
+ safe_echo(f"Edited issue #{safe_number(item, number)}")
303
330
 
304
331
 
305
332
  @issue_group.command("delete")
@@ -317,7 +344,7 @@ def issue_delete(ctx: click.Context, repo_name: str | None, identifier: str) ->
317
344
  owner, repo = resolve_repo(repo_name or app.repo)
318
345
  service = IssueService(app.client())
319
346
  service.delete(owner, repo, number)
320
- click.echo(f"Deleted issue #{number}")
347
+ safe_echo(f"Deleted issue #{number}")
321
348
 
322
349
 
323
350
  @issue_group.command("status")
@@ -328,10 +355,10 @@ def issue_status(ctx: click.Context, repo_name: str | None) -> None:
328
355
  owner, repo = resolve_repo(repo_name or app.repo)
329
356
  service = IssueService(app.client())
330
357
  items = service.list(owner, repo, state="open")
331
- click.echo("GitCode-limited approximation of gh issue status")
332
- click.echo(f"Repository open issues for {owner}/{repo}:")
358
+ safe_echo("GitCode-limited approximation of gh issue status")
359
+ safe_echo(f"Repository open issues for {owner}/{repo}:")
333
360
  for item in items:
334
- click.echo(f" #{item['number']}\t{item['state']}\t{item['title']}")
361
+ safe_echo(f" #{safe_number(item, '?')}\t{item['state']}\t{item['title']}")
335
362
 
336
363
 
337
364
  issue_group.add_command(issue_list, name="ls")
@@ -15,7 +15,15 @@ from ..cli_compat import (
15
15
  from ..formatters import format_pr_detail, format_pr_list, output_result
16
16
  from ..repo import resolve_repo
17
17
  from ..services import PullRequestService
18
- from ..utils import get_current_git_branch, open_in_browser, prompt_if_missing, resolve_pr_arg
18
+ from ..utils import (
19
+ get_current_git_branch,
20
+ open_in_browser,
21
+ prompt_if_missing,
22
+ read_body_file,
23
+ resolve_pr_arg,
24
+ safe_echo,
25
+ safe_number,
26
+ )
19
27
 
20
28
 
21
29
  @click.group("pr")
@@ -81,7 +89,7 @@ def pr_list(
81
89
  def default_formatter(data):
82
90
  output = format_pr_list(data)
83
91
  if output:
84
- click.echo(output)
92
+ safe_echo(output)
85
93
 
86
94
  output_result(
87
95
  items,
@@ -96,6 +104,7 @@ def pr_list(
96
104
  @click.option("-R", "--repo", "repo_name", help="Repository in OWNER/REPO format (default: gitcode.com).")
97
105
  @click.argument("identifier", required=False)
98
106
  @click.option("-w", "--web", is_flag=True, help="Open the pull request in the web browser.")
107
+ @click.option("-c", "--comments", is_flag=True, help="View pull request comments.")
99
108
  @click.option("--json", "json_fields", help="Output JSON. Optionally specify comma-separated fields.")
100
109
  @click.option("-q", "--jq", "jq_query", help="Filter JSON output using a jq expression.")
101
110
  @click.option("-t", "--template", help="Format output using a Go template string.")
@@ -105,6 +114,7 @@ def pr_view(
105
114
  repo_name: str | None,
106
115
  identifier: str | None,
107
116
  web: bool,
117
+ comments: bool,
108
118
  json_fields: str | None,
109
119
  jq_query: str | None,
110
120
  template: str | None,
@@ -119,12 +129,32 @@ def pr_view(
119
129
  if web:
120
130
  open_in_browser(item["html_url"])
121
131
  return
132
+ if comments:
133
+ comment_items = service.list_comments(owner, repo, number)
134
+ data = dict(item) if item else {}
135
+ data["comments"] = comment_items
136
+
137
+ def default_formatter(data: dict) -> None:
138
+ safe_echo(format_pr_detail(data))
139
+ if comment_items:
140
+ safe_echo("\nComments:")
141
+ for comment in comment_items:
142
+ safe_echo(f"- {comment.get('body') or ''}")
143
+
144
+ output_result(
145
+ data,
146
+ json_fields,
147
+ jq_query,
148
+ template,
149
+ default_formatter=default_formatter,
150
+ )
151
+ return
122
152
  output_result(
123
153
  item,
124
154
  json_fields,
125
155
  jq_query,
126
156
  template,
127
- default_formatter=lambda data: click.echo(format_pr_detail(data)),
157
+ default_formatter=lambda data: safe_echo(format_pr_detail(data)),
128
158
  )
129
159
 
130
160
 
@@ -214,7 +244,7 @@ def pr_create(
214
244
  "milestone": milestone,
215
245
  }
216
246
  if dry_run:
217
- click.echo(json.dumps({k: v for k, v in payload.items() if v is not None}, indent=2, sort_keys=True))
247
+ safe_echo(json.dumps({k: v for k, v in payload.items() if v is not None}, indent=2, sort_keys=True))
218
248
  return
219
249
  service = PullRequestService(app.client())
220
250
  item = service.create(owner, repo, **payload)
@@ -223,7 +253,11 @@ def pr_create(
223
253
  json_fields,
224
254
  jq_query,
225
255
  template,
226
- default_formatter=lambda data: click.echo(data["html_url"]),
256
+ default_formatter=lambda data: safe_echo(
257
+ data.get("html_url")
258
+ or data.get("url")
259
+ or (f"Created PR #{data['number']}" if "number" in data else "Created pull request")
260
+ ),
227
261
  )
228
262
 
229
263
 
@@ -250,12 +284,12 @@ def pr_close(
250
284
  branch = item.get("head", {}).get("ref")
251
285
  if branch:
252
286
  subprocess.run(["git", "push", "origin", "--delete", branch], check=True)
253
- click.echo(f"Deleted remote branch {branch}")
287
+ safe_echo(f"Deleted remote branch {branch}")
254
288
  else:
255
- click.echo("Warning: could not determine branch to delete.", err=True)
289
+ safe_echo("Warning: could not determine branch to delete.", err=True)
256
290
  except Exception as exc:
257
- click.echo(f"Warning: could not delete remote branch: {exc}", err=True)
258
- click.echo(f"Closed pull request #{item['number']}")
291
+ safe_echo(f"Warning: could not delete remote branch: {exc}", err=True)
292
+ safe_echo(f"Closed pull request #{safe_number(item, number)}")
259
293
 
260
294
 
261
295
  @pr_group.command("merge")
@@ -264,22 +298,43 @@ def pr_close(
264
298
  @click.option("-m", "--merge", "merge_mode", flag_value="merge")
265
299
  @click.option("-s", "--squash", "merge_mode", flag_value="squash")
266
300
  @click.option("-r", "--rebase", "merge_mode", flag_value="rebase")
301
+ @click.option("-d", "--delete-branch", is_flag=True, help="Delete the remote branch after merge.")
267
302
  @click.pass_context
268
- def pr_merge(ctx: click.Context, repo_name: str | None, identifier: str | None, merge_mode: str | None) -> None:
303
+ def pr_merge(
304
+ ctx: click.Context,
305
+ repo_name: str | None,
306
+ identifier: str | None,
307
+ merge_mode: str | None,
308
+ delete_branch: bool,
309
+ ) -> None:
269
310
  app = ctx.obj["app"]
270
311
  owner, repo = resolve_repo(repo_name or app.repo)
271
312
  service = PullRequestService(app.client())
272
313
  resolved_identifier = resolve_pr_identifier_or_current_branch(identifier)
273
314
  owner, repo, number = resolve_pr_arg(resolved_identifier, owner, repo, service)
274
315
  number = int(number)
316
+ pr_item = service.get(owner, repo, number)
275
317
  item = service.merge(owner, repo, number, merge_method=merge_mode or "merge")
276
- click.echo(item["message"])
318
+ safe_echo(item["message"])
319
+ if delete_branch:
320
+ try:
321
+ branch = pr_item.get("head", {}).get("ref")
322
+ if branch:
323
+ subprocess.run(["git", "push", "origin", "--delete", branch], check=True)
324
+ safe_echo(f"Deleted remote branch {branch}")
325
+ else:
326
+ safe_echo("Warning: could not determine branch to delete.", err=True)
327
+ except Exception as exc:
328
+ safe_echo(f"Warning: could not delete remote branch: {exc}", err=True)
277
329
 
278
330
 
279
331
  @pr_group.command("comment")
280
332
  @click.option("-R", "--repo", "repo_name", help="Repository in OWNER/REPO format (default: gitcode.com).")
281
333
  @click.argument("identifier", required=False)
282
334
  @click.option("-b", "--body")
335
+ @click.option("-F", "--body-file")
336
+ @click.option("-e", "--editor", is_flag=True)
337
+ @click.option("-w", "--web", is_flag=True, help="Open the pull request in the web browser.")
283
338
  @click.option("--path")
284
339
  @click.option("--position", type=int)
285
340
  @click.pass_context
@@ -288,6 +343,9 @@ def pr_comment(
288
343
  repo_name: str | None,
289
344
  identifier: str | None,
290
345
  body: str | None,
346
+ body_file: str | None,
347
+ editor: bool,
348
+ web: bool,
291
349
  path: str | None,
292
350
  position: int | None,
293
351
  ) -> None:
@@ -297,9 +355,14 @@ def pr_comment(
297
355
  resolved_identifier = resolve_pr_identifier_or_current_branch(identifier)
298
356
  owner, repo, number = resolve_pr_arg(resolved_identifier, owner, repo, service)
299
357
  number = int(number)
358
+ if web:
359
+ pr_item = service.get(owner, repo, number)
360
+ open_in_browser(pr_item["html_url"])
361
+ return
362
+ body = get_body_from_options(body=body, body_file=body_file, editor=editor)
300
363
  body = prompt_if_missing(body, "Body")
301
364
  item = service.comment(owner, repo, number, body=body, path=path, position=position)
302
- click.echo(str(item["id"]))
365
+ safe_echo(str(item["id"]))
303
366
 
304
367
 
305
368
  @pr_group.command("review")
@@ -337,16 +400,16 @@ def pr_review(
337
400
  raise click.ClickException("Body is required when using --comment or --request-changes.")
338
401
  item = service.comment(owner, repo, number, body=body)
339
402
  if comment:
340
- click.echo("GitCode review API does not support comment reviews; posted a pull request comment instead.")
403
+ safe_echo("GitCode review API does not support comment reviews; posted a pull request comment instead.")
341
404
  else:
342
- click.echo(
405
+ safe_echo(
343
406
  "GitCode review API does not support request-changes reviews; posted a pull request comment instead."
344
407
  )
345
- click.echo(f"Posted pull request comment {item['id']}")
408
+ safe_echo(f"Posted pull request comment {item['id']}")
346
409
  return
347
410
 
348
411
  item = service.review(owner, repo, number, body=body, force=force)
349
- click.echo(f"Reviewed pull request #{number}")
412
+ safe_echo(f"Reviewed pull request #{number}")
350
413
 
351
414
 
352
415
  @pr_group.command("reopen")
@@ -361,7 +424,7 @@ def pr_reopen(ctx: click.Context, repo_name: str | None, identifier: str | None)
361
424
  owner, repo, number = resolve_pr_arg(resolved_identifier, owner, repo, service)
362
425
  number = int(number)
363
426
  item = service.update(owner, repo, number, state="open")
364
- click.echo(f"Reopened pull request #{item['number']}")
427
+ safe_echo(f"Reopened pull request #{safe_number(item, number)}")
365
428
 
366
429
 
367
430
  @pr_group.command("edit")
@@ -369,6 +432,7 @@ def pr_reopen(ctx: click.Context, repo_name: str | None, identifier: str | None)
369
432
  @click.argument("identifier", required=False)
370
433
  @click.option("-t", "--title")
371
434
  @click.option("-b", "--body")
435
+ @click.option("-F", "--body-file")
372
436
  @click.option("-B", "--base")
373
437
  @click.option("-a", "--add-assignee")
374
438
  @click.option("-l", "--add-label")
@@ -376,6 +440,8 @@ def pr_reopen(ctx: click.Context, repo_name: str | None, identifier: str | None)
376
440
  @click.option("--remove-assignee")
377
441
  @click.option("--remove-label")
378
442
  @click.option("--remove-reviewer")
443
+ @click.option("-m", "--milestone")
444
+ @click.option("--remove-milestone", is_flag=True, help="Remove the milestone from the pull request.")
379
445
  @click.pass_context
380
446
  def pr_edit(
381
447
  ctx: click.Context,
@@ -383,6 +449,7 @@ def pr_edit(
383
449
  identifier: str | None,
384
450
  title: str | None,
385
451
  body: str | None,
452
+ body_file: str | None,
386
453
  base: str | None,
387
454
  add_assignee: str | None,
388
455
  add_label: str | None,
@@ -390,6 +457,8 @@ def pr_edit(
390
457
  remove_assignee: str | None,
391
458
  remove_label: str | None,
392
459
  remove_reviewer: str | None,
460
+ milestone: str | None,
461
+ remove_milestone: bool,
393
462
  ) -> None:
394
463
  app = ctx.obj["app"]
395
464
  owner, repo = resolve_repo(repo_name or app.repo)
@@ -397,6 +466,8 @@ def pr_edit(
397
466
  resolved_identifier = resolve_pr_identifier_or_current_branch(identifier)
398
467
  owner, repo, number = resolve_pr_arg(resolved_identifier, owner, repo, service)
399
468
  number = int(number)
469
+ if body_file:
470
+ body = read_body_file(body_file)
400
471
  data = {
401
472
  k: v
402
473
  for k, v in {
@@ -409,13 +480,16 @@ def pr_edit(
409
480
  "unassignee": remove_assignee,
410
481
  "unset_labels": remove_label,
411
482
  "unset_reviewer": remove_reviewer,
483
+ "milestone": milestone,
412
484
  }.items()
413
485
  if v is not None
414
486
  }
487
+ if remove_milestone:
488
+ data["milestone"] = ""
415
489
  if not data:
416
490
  raise click.UsageError("must specify at least one field to edit")
417
491
  item = service.update(owner, repo, number, **data)
418
- click.echo(f"Edited pull request #{item['number']}")
492
+ safe_echo(f"Edited pull request #{safe_number(item, number)}")
419
493
 
420
494
 
421
495
  @pr_group.command("diff")
@@ -429,7 +503,7 @@ def pr_diff(ctx: click.Context, repo_name: str | None, identifier: str | None) -
429
503
  resolved_identifier = resolve_pr_identifier_or_current_branch(identifier)
430
504
  owner, repo, number = resolve_pr_arg(resolved_identifier, owner, repo, service)
431
505
  diff_text = service.diff(owner, repo, int(number))
432
- click.echo(diff_text)
506
+ safe_echo(diff_text)
433
507
 
434
508
 
435
509
  @pr_group.command("checkout")
@@ -451,7 +525,7 @@ def pr_checkout(ctx: click.Context, repo_name: str | None, identifier: str | Non
451
525
  try:
452
526
  subprocess.run(["git", "fetch", "origin", head_ref], check=True)
453
527
  subprocess.run(["git", "checkout", "-b", local_branch, f"origin/{head_ref}"], check=True)
454
- click.echo(f"Checked out branch {local_branch}")
528
+ safe_echo(f"Checked out branch {local_branch}")
455
529
  except subprocess.CalledProcessError as exc:
456
530
  raise click.ClickException(f"Git checkout failed: {exc}") from exc
457
531
 
@@ -469,9 +543,9 @@ def pr_ready(ctx: click.Context, repo_name: str | None, identifier: str | None,
469
543
  owner, repo, number = resolve_pr_arg(resolved_identifier, owner, repo, service)
470
544
  item = service.update(owner, repo, int(number), draft=undo)
471
545
  if undo:
472
- click.echo(f"Converted pull request #{item['number']} to draft")
546
+ safe_echo(f"Converted pull request #{safe_number(item, number)} to draft")
473
547
  else:
474
- click.echo(f"Marked pull request #{item['number']} as ready for review")
548
+ safe_echo(f"Marked pull request #{safe_number(item, number)} as ready for review")
475
549
 
476
550
 
477
551
  @pr_group.command("status")
@@ -482,14 +556,14 @@ def pr_status(ctx: click.Context, repo_name: str | None) -> None:
482
556
  owner, repo = resolve_repo(repo_name or app.repo)
483
557
  service = PullRequestService(app.client())
484
558
  items = service.list(owner, repo, state="open")
485
- click.echo(
559
+ safe_echo(
486
560
  f"Open pull requests in {owner}/{repo} (GitCode API approximation -- user-specific filtering is not available)"
487
561
  )
488
562
  if items:
489
563
  for item in items:
490
- click.echo(f" #{item['number']}\t{item['state']}\t{item['title']}")
564
+ safe_echo(f" #{safe_number(item, '?')}\t{item['state']}\t{item['title']}")
491
565
  else:
492
- click.echo(" No open pull requests")
566
+ safe_echo(" No open pull requests")
493
567
 
494
568
 
495
569
  pr_group.add_command(pr_list, name="ls")
@@ -7,6 +7,8 @@ import subprocess
7
7
 
8
8
  import click
9
9
 
10
+ from .utils import safe_echo
11
+
10
12
 
11
13
  def dump_json(data, fields: list[str] | None = None) -> str:
12
14
  if fields:
@@ -147,17 +149,17 @@ def render_template(data, template: str) -> str:
147
149
  def output_result(data, json_fields: str | None, jq_query: str | None, template: str | None, default_formatter):
148
150
  if jq_query:
149
151
  data = apply_jq(data, jq_query)
150
- click.echo(dump_json(data))
152
+ safe_echo(dump_json(data))
151
153
  return
152
154
  if json_fields:
153
155
  fields = [f.strip() for f in json_fields.split(",")]
154
- click.echo(dump_json(data, fields=fields))
156
+ safe_echo(dump_json(data, fields=fields))
155
157
  return
156
158
  if template:
157
159
  if isinstance(data, list):
158
160
  for item in data:
159
- click.echo(render_template(item, template))
161
+ safe_echo(render_template(item, template))
160
162
  else:
161
- click.echo(render_template(data, template))
163
+ safe_echo(render_template(data, template))
162
164
  return
163
165
  default_formatter(data)
@@ -41,6 +41,9 @@ class PullRequestService:
41
41
  filtered_payload = {k: v for k, v in payload.items() if v is not None}
42
42
  return self.client.post(f"/repos/{owner}/{repo}/pulls/{number}/review", json=filtered_payload)
43
43
 
44
+ def list_comments(self, owner: str, repo: str, number: int) -> Any | None:
45
+ return self.client.get(f"/repos/{owner}/{repo}/pulls/{number}/comments")
46
+
44
47
  def diff(self, owner: str, repo: str, number: int) -> str:
45
48
  response = self.client.request(
46
49
  "GET",
@@ -2,9 +2,10 @@ from __future__ import annotations
2
2
 
3
3
  import re
4
4
  import subprocess
5
+ import sys
5
6
  import webbrowser
6
7
  from pathlib import Path
7
- from typing import TYPE_CHECKING
8
+ from typing import TYPE_CHECKING, Any
8
9
 
9
10
  import click
10
11
 
@@ -60,6 +61,15 @@ def open_in_browser(url: str) -> None:
60
61
  webbrowser.open(url)
61
62
 
62
63
 
64
+ def safe_number(item: Any, fallback: int | str) -> int | str:
65
+ if isinstance(item, dict):
66
+ if "number" in item and item["number"] is not None:
67
+ return item["number"]
68
+ if "iid" in item and item["iid"] is not None:
69
+ return item["iid"]
70
+ return fallback
71
+
72
+
63
73
  # --- Issue / PR identifier resolvers ---
64
74
 
65
75
  ISSUE_URL_RE = re.compile(r"https?://[^/]+/(?P<owner>[^/]+)/(?P<repo>[^/]+)/issues/(?P<number>\d+)")
@@ -116,3 +126,13 @@ def resolve_pr_arg(identifier: str, owner: str, repo: str, service: PullRequestS
116
126
  return owner, repo, str(item["number"])
117
127
 
118
128
  raise click.ClickException(f"No open pull request found for branch '{identifier}'.")
129
+
130
+
131
+ def safe_echo(message: str | None = None, err: bool = False) -> None:
132
+ try:
133
+ click.echo(message, err=err)
134
+ except UnicodeEncodeError:
135
+ stream = sys.stderr if err else sys.stdout
136
+ encoding = stream.encoding or "utf-8"
137
+ encoded = (message or "").encode(encoding, errors="replace").decode(encoding, errors="replace")
138
+ click.echo(encoded, err=err)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pygitcode
3
- Version: 0.1.1
3
+ Version: 0.1.2a0
4
4
  Summary: A CLI tool for GitCode (api.gitcode.com), modeled after GitHub CLI.
5
5
  Author-email: codeasier <825770651@qq.com>
6
6
  License-Expression: MIT
@@ -45,7 +45,6 @@ Dynamic: license-file
45
45
  [![PyPI](https://img.shields.io/pypi/v/pygitcode)](https://pypi.org/project/pygitcode/)
46
46
  [![Python](https://img.shields.io/pypi/pyversions/pygitcode)](https://pypi.org/project/pygitcode/)
47
47
  [![License](https://img.shields.io/pypi/l/pygitcode)](https://github.com/yourusername/pygitcode/blob/main/LICENSE)
48
- [![Tests](https://github.com/codeasier/gitcode-cli/workflows/Tests/badge.svg)](https://github.com/yourusername/pygitcode/actions)
49
48
 
50
49
  ## Installation
51
50
 
@@ -83,6 +82,15 @@ Set-Alias -Name gc -Value 'C:\Users\<user>\AppData\Roaming\Python\Python313\Scri
83
82
  # Login with your GitCode personal access token
84
83
  gc auth login
85
84
 
85
+ # Check auth status
86
+ gc auth status
87
+
88
+ # Print the current auth token
89
+ gc auth token
90
+
91
+ # Logout
92
+ gc auth logout
93
+
86
94
  # Or set environment variable
87
95
  export GC_TOKEN=your_token_here
88
96
  ```
@@ -103,12 +111,14 @@ gc issue view 42 --web
103
111
 
104
112
  # Create an issue
105
113
  gc issue create -t "Bug report" -b "Something is broken"
114
+ gc issue create --label bug --label "help wanted" # Multiple labels
106
115
  gc issue create --web # Create in browser
107
116
 
108
- # Edit an issue (add/remove labels, assignees)
117
+ # Edit an issue (add/remove labels, assignees, milestones)
109
118
  gc issue edit 42 -t "Updated title"
110
119
  gc issue edit 42 --add-label bug --remove-label "in progress"
111
120
  gc issue edit 42 --add-assignee @me --remove-assignee otheruser
121
+ gc issue edit 42 --milestone v1.0 --remove-milestone
112
122
 
113
123
  # Comment on an issue
114
124
  gc issue comment 42 -b "Thanks for the report!"
@@ -116,8 +126,7 @@ gc issue comment 42 --editor # Use system editor
116
126
 
117
127
  # Close / reopen / delete
118
128
  gc issue close 42
119
- gc issue reopen 42
120
- gc issue delete 42
129
+ gc issue close 42 -c "Fixed in #50" --reason completed
121
130
  ```
122
131
 
123
132
  ### Pull Requests
@@ -132,6 +141,7 @@ gc pr list --web # Open in browser
132
141
  # View a PR (identifier optional - infers from current branch)
133
142
  gc pr view # View PR for current branch
134
143
  gc pr view 42 # By number
144
+ gc pr view 42 --comments # Include comments
135
145
  gc pr view https://gitcode.com/owner/repo/pulls/42
136
146
  gc pr view feature-branch
137
147
 
@@ -160,11 +170,13 @@ gc pr close --delete-branch # Delete remote branch too
160
170
  gc pr merge # Merge PR for current branch
161
171
  gc pr merge 42 -s # Squash merge
162
172
  gc pr merge --rebase
173
+ gc pr merge --delete-branch # Delete remote branch after merge
163
174
 
164
- # Edit a PR (add/remove labels, assignees, reviewers)
175
+ # Edit a PR (add/remove labels, assignees, reviewers, milestones)
165
176
  gc pr edit 42 -t "New title"
166
177
  gc pr edit --add-label bug --remove-label "needs review"
167
178
  gc pr edit --add-reviewer @me --remove-reviewer otheruser
179
+ gc pr edit --milestone v1.0 --remove-milestone
168
180
 
169
181
  # Mark as ready or convert to draft
170
182
  gc pr ready # Mark current branch's PR as ready
@@ -173,6 +185,8 @@ gc pr ready --undo # Convert back to draft
173
185
  # Comment / review / diff (identifier optional)
174
186
  gc pr comment -b "LGTM"
175
187
  gc pr comment --path src/file.py --position 5 -b "Suggestion"
188
+ gc pr comment --body-file comment.txt
189
+ gc pr comment --editor
176
190
 
177
191
  gc pr review --approve
178
192
  gc pr review --comment -b "Looks good but needs tests"
@@ -1,23 +0,0 @@
1
- from __future__ import annotations
2
-
3
- import click
4
-
5
- from ..config import save_config
6
-
7
-
8
- @click.group("auth")
9
- def auth_group() -> None:
10
- pass
11
-
12
-
13
- @auth_group.command("login")
14
- @click.option("--with-token", is_flag=True, help="Read token from stdin.")
15
- def auth_login(with_token: bool) -> None:
16
- if with_token:
17
- token = click.get_text_stream("stdin").read().strip()
18
- if not token:
19
- raise click.ClickException("No token provided on stdin.")
20
- else:
21
- token = click.prompt("GitCode token", hide_input=True)
22
- save_config({"token": token})
23
- click.echo("Authentication saved.")
File without changes
File without changes