pygitcode 0.1.0__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.0 → pygitcode-0.1.2a0}/PKG-INFO +41 -7
  2. {pygitcode-0.1.0 → pygitcode-0.1.2a0}/README.md +41 -7
  3. {pygitcode-0.1.0 → pygitcode-0.1.2a0}/pyproject.toml +5 -1
  4. {pygitcode-0.1.0 → pygitcode-0.1.2a0}/src/gitcode_cli/__init__.py +1 -1
  5. {pygitcode-0.1.0 → pygitcode-0.1.2a0}/src/gitcode_cli/cli.py +25 -3
  6. pygitcode-0.1.2a0/src/gitcode_cli/commands/auth.py +54 -0
  7. {pygitcode-0.1.0 → pygitcode-0.1.2a0}/src/gitcode_cli/commands/issue.py +57 -30
  8. {pygitcode-0.1.0 → pygitcode-0.1.2a0}/src/gitcode_cli/commands/pr.py +113 -40
  9. {pygitcode-0.1.0 → pygitcode-0.1.2a0}/src/gitcode_cli/formatters.py +6 -4
  10. {pygitcode-0.1.0 → pygitcode-0.1.2a0}/src/gitcode_cli/services/pulls.py +3 -0
  11. {pygitcode-0.1.0 → pygitcode-0.1.2a0}/src/gitcode_cli/utils.py +21 -1
  12. {pygitcode-0.1.0 → pygitcode-0.1.2a0}/src/pygitcode.egg-info/PKG-INFO +41 -7
  13. {pygitcode-0.1.0 → pygitcode-0.1.2a0}/src/pygitcode.egg-info/entry_points.txt +1 -0
  14. pygitcode-0.1.0/src/gitcode_cli/commands/auth.py +0 -23
  15. {pygitcode-0.1.0 → pygitcode-0.1.2a0}/LICENSE +0 -0
  16. {pygitcode-0.1.0 → pygitcode-0.1.2a0}/setup.cfg +0 -0
  17. {pygitcode-0.1.0 → pygitcode-0.1.2a0}/src/gitcode_cli/cli_compat.py +0 -0
  18. {pygitcode-0.1.0 → pygitcode-0.1.2a0}/src/gitcode_cli/client.py +0 -0
  19. {pygitcode-0.1.0 → pygitcode-0.1.2a0}/src/gitcode_cli/commands/__init__.py +0 -0
  20. {pygitcode-0.1.0 → pygitcode-0.1.2a0}/src/gitcode_cli/config.py +0 -0
  21. {pygitcode-0.1.0 → pygitcode-0.1.2a0}/src/gitcode_cli/context.py +0 -0
  22. {pygitcode-0.1.0 → pygitcode-0.1.2a0}/src/gitcode_cli/errors.py +0 -0
  23. {pygitcode-0.1.0 → pygitcode-0.1.2a0}/src/gitcode_cli/repo.py +0 -0
  24. {pygitcode-0.1.0 → pygitcode-0.1.2a0}/src/gitcode_cli/services/__init__.py +0 -0
  25. {pygitcode-0.1.0 → pygitcode-0.1.2a0}/src/gitcode_cli/services/issues.py +0 -0
  26. {pygitcode-0.1.0 → pygitcode-0.1.2a0}/src/pygitcode.egg-info/SOURCES.txt +0 -0
  27. {pygitcode-0.1.0 → pygitcode-0.1.2a0}/src/pygitcode.egg-info/dependency_links.txt +0 -0
  28. {pygitcode-0.1.0 → pygitcode-0.1.2a0}/src/pygitcode.egg-info/requires.txt +0 -0
  29. {pygitcode-0.1.0 → 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.0
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/yourusername/pygitcode/workflows/Tests/badge.svg)](https://github.com/yourusername/pygitcode/actions)
49
48
 
50
49
  ## Installation
51
50
 
@@ -53,7 +52,27 @@ Dynamic: license-file
53
52
  pip install pygitcode
54
53
  ```
55
54
 
56
- This exposes the `gc` command in your shell.
55
+ This exposes the `gc` (or `gitcode`) command in your shell.
56
+
57
+ ## Windows PowerShell Users
58
+
59
+ On Windows PowerShell, `gc` is a built-in alias for the `Get-Content` cmdlet, which shadows the GitCode CLI. Use `gitcode` instead:
60
+
61
+ ```powershell
62
+ gitcode auth login
63
+ gitcode issue list
64
+ gitcode pr list
65
+ ```
66
+
67
+ Alternatively, you can remove or override the alias in your PowerShell profile:
68
+
69
+ ```powershell
70
+ # Remove for the current session
71
+ Remove-Item Alias:gc -Force
72
+
73
+ # Or persist the override in your profile
74
+ Set-Alias -Name gc -Value 'C:\Users\<user>\AppData\Roaming\Python\Python313\Scripts\gc.exe'
75
+ ```
57
76
 
58
77
  ## Quick Start
59
78
 
@@ -63,6 +82,15 @@ This exposes the `gc` command in your shell.
63
82
  # Login with your GitCode personal access token
64
83
  gc auth login
65
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
+
66
94
  # Or set environment variable
67
95
  export GC_TOKEN=your_token_here
68
96
  ```
@@ -83,12 +111,14 @@ gc issue view 42 --web
83
111
 
84
112
  # Create an issue
85
113
  gc issue create -t "Bug report" -b "Something is broken"
114
+ gc issue create --label bug --label "help wanted" # Multiple labels
86
115
  gc issue create --web # Create in browser
87
116
 
88
- # Edit an issue (add/remove labels, assignees)
117
+ # Edit an issue (add/remove labels, assignees, milestones)
89
118
  gc issue edit 42 -t "Updated title"
90
119
  gc issue edit 42 --add-label bug --remove-label "in progress"
91
120
  gc issue edit 42 --add-assignee @me --remove-assignee otheruser
121
+ gc issue edit 42 --milestone v1.0 --remove-milestone
92
122
 
93
123
  # Comment on an issue
94
124
  gc issue comment 42 -b "Thanks for the report!"
@@ -96,8 +126,7 @@ gc issue comment 42 --editor # Use system editor
96
126
 
97
127
  # Close / reopen / delete
98
128
  gc issue close 42
99
- gc issue reopen 42
100
- gc issue delete 42
129
+ gc issue close 42 -c "Fixed in #50" --reason completed
101
130
  ```
102
131
 
103
132
  ### Pull Requests
@@ -112,6 +141,7 @@ gc pr list --web # Open in browser
112
141
  # View a PR (identifier optional - infers from current branch)
113
142
  gc pr view # View PR for current branch
114
143
  gc pr view 42 # By number
144
+ gc pr view 42 --comments # Include comments
115
145
  gc pr view https://gitcode.com/owner/repo/pulls/42
116
146
  gc pr view feature-branch
117
147
 
@@ -140,11 +170,13 @@ gc pr close --delete-branch # Delete remote branch too
140
170
  gc pr merge # Merge PR for current branch
141
171
  gc pr merge 42 -s # Squash merge
142
172
  gc pr merge --rebase
173
+ gc pr merge --delete-branch # Delete remote branch after merge
143
174
 
144
- # Edit a PR (add/remove labels, assignees, reviewers)
175
+ # Edit a PR (add/remove labels, assignees, reviewers, milestones)
145
176
  gc pr edit 42 -t "New title"
146
177
  gc pr edit --add-label bug --remove-label "needs review"
147
178
  gc pr edit --add-reviewer @me --remove-reviewer otheruser
179
+ gc pr edit --milestone v1.0 --remove-milestone
148
180
 
149
181
  # Mark as ready or convert to draft
150
182
  gc pr ready # Mark current branch's PR as ready
@@ -153,6 +185,8 @@ gc pr ready --undo # Convert back to draft
153
185
  # Comment / review / diff (identifier optional)
154
186
  gc pr comment -b "LGTM"
155
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
156
190
 
157
191
  gc pr review --approve
158
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/yourusername/pygitcode/workflows/Tests/badge.svg)](https://github.com/yourusername/pygitcode/actions)
9
8
 
10
9
  ## Installation
11
10
 
@@ -13,7 +12,27 @@
13
12
  pip install pygitcode
14
13
  ```
15
14
 
16
- This exposes the `gc` command in your shell.
15
+ This exposes the `gc` (or `gitcode`) command in your shell.
16
+
17
+ ## Windows PowerShell Users
18
+
19
+ On Windows PowerShell, `gc` is a built-in alias for the `Get-Content` cmdlet, which shadows the GitCode CLI. Use `gitcode` instead:
20
+
21
+ ```powershell
22
+ gitcode auth login
23
+ gitcode issue list
24
+ gitcode pr list
25
+ ```
26
+
27
+ Alternatively, you can remove or override the alias in your PowerShell profile:
28
+
29
+ ```powershell
30
+ # Remove for the current session
31
+ Remove-Item Alias:gc -Force
32
+
33
+ # Or persist the override in your profile
34
+ Set-Alias -Name gc -Value 'C:\Users\<user>\AppData\Roaming\Python\Python313\Scripts\gc.exe'
35
+ ```
17
36
 
18
37
  ## Quick Start
19
38
 
@@ -23,6 +42,15 @@ This exposes the `gc` command in your shell.
23
42
  # Login with your GitCode personal access token
24
43
  gc auth login
25
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
+
26
54
  # Or set environment variable
27
55
  export GC_TOKEN=your_token_here
28
56
  ```
@@ -43,12 +71,14 @@ gc issue view 42 --web
43
71
 
44
72
  # Create an issue
45
73
  gc issue create -t "Bug report" -b "Something is broken"
74
+ gc issue create --label bug --label "help wanted" # Multiple labels
46
75
  gc issue create --web # Create in browser
47
76
 
48
- # Edit an issue (add/remove labels, assignees)
77
+ # Edit an issue (add/remove labels, assignees, milestones)
49
78
  gc issue edit 42 -t "Updated title"
50
79
  gc issue edit 42 --add-label bug --remove-label "in progress"
51
80
  gc issue edit 42 --add-assignee @me --remove-assignee otheruser
81
+ gc issue edit 42 --milestone v1.0 --remove-milestone
52
82
 
53
83
  # Comment on an issue
54
84
  gc issue comment 42 -b "Thanks for the report!"
@@ -56,8 +86,7 @@ gc issue comment 42 --editor # Use system editor
56
86
 
57
87
  # Close / reopen / delete
58
88
  gc issue close 42
59
- gc issue reopen 42
60
- gc issue delete 42
89
+ gc issue close 42 -c "Fixed in #50" --reason completed
61
90
  ```
62
91
 
63
92
  ### Pull Requests
@@ -72,6 +101,7 @@ gc pr list --web # Open in browser
72
101
  # View a PR (identifier optional - infers from current branch)
73
102
  gc pr view # View PR for current branch
74
103
  gc pr view 42 # By number
104
+ gc pr view 42 --comments # Include comments
75
105
  gc pr view https://gitcode.com/owner/repo/pulls/42
76
106
  gc pr view feature-branch
77
107
 
@@ -100,11 +130,13 @@ gc pr close --delete-branch # Delete remote branch too
100
130
  gc pr merge # Merge PR for current branch
101
131
  gc pr merge 42 -s # Squash merge
102
132
  gc pr merge --rebase
133
+ gc pr merge --delete-branch # Delete remote branch after merge
103
134
 
104
- # Edit a PR (add/remove labels, assignees, reviewers)
135
+ # Edit a PR (add/remove labels, assignees, reviewers, milestones)
105
136
  gc pr edit 42 -t "New title"
106
137
  gc pr edit --add-label bug --remove-label "needs review"
107
138
  gc pr edit --add-reviewer @me --remove-reviewer otheruser
139
+ gc pr edit --milestone v1.0 --remove-milestone
108
140
 
109
141
  # Mark as ready or convert to draft
110
142
  gc pr ready # Mark current branch's PR as ready
@@ -113,6 +145,8 @@ gc pr ready --undo # Convert back to draft
113
145
  # Comment / review / diff (identifier optional)
114
146
  gc pr comment -b "LGTM"
115
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
116
150
 
117
151
  gc pr review --approve
118
152
  gc pr review --comment -b "Looks good but needs tests"
@@ -206,4 +240,4 @@ See [ROADMAP.md](ROADMAP.md) for planned features and current gaps.
206
240
 
207
241
  ## License
208
242
 
209
- MIT
243
+ MIT
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "pygitcode"
7
- version = "0.1.0"
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"
@@ -53,10 +53,14 @@ Issues = "https://github.com/yourusername/pygitcode/issues"
53
53
 
54
54
  [project.scripts]
55
55
  gc = "gitcode_cli.cli:main"
56
+ gitcode = "gitcode_cli.cli:main"
56
57
 
57
58
  [tool.setuptools.packages.find]
58
59
  where = ["src"]
59
60
 
61
+ [tool.setuptools.dynamic]
62
+ version = {attr = "gitcode_cli.__version__"}
63
+
60
64
  # --- pytest ---
61
65
  [tool.pytest.ini_options]
62
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="gc")
16
- @click.option("--repo", "repo_name", "-R", help="Select another repository using the [HOST/]OWNER/REPO format.")
36
+ @click.version_option(version=_get_version(), prog_name="gitcode")
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"gc 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")
@@ -21,7 +21,7 @@ def issue_group() -> None:
21
21
 
