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.
- {pygitcode-0.1.1 → pygitcode-0.1.2a0}/PKG-INFO +20 -6
- {pygitcode-0.1.1 → pygitcode-0.1.2a0}/README.md +19 -5
- {pygitcode-0.1.1 → pygitcode-0.1.2a0}/pyproject.toml +4 -1
- {pygitcode-0.1.1 → pygitcode-0.1.2a0}/src/gitcode_cli/__init__.py +1 -1
- {pygitcode-0.1.1 → pygitcode-0.1.2a0}/src/gitcode_cli/cli.py +24 -2
- pygitcode-0.1.2a0/src/gitcode_cli/commands/auth.py +54 -0
- {pygitcode-0.1.1 → pygitcode-0.1.2a0}/src/gitcode_cli/commands/issue.py +48 -21
- {pygitcode-0.1.1 → pygitcode-0.1.2a0}/src/gitcode_cli/commands/pr.py +99 -25
- {pygitcode-0.1.1 → pygitcode-0.1.2a0}/src/gitcode_cli/formatters.py +6 -4
- {pygitcode-0.1.1 → pygitcode-0.1.2a0}/src/gitcode_cli/services/pulls.py +3 -0
- {pygitcode-0.1.1 → pygitcode-0.1.2a0}/src/gitcode_cli/utils.py +21 -1
- {pygitcode-0.1.1 → pygitcode-0.1.2a0}/src/pygitcode.egg-info/PKG-INFO +20 -6
- pygitcode-0.1.1/src/gitcode_cli/commands/auth.py +0 -23
- {pygitcode-0.1.1 → pygitcode-0.1.2a0}/LICENSE +0 -0
- {pygitcode-0.1.1 → pygitcode-0.1.2a0}/setup.cfg +0 -0
- {pygitcode-0.1.1 → pygitcode-0.1.2a0}/src/gitcode_cli/cli_compat.py +0 -0
- {pygitcode-0.1.1 → pygitcode-0.1.2a0}/src/gitcode_cli/client.py +0 -0
- {pygitcode-0.1.1 → pygitcode-0.1.2a0}/src/gitcode_cli/commands/__init__.py +0 -0
- {pygitcode-0.1.1 → pygitcode-0.1.2a0}/src/gitcode_cli/config.py +0 -0
- {pygitcode-0.1.1 → pygitcode-0.1.2a0}/src/gitcode_cli/context.py +0 -0
- {pygitcode-0.1.1 → pygitcode-0.1.2a0}/src/gitcode_cli/errors.py +0 -0
- {pygitcode-0.1.1 → pygitcode-0.1.2a0}/src/gitcode_cli/repo.py +0 -0
- {pygitcode-0.1.1 → pygitcode-0.1.2a0}/src/gitcode_cli/services/__init__.py +0 -0
- {pygitcode-0.1.1 → pygitcode-0.1.2a0}/src/gitcode_cli/services/issues.py +0 -0
- {pygitcode-0.1.1 → pygitcode-0.1.2a0}/src/pygitcode.egg-info/SOURCES.txt +0 -0
- {pygitcode-0.1.1 → pygitcode-0.1.2a0}/src/pygitcode.egg-info/dependency_links.txt +0 -0
- {pygitcode-0.1.1 → pygitcode-0.1.2a0}/src/pygitcode.egg-info/entry_points.txt +0 -0
- {pygitcode-0.1.1 → pygitcode-0.1.2a0}/src/pygitcode.egg-info/requires.txt +0 -0
- {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.
|
|
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
|
[](https://pypi.org/project/pygitcode/)
|
|
46
46
|
[](https://pypi.org/project/pygitcode/)
|
|
47
47
|
[](https://github.com/yourusername/pygitcode/blob/main/LICENSE)
|
|
48
|
-
[](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
|
|
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
|
[](https://pypi.org/project/pygitcode/)
|
|
6
6
|
[](https://pypi.org/project/pygitcode/)
|
|
7
7
|
[](https://github.com/yourusername/pygitcode/blob/main/LICENSE)
|
|
8
|
-
[](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
|
|
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
|
-
|
|
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,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=
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
121
|
+
safe_echo(format_issue_detail(data))
|
|
122
122
|
if comment_items:
|
|
123
|
-
|
|
123
|
+
safe_echo("\nComments:")
|
|
124
124
|
for comment in comment_items:
|
|
125
|
-
|
|
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:
|
|
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(
|
|
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:
|
|
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(
|
|
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
|
-
|
|
204
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
332
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
287
|
+
safe_echo(f"Deleted remote branch {branch}")
|
|
254
288
|
else:
|
|
255
|
-
|
|
289
|
+
safe_echo("Warning: could not determine branch to delete.", err=True)
|
|
256
290
|
except Exception as exc:
|
|
257
|
-
|
|
258
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
403
|
+
safe_echo("GitCode review API does not support comment reviews; posted a pull request comment instead.")
|
|
341
404
|
else:
|
|
342
|
-
|
|
405
|
+
safe_echo(
|
|
343
406
|
"GitCode review API does not support request-changes reviews; posted a pull request comment instead."
|
|
344
407
|
)
|
|
345
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
546
|
+
safe_echo(f"Converted pull request #{safe_number(item, number)} to draft")
|
|
473
547
|
else:
|
|
474
|
-
|
|
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
|
-
|
|
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
|
-
|
|
564
|
+
safe_echo(f" #{safe_number(item, '?')}\t{item['state']}\t{item['title']}")
|
|
491
565
|
else:
|
|
492
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
161
|
+
safe_echo(render_template(item, template))
|
|
160
162
|
else:
|
|
161
|
-
|
|
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.
|
|
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
|
[](https://pypi.org/project/pygitcode/)
|
|
46
46
|
[](https://pypi.org/project/pygitcode/)
|
|
47
47
|
[](https://github.com/yourusername/pygitcode/blob/main/LICENSE)
|
|
48
|
-
[](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
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|