22
22
 
23
23
  @issue_group.command("list")
24
- @click.option("-R", "--repo", "repo_name", help="Select another repository using the [HOST/]OWNER/REPO format.")
24
+ @click.option("-R", "--repo", "repo_name", help="Repository in OWNER/REPO format (default: gitcode.com).")
25
25
  @click.option("-s", "--state")
26
26
  @click.option("-l", "--label", "labels", multiple=True)
27
27
  @click.option("-A", "--author")
@@ -81,7 +81,7 @@ def issue_list(
81
81
 
82
82
 
83
83
  @issue_group.command("view")
84
- @click.option("-R", "--repo", "repo_name", help="Select another repository using the [HOST/]OWNER/REPO format.")
84
+ @click.option("-R", "--repo", "repo_name", help="Repository in OWNER/REPO format (default: gitcode.com).")
85
85
  @click.argument("identifier")
86
86
  @click.option("-w", "--web", is_flag=True, help="Open the issue in the web browser.")
87
87
  @click.option("-c", "--comments", is_flag=True, help="View issue comments.")
@@ -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,16 +137,16 @@ 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
 
144
144
  @issue_group.command("create")
145
- @click.option("-R", "--repo", "repo_name", help="Select another repository using the [HOST/]OWNER/REPO format.")
145
+ @click.option("-R", "--repo", "repo_name", help="Repository in OWNER/REPO format (default: gitcode.com).")
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
- @click.option("-R", "--repo", "repo_name", help="Select another repository using the [HOST/]OWNER/REPO format.")
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,12 +211,17 @@ 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")
208
- @click.option("-R", "--repo", "repo_name", help="Select another repository using the [HOST/]OWNER/REPO format.")
224
+ @click.option("-R", "--repo", "repo_name", help="Repository in OWNER/REPO format (default: gitcode.com).")
209
225
  @click.argument("identifier")
210
226
  @click.option("-b", "--body")
211
227
  @click.option("-F", "--body-file")
@@ -235,11 +251,11 @@ 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")
242
- @click.option("-R", "--repo", "repo_name", help="Select another repository using the [HOST/]OWNER/REPO format.")
258
+ @click.option("-R", "--repo", "repo_name", help="Repository in OWNER/REPO format (default: gitcode.com).")
243
259
  @click.argument("identifier")
244
260
  @click.pass_context
245
261
  def issue_reopen(ctx: click.Context, repo_name: str | None, identifier: str) -> None:
@@ -252,18 +268,21 @@ 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")
259
- @click.option("-R", "--repo", "repo_name", help="Select another repository using the [HOST/]OWNER/REPO format.")
275
+ @click.option("-R", "--repo", "repo_name", help="Repository in OWNER/REPO format (default: gitcode.com).")
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,17 +316,21 @@ 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")
306
- @click.option("-R", "--repo", "repo_name", help="Select another repository using the [HOST/]OWNER/REPO format.")
333
+ @click.option("-R", "--repo", "repo_name", help="Repository in OWNER/REPO format (default: gitcode.com).")
307
334
  @click.argument("identifier")
308
335
  @click.confirmation_option(prompt="Are you sure you want to delete this issue?")
309
336
  @click.pass_context
@@ -317,21 +344,21 @@ 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")
324
- @click.option("-R", "--repo", "repo_name", help="Select another repository using the [HOST/]OWNER/REPO format.")
351
+ @click.option("-R", "--repo", "repo_name", help="Repository in OWNER/REPO format (default: gitcode.com).")
325
352
  @click.pass_context
326
353
  def issue_status(ctx: click.Context, repo_name: str | None) -> None:
327
354
  app = ctx.obj["app"]
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")
@@ -24,7 +32,7 @@ def pr_group() -> None:
24
32
 
25
33
 
26
34
  @pr_group.command("list")
27
- @click.option("-R", "--repo", "repo_name", help="Select another repository using the [HOST/]OWNER/REPO format.")
35
+ @click.option("-R", "--repo", "repo_name", help="Repository in OWNER/REPO format (default: gitcode.com).")
28
36
  @click.option("-s", "--state")
29
37
  @click.option("-A", "--author")
30
38
  @click.option("-B", "--base")
@@ -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,
@@ -93,9 +101,10 @@ def pr_list(
93
101
 
94
102
 
95
103
  @pr_group.command("view")
96
- @click.option("-R", "--repo", "repo_name", help="Select another repository using the [HOST/]OWNER/REPO format.")
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,17 +129,37 @@ 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
 
131
161
  @pr_group.command("create")
132
- @click.option("-R", "--repo", "repo_name", help="Select another repository using the [HOST/]OWNER/REPO format.")
162
+ @click.option("-R", "--repo", "repo_name", help="Repository in OWNER/REPO format (default: gitcode.com).")
133
163
  @click.option("-t", "--title")
134
164
  @click.option("-b", "--body")
135
165
  @click.option("-F", "--body-file")
@@ -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,12 +253,16 @@ 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
 
230
264
  @pr_group.command("close")
231
- @click.option("-R", "--repo", "repo_name", help="Select another repository using the [HOST/]OWNER/REPO format.")
265
+ @click.option("-R", "--repo", "repo_name", help="Repository in OWNER/REPO format (default: gitcode.com).")
232
266
  @click.argument("identifier", required=False)
233
267
  @click.option("-c", "--comment")
234
268
  @click.option("-d", "--delete-branch", is_flag=True, help="Delete the remote branch after closing.")
@@ -250,36 +284,57 @@ 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")
262
- @click.option("-R", "--repo", "repo_name", help="Select another repository using the [HOST/]OWNER/REPO format.")
296
+ @click.option("-R", "--repo", "repo_name", help="Repository in OWNER/REPO format (default: gitcode.com).")
263
297
  @click.argument("identifier", required=False)
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
- @click.option("-R", "--repo", "repo_name", help="Select another repository using the [HOST/]OWNER/REPO format.")
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,13 +355,18 @@ 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")
306
- @click.option("-R", "--repo", "repo_name", help="Select another repository using the [HOST/]OWNER/REPO format.")
369
+ @click.option("-R", "--repo", "repo_name", help="Repository in OWNER/REPO format (default: gitcode.com).")
307
370
  @click.argument("identifier", required=False)
308
371
  @click.option("-a", "--approve", is_flag=True, help="Approve the pull request. GitCode maps this to its review API.")
309
372
  @click.option("--body")
@@ -337,20 +400,20 @@ 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")
353
- @click.option("-R", "--repo", "repo_name", help="Select another repository using the [HOST/]OWNER/REPO format.")
416
+ @click.option("-R", "--repo", "repo_name", help="Repository in OWNER/REPO format (default: gitcode.com).")
354
417
  @click.argument("identifier", required=False)
355
418
  @click.pass_context
356
419
  def pr_reopen(ctx: click.Context, repo_name: str | None, identifier: str | None) -> None:
@@ -361,14 +424,15 @@ 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")
368
- @click.option("-R", "--repo", "repo_name", help="Select another repository using the [HOST/]OWNER/REPO format.")
431
+ @click.option("-R", "--repo", "repo_name", help="Repository in OWNER/REPO format (default: gitcode.com).")
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,17 +480,20 @@ 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")
422
- @click.option("-R", "--repo", "repo_name", help="Select another repository using the [HOST/]OWNER/REPO format.")
496
+ @click.option("-R", "--repo", "repo_name", help="Repository in OWNER/REPO format (default: gitcode.com).")
423
497
  @click.argument("identifier", required=False)
424
498
  @click.pass_context
425
499
  def pr_diff(ctx: click.Context, repo_name: str | None, identifier: str | None) -> None:
@@ -429,11 +503,11 @@ 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")
436
- @click.option("-R", "--repo", "repo_name", help="Select another repository using the [HOST/]OWNER/REPO format.")
510
+ @click.option("-R", "--repo", "repo_name", help="Repository in OWNER/REPO format (default: gitcode.com).")
437
511
  @click.argument("identifier", required=False)
438
512
  @click.option("-b", "--branch", help="Local branch name to checkout into.")
439
513
  @click.pass_context
@@ -451,13 +525,13 @@ 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
 
458
532
 
459
533
  @pr_group.command("ready")
460
- @click.option("-R", "--repo", "repo_name", help="Select another repository using the [HOST/]OWNER/REPO format.")
534
+ @click.option("-R", "--repo", "repo_name", help="Repository in OWNER/REPO format (default: gitcode.com).")
461
535
  @click.argument("identifier", required=False)
462
536
  @click.option("--undo", is_flag=True, help="Convert a pull request to draft.")
463
537
  @click.pass_context
@@ -469,28 +543,27 @@ 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")
478
- @click.option("-R", "--repo", "repo_name", help="Select another repository using the [HOST/]OWNER/REPO format.")
552
+ @click.option("-R", "--repo", "repo_name", help="Repository in OWNER/REPO format (default: gitcode.com).")
479
553
  @click.pass_context
480
554
  def pr_status(ctx: click.Context, repo_name: str | None) -> None:
481
555
  app = ctx.obj["app"]
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(
486
- f"Open pull requests in {owner}/{repo} "
487
- "(GitCode API approximation -- user-specific filtering is not available)"
559
+ safe_echo(
560
+ f"Open pull requests in {owner}/{repo} (GitCode API approximation -- user-specific filtering is not available)"
488
561
  )
489
562
  if items:
490
563
  for item in items:
491
- click.echo(f" #{item['number']}\t{item['state']}\t{item['title']}")
564
+ safe_echo(f" #{safe_number(item, '?')}\t{item['state']}\t{item['title']}")
492
565
  else:
493
- click.echo(" No open pull requests")
566
+ safe_echo(" No open pull requests")
494
567
 
495
568
 
496
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.0
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/yourusername/pygitcode/workflows/Tests/badge.svg)](https://github.com/yourusername/pygitcode/actions)
49
48
 
50
49
  ## Installation
51
50
 
@@ -53,7 +52,27 @@ Dynamic: license-file
53
52
  pip install pygitcode
54
53
  ```
55
54
 
56
- This exposes the `gc` command in your shell.
55
+ This exposes the `gc` (or `gitcode`) command in your shell.
56
+
57
+ ## Windows PowerShell Users
58
+
59
+ On Windows PowerShell, `gc` is a built-in alias for the `Get-Content` cmdlet, which shadows the GitCode CLI. Use `gitcode` instead:
60
+
61
+ ```powershell
62
+ gitcode auth login
63
+ gitcode issue list
64
+ gitcode pr list
65
+ ```
66
+
67
+ Alternatively, you can remove or override the alias in your PowerShell profile:
68
+
69
+ ```powershell
70
+ # Remove for the current session
71
+ Remove-Item Alias:gc -Force
72
+
73
+ # Or persist the override in your profile
74
+ Set-Alias -Name gc -Value 'C:\Users\<user>\AppData\Roaming\Python\Python313\Scripts\gc.exe'
75
+ ```
57
76
 
58
77
  ## Quick Start
59
78
 
@@ -63,6 +82,15 @@ This exposes the `gc` command in your shell.
63
82
  # Login with your GitCode personal access token
64
83
  gc auth login
65
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
+
66
94
  # Or set environment variable
67
95
  export GC_TOKEN=your_token_here
68
96
  ```
@@ -83,12 +111,14 @@ gc issue view 42 --web
83
111
 
84
112
  # Create an issue
85
113
  gc issue create -t "Bug report" -b "Something is broken"
114
+ gc issue create --label bug --label "help wanted" # Multiple labels
86
115
  gc issue create --web # Create in browser
87
116
 
88
- # Edit an issue (add/remove labels, assignees)
117
+ # Edit an issue (add/remove labels, assignees, milestones)
89
118
  gc issue edit 42 -t "Updated title"
90
119
  gc issue edit 42 --add-label bug --remove-label "in progress"
91
120
  gc issue edit 42 --add-assignee @me --remove-assignee otheruser
121
+ gc issue edit 42 --milestone v1.0 --remove-milestone
92
122
 
93
123
  # Comment on an issue
94
124
  gc issue comment 42 -b "Thanks for the report!"
@@ -96,8 +126,7 @@ gc issue comment 42 --editor # Use system editor
96
126
 
97
127
  # Close / reopen / delete
98
128
  gc issue close 42
99
- gc issue reopen 42
100
- gc issue delete 42
129
+ gc issue close 42 -c "Fixed in #50" --reason completed
101
130
  ```
102
131
 
103
132
  ### Pull Requests
@@ -112,6 +141,7 @@ gc pr list --web # Open in browser
112
141
  # View a PR (identifier optional - infers from current branch)
113
142
  gc pr view # View PR for current branch
114
143
  gc pr view 42 # By number
144
+ gc pr view 42 --comments # Include comments
115
145
  gc pr view https://gitcode.com/owner/repo/pulls/42
116
146
  gc pr view feature-branch
117
147
 
@@ -140,11 +170,13 @@ gc pr close --delete-branch # Delete remote branch too
140
170
  gc pr merge # Merge PR for current branch
141
171
  gc pr merge 42 -s # Squash merge
142
172
  gc pr merge --rebase
173
+ gc pr merge --delete-branch # Delete remote branch after merge
143
174
 
144
- # Edit a PR (add/remove labels, assignees, reviewers)
175
+ # Edit a PR (add/remove labels, assignees, reviewers, milestones)
145
176
  gc pr edit 42 -t "New title"
146
177
  gc pr edit --add-label bug --remove-label "needs review"
147
178
  gc pr edit --add-reviewer @me --remove-reviewer otheruser
179
+ gc pr edit --milestone v1.0 --remove-milestone
148
180
 
149
181
  # Mark as ready or convert to draft
150
182
  gc pr ready # Mark current branch's PR as ready
@@ -153,6 +185,8 @@ gc pr ready --undo # Convert back to draft
153
185
  # Comment / review / diff (identifier optional)
154
186
  gc pr comment -b "LGTM"
155
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
156
190
 
157
191
  gc pr review --approve
158
192
  gc pr review --comment -b "Looks good but needs tests"
@@ -1,2 +1,3 @@
1
1
  [console_scripts]
2
2
  gc = gitcode_cli.cli:main
3
+ gitcode = gitcode_cli.cli:main
@@ -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