pygitcode 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- gitcode_cli/__init__.py +3 -0
- gitcode_cli/cli.py +44 -0
- gitcode_cli/cli_compat.py +116 -0
- gitcode_cli/client.py +71 -0
- gitcode_cli/commands/__init__.py +0 -0
- gitcode_cli/commands/auth.py +23 -0
- gitcode_cli/commands/issue.py +338 -0
- gitcode_cli/commands/pr.py +497 -0
- gitcode_cli/config.py +39 -0
- gitcode_cli/context.py +14 -0
- gitcode_cli/errors.py +23 -0
- gitcode_cli/formatters.py +163 -0
- gitcode_cli/repo.py +43 -0
- gitcode_cli/services/__init__.py +6 -0
- gitcode_cli/services/issues.py +33 -0
- gitcode_cli/services/pulls.py +51 -0
- gitcode_cli/utils.py +118 -0
- pygitcode-0.1.0.dist-info/METADATA +249 -0
- pygitcode-0.1.0.dist-info/RECORD +23 -0
- pygitcode-0.1.0.dist-info/WHEEL +5 -0
- pygitcode-0.1.0.dist-info/entry_points.txt +2 -0
- pygitcode-0.1.0.dist-info/licenses/LICENSE +21 -0
- pygitcode-0.1.0.dist-info/top_level.txt +1 -0
gitcode_cli/__init__.py
ADDED
gitcode_cli/cli.py
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import click
|
|
4
|
+
|
|
5
|
+
from . import __version__
|
|
6
|
+
from .commands.auth import auth_group
|
|
7
|
+
from .commands.issue import issue_group
|
|
8
|
+
from .commands.pr import pr_group
|
|
9
|
+
from .config import get_token
|
|
10
|
+
from .context import AppContext
|
|
11
|
+
from .errors import GCError
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@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.")
|
|
17
|
+
@click.option("--token", hidden=True, help="Override authentication token.")
|
|
18
|
+
@click.pass_context
|
|
19
|
+
def main(ctx: click.Context, repo_name: str | None, token: str | None) -> None:
|
|
20
|
+
ctx.ensure_object(dict)
|
|
21
|
+
try:
|
|
22
|
+
resolved_token = token or get_token()
|
|
23
|
+
except GCError:
|
|
24
|
+
resolved_token = token
|
|
25
|
+
ctx.obj["app"] = AppContext(token=resolved_token or "", repo=repo_name)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@main.result_callback()
|
|
29
|
+
def process_result(*_args: object, **_kwargs: object) -> None:
|
|
30
|
+
return None
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@main.command("version")
|
|
34
|
+
def version_command() -> None:
|
|
35
|
+
click.echo(f"gc version {__version__}")
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
main.add_command(auth_group)
|
|
39
|
+
main.add_command(issue_group)
|
|
40
|
+
main.add_command(pr_group)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
if __name__ == "__main__": # pragma: no cover
|
|
44
|
+
main()
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import subprocess
|
|
4
|
+
import sys
|
|
5
|
+
|
|
6
|
+
import click
|
|
7
|
+
|
|
8
|
+
from .utils import get_current_git_branch, get_default_git_branch, read_body_file
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def get_body_from_options(body: str | None, body_file: str | None, editor: bool) -> str | None:
|
|
12
|
+
"""Resolve body text from inline text, file/stdin, or editor."""
|
|
13
|
+
if body is not None:
|
|
14
|
+
return body
|
|
15
|
+
if body_file:
|
|
16
|
+
if body_file == "-":
|
|
17
|
+
return sys.stdin.read()
|
|
18
|
+
return read_body_file(body_file)
|
|
19
|
+
if editor:
|
|
20
|
+
return click.edit()
|
|
21
|
+
return None
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def get_default_base_branch() -> str:
|
|
25
|
+
"""Return the detected default git branch for CLI base-branch inference."""
|
|
26
|
+
branch = get_default_git_branch()
|
|
27
|
+
if not branch:
|
|
28
|
+
raise click.ClickException("Unable to determine the default base branch. Use --base.")
|
|
29
|
+
return branch
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def normalize_multi_values(values: tuple[str, ...] | None) -> str | None:
|
|
33
|
+
"""Normalize repeatable click option values for API calls."""
|
|
34
|
+
if not values:
|
|
35
|
+
return None
|
|
36
|
+
return ",".join(values)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def resolve_pr_identifier_or_current_branch(identifier: str | None) -> str:
|
|
40
|
+
"""Return the given PR identifier or fall back to the current branch name."""
|
|
41
|
+
if identifier is not None:
|
|
42
|
+
return identifier
|
|
43
|
+
branch = get_current_git_branch()
|
|
44
|
+
if not branch:
|
|
45
|
+
raise click.ClickException("Unable to detect current branch. Specify a PR or branch explicitly.")
|
|
46
|
+
return branch
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def get_fill_info(mode: str = "last") -> tuple[str, str]:
|
|
50
|
+
"""Get title and body from git commits on current branch.
|
|
51
|
+
|
|
52
|
+
Args:
|
|
53
|
+
mode: "last" (latest commit), "first" (first commit), or "verbose" (all commits)
|
|
54
|
+
|
|
55
|
+
Returns:
|
|
56
|
+
(title, body) tuple
|
|
57
|
+
"""
|
|
58
|
+
base_branch = get_default_git_branch() or "main"
|
|
59
|
+
current_branch = get_current_git_branch()
|
|
60
|
+
if not current_branch:
|
|
61
|
+
raise click.ClickException("Unable to detect current branch for --fill.")
|
|
62
|
+
|
|
63
|
+
try:
|
|
64
|
+
if mode == "last":
|
|
65
|
+
result = subprocess.run(
|
|
66
|
+
["git", "log", "-1", "--format=%s%n%n%b", "--no-merges"],
|
|
67
|
+
capture_output=True,
|
|
68
|
+
text=True,
|
|
69
|
+
check=True,
|
|
70
|
+
)
|
|
71
|
+
elif mode == "first":
|
|
72
|
+
result = subprocess.run(
|
|
73
|
+
["git", "log", f"{base_branch}..{current_branch}", "--format=%s%n%n%b", "--no-merges", "--reverse"],
|
|
74
|
+
capture_output=True,
|
|
75
|
+
text=True,
|
|
76
|
+
check=True,
|
|
77
|
+
)
|
|
78
|
+
lines = result.stdout.strip().split("\n\n", 1)
|
|
79
|
+
result = subprocess.run(
|
|
80
|
+
["echo", lines[0] if lines else ""],
|
|
81
|
+
capture_output=True,
|
|
82
|
+
text=True,
|
|
83
|
+
check=True,
|
|
84
|
+
)
|
|
85
|
+
else:
|
|
86
|
+
result = subprocess.run(
|
|
87
|
+
["git", "log", f"{base_branch}..{current_branch}", "--format=%s%n%n%b", "--no-merges"],
|
|
88
|
+
capture_output=True,
|
|
89
|
+
text=True,
|
|
90
|
+
check=True,
|
|
91
|
+
)
|
|
92
|
+
except subprocess.CalledProcessError as exc:
|
|
93
|
+
raise click.ClickException(f"Failed to get commit info: {exc}") from exc
|
|
94
|
+
|
|
95
|
+
output = result.stdout.strip()
|
|
96
|
+
if not output:
|
|
97
|
+
return "", ""
|
|
98
|
+
|
|
99
|
+
parts = output.split("\n\n", 1)
|
|
100
|
+
title = parts[0].strip()
|
|
101
|
+
body = parts[1].strip() if len(parts) > 1 else ""
|
|
102
|
+
|
|
103
|
+
if mode == "verbose" and body:
|
|
104
|
+
lines = []
|
|
105
|
+
commits = subprocess.run(
|
|
106
|
+
["git", "log", f"{base_branch}..{current_branch}", "--format=%s", "--no-merges"],
|
|
107
|
+
capture_output=True,
|
|
108
|
+
text=True,
|
|
109
|
+
check=True,
|
|
110
|
+
)
|
|
111
|
+
for line in commits.stdout.strip().split("\n"):
|
|
112
|
+
if line:
|
|
113
|
+
lines.append(f"- {line}")
|
|
114
|
+
body = "\n".join(lines)
|
|
115
|
+
|
|
116
|
+
return title, body
|
gitcode_cli/client.py
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any, Literal
|
|
4
|
+
from urllib.parse import urljoin
|
|
5
|
+
|
|
6
|
+
import httpx
|
|
7
|
+
|
|
8
|
+
from .errors import APIError
|
|
9
|
+
|
|
10
|
+
BASE_URL = "https://api.gitcode.com/api/v5/"
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class GitCodeClient:
|
|
14
|
+
def __init__(self, token: str, base_url: str = BASE_URL):
|
|
15
|
+
self.token = token
|
|
16
|
+
self.base_url = base_url
|
|
17
|
+
self._client = httpx.Client(timeout=30.0)
|
|
18
|
+
|
|
19
|
+
def request(
|
|
20
|
+
self,
|
|
21
|
+
method: str,
|
|
22
|
+
path: str,
|
|
23
|
+
*,
|
|
24
|
+
params: dict[str, Any] | None = None,
|
|
25
|
+
json: dict[str, Any] | None = None,
|
|
26
|
+
accept: str = "application/json",
|
|
27
|
+
response_format: Literal["json", "text"] = "json",
|
|
28
|
+
) -> Any | None:
|
|
29
|
+
merged_params: dict[str, Any] = {"access_token": self.token}
|
|
30
|
+
if params:
|
|
31
|
+
merged_params.update({k: v for k, v in params.items() if v is not None})
|
|
32
|
+
response = self._client.request(
|
|
33
|
+
method,
|
|
34
|
+
urljoin(self.base_url, path.lstrip("/")),
|
|
35
|
+
params=merged_params,
|
|
36
|
+
json=json,
|
|
37
|
+
headers={"Accept": accept},
|
|
38
|
+
)
|
|
39
|
+
if response.status_code >= 400:
|
|
40
|
+
try:
|
|
41
|
+
data = response.json()
|
|
42
|
+
message = data.get("message") or str(data)
|
|
43
|
+
except Exception:
|
|
44
|
+
message = response.text
|
|
45
|
+
raise APIError(message or "GitCode API request failed", response.status_code)
|
|
46
|
+
if not response.content:
|
|
47
|
+
return None
|
|
48
|
+
if response_format == "text":
|
|
49
|
+
return response.text
|
|
50
|
+
return response.json()
|
|
51
|
+
|
|
52
|
+
def get(self, path: str, *, params: dict[str, Any] | None = None) -> Any | None:
|
|
53
|
+
return self.request("GET", path, params=params)
|
|
54
|
+
|
|
55
|
+
def post(
|
|
56
|
+
self, path: str, *, params: dict[str, Any] | None = None, json: dict[str, Any] | None = None
|
|
57
|
+
) -> Any | None:
|
|
58
|
+
return self.request("POST", path, params=params, json=json)
|
|
59
|
+
|
|
60
|
+
def patch(
|
|
61
|
+
self, path: str, *, params: dict[str, Any] | None = None, json: dict[str, Any] | None = None
|
|
62
|
+
) -> Any | None:
|
|
63
|
+
return self.request("PATCH", path, params=params, json=json)
|
|
64
|
+
|
|
65
|
+
def put(self, path: str, *, params: dict[str, Any] | None = None, json: dict[str, Any] | None = None) -> Any | None:
|
|
66
|
+
return self.request("PUT", path, params=params, json=json)
|
|
67
|
+
|
|
68
|
+
def delete(
|
|
69
|
+
self, path: str, *, params: dict[str, Any] | None = None, json: dict[str, Any] | None = None
|
|
70
|
+
) -> Any | None:
|
|
71
|
+
return self.request("DELETE", path, params=params, json=json)
|
|
File without changes
|
|
@@ -0,0 +1,23 @@
|
|
|
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.")
|
|
@@ -0,0 +1,338 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import click
|
|
4
|
+
|
|
5
|
+
from ..cli_compat import get_body_from_options, normalize_multi_values
|
|
6
|
+
from ..formatters import format_issue_detail, format_issue_list, output_result
|
|
7
|
+
from ..repo import resolve_repo
|
|
8
|
+
from ..services import IssueService
|
|
9
|
+
from ..utils import open_in_browser, prompt_if_missing, read_body_file, resolve_issue_arg
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def _echo_issue_summary(items: list[dict]) -> None:
|
|
13
|
+
output = format_issue_list(items)
|
|
14
|
+
if output:
|
|
15
|
+
click.echo(output)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@click.group("issue")
|
|
19
|
+
def issue_group() -> None:
|
|
20
|
+
pass
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@issue_group.command("list")
|
|
24
|
+
@click.option("-R", "--repo", "repo_name", help="Select another repository using the [HOST/]OWNER/REPO format.")
|
|
25
|
+
@click.option("-s", "--state")
|
|
26
|
+
@click.option("-l", "--label", "labels", multiple=True)
|
|
27
|
+
@click.option("-A", "--author")
|
|
28
|
+
@click.option("-a", "--assignee")
|
|
29
|
+
@click.option("--milestone")
|
|
30
|
+
@click.option("--mention")
|
|
31
|
+
@click.option("-S", "--search")
|
|
32
|
+
@click.option("-L", "--limit", type=int, default=30, show_default=True, help="Maximum number of items to fetch.")
|
|
33
|
+
@click.option("-w", "--web", is_flag=True, help="Open the issue list in the web browser.")
|
|
34
|
+
@click.option("--json", "json_fields", help="Output JSON. Optionally specify comma-separated fields.")
|
|
35
|
+
@click.option("-q", "--jq", "jq_query", help="Filter JSON output using a jq expression.")
|
|
36
|
+
@click.option("-t", "--template", help="Format output using a Go template string.")
|
|
37
|
+
@click.pass_context
|
|
38
|
+
def issue_list(
|
|
39
|
+
ctx: click.Context,
|
|
40
|
+
repo_name: str | None,
|
|
41
|
+
state: str | None,
|
|
42
|
+
labels: tuple[str, ...] | None,
|
|
43
|
+
author: str | None,
|
|
44
|
+
assignee: str | None,
|
|
45
|
+
milestone: str | None,
|
|
46
|
+
mention: str | None,
|
|
47
|
+
search: str | None,
|
|
48
|
+
limit: int | None,
|
|
49
|
+
web: bool,
|
|
50
|
+
json_fields: str | None,
|
|
51
|
+
jq_query: str | None,
|
|
52
|
+
template: str | None,
|
|
53
|
+
) -> None:
|
|
54
|
+
app = ctx.obj["app"]
|
|
55
|
+
owner, repo = resolve_repo(repo_name or app.repo)
|
|
56
|
+
if web:
|
|
57
|
+
open_in_browser(f"https://gitcode.com/{owner}/{repo}/issues")
|
|
58
|
+
return
|
|
59
|
+
service = IssueService(app.client())
|
|
60
|
+
labels_str = normalize_multi_values(labels)
|
|
61
|
+
items = service.list(
|
|
62
|
+
owner,
|
|
63
|
+
repo,
|
|
64
|
+
state=state,
|
|
65
|
+
labels=labels_str,
|
|
66
|
+
creator=author,
|
|
67
|
+
assignee=assignee,
|
|
68
|
+
milestone=milestone,
|
|
69
|
+
mention=mention,
|
|
70
|
+
search=search,
|
|
71
|
+
)
|
|
72
|
+
if limit is not None:
|
|
73
|
+
items = items[:limit]
|
|
74
|
+
output_result(
|
|
75
|
+
items,
|
|
76
|
+
json_fields,
|
|
77
|
+
jq_query,
|
|
78
|
+
template,
|
|
79
|
+
default_formatter=_echo_issue_summary,
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
@issue_group.command("view")
|
|
84
|
+
@click.option("-R", "--repo", "repo_name", help="Select another repository using the [HOST/]OWNER/REPO format.")
|
|
85
|
+
@click.argument("identifier")
|
|
86
|
+
@click.option("-w", "--web", is_flag=True, help="Open the issue in the web browser.")
|
|
87
|
+
@click.option("-c", "--comments", is_flag=True, help="View issue comments.")
|
|
88
|
+
@click.option("--json", "json_fields", help="Output JSON. Optionally specify comma-separated fields.")
|
|
89
|
+
@click.option("-q", "--jq", "jq_query", help="Filter JSON output using a jq expression.")
|
|
90
|
+
@click.option("-t", "--template", help="Format output using a Go template string.")
|
|
91
|
+
@click.pass_context
|
|
92
|
+
def issue_view(
|
|
93
|
+
ctx: click.Context,
|
|
94
|
+
repo_name: str | None,
|
|
95
|
+
identifier: str,
|
|
96
|
+
web: bool,
|
|
97
|
+
comments: bool,
|
|
98
|
+
json_fields: str | None,
|
|
99
|
+
jq_query: str | None,
|
|
100
|
+
template: str | None,
|
|
101
|
+
) -> None:
|
|
102
|
+
app = ctx.obj["app"]
|
|
103
|
+
url_owner, url_repo, number = resolve_issue_arg(identifier)
|
|
104
|
+
if url_owner:
|
|
105
|
+
assert url_repo is not None
|
|
106
|
+
owner, repo = url_owner, url_repo
|
|
107
|
+
else:
|
|
108
|
+
owner, repo = resolve_repo(repo_name or app.repo)
|
|
109
|
+
if web:
|
|
110
|
+
target_url = identifier if url_owner else f"https://gitcode.com/{owner}/{repo}/issues/{number}"
|
|
111
|
+
open_in_browser(target_url)
|
|
112
|
+
return
|
|
113
|
+
service = IssueService(app.client())
|
|
114
|
+
item = service.get(owner, repo, number)
|
|
115
|
+
if comments:
|
|
116
|
+
comment_items = service.list_comments(owner, repo, number)
|
|
117
|
+
data = dict(item) if item else {}
|
|
118
|
+
data["comments"] = comment_items
|
|
119
|
+
|
|
120
|
+
def default_formatter(data: dict) -> None:
|
|
121
|
+
click.echo(format_issue_detail(data))
|
|
122
|
+
if comment_items:
|
|
123
|
+
click.echo("\nComments:")
|
|
124
|
+
for comment in comment_items:
|
|
125
|
+
click.echo(f"- {comment.get('body') or ''}")
|
|
126
|
+
|
|
127
|
+
output_result(
|
|
128
|
+
data,
|
|
129
|
+
json_fields,
|
|
130
|
+
jq_query,
|
|
131
|
+
template,
|
|
132
|
+
default_formatter=default_formatter,
|
|
133
|
+
)
|
|
134
|
+
return
|
|
135
|
+
output_result(
|
|
136
|
+
item,
|
|
137
|
+
json_fields,
|
|
138
|
+
jq_query,
|
|
139
|
+
template,
|
|
140
|
+
default_formatter=lambda data: click.echo(format_issue_detail(data)),
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
@issue_group.command("create")
|
|
145
|
+
@click.option("-R", "--repo", "repo_name", help="Select another repository using the [HOST/]OWNER/REPO format.")
|
|
146
|
+
@click.option("-t", "--title")
|
|
147
|
+
@click.option("-b", "--body")
|
|
148
|
+
@click.option("-a", "--assignee")
|
|
149
|
+
@click.option("-l", "--label", "labels")
|
|
150
|
+
@click.option("-m", "--milestone")
|
|
151
|
+
@click.option("-F", "--body-file")
|
|
152
|
+
@click.option("-w", "--web", is_flag=True, help="Open the issue in the web browser.")
|
|
153
|
+
@click.option("--json", "json_fields", help="Output JSON. Optionally specify comma-separated fields.")
|
|
154
|
+
@click.option("-q", "--jq", "jq_query", help="Filter JSON output using a jq expression.")
|
|
155
|
+
@click.option("--template", help="Format output using a Go template string.")
|
|
156
|
+
@click.pass_context
|
|
157
|
+
def issue_create(
|
|
158
|
+
ctx: click.Context,
|
|
159
|
+
repo_name: str | None,
|
|
160
|
+
title: str | None,
|
|
161
|
+
body: str | None,
|
|
162
|
+
assignee: str | None,
|
|
163
|
+
labels: str | None,
|
|
164
|
+
milestone: str | None,
|
|
165
|
+
body_file: str | None,
|
|
166
|
+
web: bool,
|
|
167
|
+
json_fields: str | None,
|
|
168
|
+
jq_query: str | None,
|
|
169
|
+
template: str | None,
|
|
170
|
+
) -> None:
|
|
171
|
+
app = ctx.obj["app"]
|
|
172
|
+
owner, repo = resolve_repo(repo_name or app.repo)
|
|
173
|
+
if web:
|
|
174
|
+
open_in_browser(f"https://gitcode.com/{owner}/{repo}/issues/new")
|
|
175
|
+
return
|
|
176
|
+
title = prompt_if_missing(title, "Title")
|
|
177
|
+
if body_file:
|
|
178
|
+
body = read_body_file(body_file)
|
|
179
|
+
service = IssueService(app.client())
|
|
180
|
+
item = service.create(owner, repo, title=title, body=body, assignee=assignee, labels=labels, milestone=milestone)
|
|
181
|
+
output_result(
|
|
182
|
+
item,
|
|
183
|
+
json_fields,
|
|
184
|
+
jq_query,
|
|
185
|
+
template,
|
|
186
|
+
default_formatter=lambda data: click.echo(data["html_url"]),
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
@issue_group.command("close")
|
|
191
|
+
@click.option("-R", "--repo", "repo_name", help="Select another repository using the [HOST/]OWNER/REPO format.")
|
|
192
|
+
@click.argument("identifier")
|
|
193
|
+
@click.pass_context
|
|
194
|
+
def issue_close(ctx: click.Context, repo_name: str | None, identifier: str) -> None:
|
|
195
|
+
app = ctx.obj["app"]
|
|
196
|
+
url_owner, url_repo, number = resolve_issue_arg(identifier)
|
|
197
|
+
if url_owner:
|
|
198
|
+
assert url_repo is not None
|
|
199
|
+
owner, repo = url_owner, url_repo
|
|
200
|
+
else:
|
|
201
|
+
owner, repo = resolve_repo(repo_name or app.repo)
|
|
202
|
+
service = IssueService(app.client())
|
|
203
|
+
item = service.update(owner, repo, number, state="closed")
|
|
204
|
+
click.echo(f"Closed issue #{item['number']}")
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
@issue_group.command("comment")
|
|
208
|
+
@click.option("-R", "--repo", "repo_name", help="Select another repository using the [HOST/]OWNER/REPO format.")
|
|
209
|
+
@click.argument("identifier")
|
|
210
|
+
@click.option("-b", "--body")
|
|
211
|
+
@click.option("-F", "--body-file")
|
|
212
|
+
@click.option("-e", "--editor", is_flag=True)
|
|
213
|
+
@click.option("-w", "--web", is_flag=True, help="Open the issue in the web browser.")
|
|
214
|
+
@click.pass_context
|
|
215
|
+
def issue_comment(
|
|
216
|
+
ctx: click.Context,
|
|
217
|
+
repo_name: str | None,
|
|
218
|
+
identifier: str,
|
|
219
|
+
body: str | None,
|
|
220
|
+
body_file: str | None,
|
|
221
|
+
editor: bool,
|
|
222
|
+
web: bool,
|
|
223
|
+
) -> None:
|
|
224
|
+
app = ctx.obj["app"]
|
|
225
|
+
url_owner, url_repo, number = resolve_issue_arg(identifier)
|
|
226
|
+
if url_owner:
|
|
227
|
+
assert url_repo is not None
|
|
228
|
+
owner, repo = url_owner, url_repo
|
|
229
|
+
else:
|
|
230
|
+
owner, repo = resolve_repo(repo_name or app.repo)
|
|
231
|
+
if web:
|
|
232
|
+
open_in_browser(f"https://gitcode.com/{owner}/{repo}/issues/{number}")
|
|
233
|
+
return
|
|
234
|
+
body = get_body_from_options(body=body, body_file=body_file, editor=editor)
|
|
235
|
+
body = prompt_if_missing(body, "Body")
|
|
236
|
+
service = IssueService(app.client())
|
|
237
|
+
item = service.comment(owner, repo, number, body)
|
|
238
|
+
click.echo(str(item["id"]))
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
@issue_group.command("reopen")
|
|
242
|
+
@click.option("-R", "--repo", "repo_name", help="Select another repository using the [HOST/]OWNER/REPO format.")
|
|
243
|
+
@click.argument("identifier")
|
|
244
|
+
@click.pass_context
|
|
245
|
+
def issue_reopen(ctx: click.Context, repo_name: str | None, identifier: str) -> None:
|
|
246
|
+
app = ctx.obj["app"]
|
|
247
|
+
url_owner, url_repo, number = resolve_issue_arg(identifier)
|
|
248
|
+
if url_owner:
|
|
249
|
+
assert url_repo is not None
|
|
250
|
+
owner, repo = url_owner, url_repo
|
|
251
|
+
else:
|
|
252
|
+
owner, repo = resolve_repo(repo_name or app.repo)
|
|
253
|
+
service = IssueService(app.client())
|
|
254
|
+
item = service.update(owner, repo, number, state="open")
|
|
255
|
+
click.echo(f"Reopened issue #{item['number']}")
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
@issue_group.command("edit")
|
|
259
|
+
@click.option("-R", "--repo", "repo_name", help="Select another repository using the [HOST/]OWNER/REPO format.")
|
|
260
|
+
@click.argument("identifier")
|
|
261
|
+
@click.option("-t", "--title")
|
|
262
|
+
@click.option("-b", "--body")
|
|
263
|
+
@click.option("-a", "--add-assignee")
|
|
264
|
+
@click.option("-l", "--add-label")
|
|
265
|
+
@click.option("--remove-assignee")
|
|
266
|
+
@click.option("--remove-label")
|
|
267
|
+
@click.pass_context
|
|
268
|
+
def issue_edit(
|
|
269
|
+
ctx: click.Context,
|
|
270
|
+
repo_name: str | None,
|
|
271
|
+
identifier: str,
|
|
272
|
+
title: str | None,
|
|
273
|
+
body: str | None,
|
|
274
|
+
add_assignee: str | None,
|
|
275
|
+
add_label: str | None,
|
|
276
|
+
remove_assignee: str | None,
|
|
277
|
+
remove_label: str | None,
|
|
278
|
+
) -> None:
|
|
279
|
+
app = ctx.obj["app"]
|
|
280
|
+
url_owner, url_repo, number = resolve_issue_arg(identifier)
|
|
281
|
+
if url_owner:
|
|
282
|
+
assert url_repo is not None
|
|
283
|
+
owner, repo = url_owner, url_repo
|
|
284
|
+
else:
|
|
285
|
+
owner, repo = resolve_repo(repo_name or app.repo)
|
|
286
|
+
service = IssueService(app.client())
|
|
287
|
+
data = {
|
|
288
|
+
k: v
|
|
289
|
+
for k, v in {
|
|
290
|
+
"title": title,
|
|
291
|
+
"body": body,
|
|
292
|
+
"assignee": add_assignee,
|
|
293
|
+
"labels": add_label,
|
|
294
|
+
"unassignee": remove_assignee,
|
|
295
|
+
"unset_labels": remove_label,
|
|
296
|
+
}.items()
|
|
297
|
+
if v is not None
|
|
298
|
+
}
|
|
299
|
+
if not data:
|
|
300
|
+
raise click.UsageError("must specify at least one field to edit")
|
|
301
|
+
item = service.update(owner, repo, number, **data)
|
|
302
|
+
click.echo(f"Edited issue #{item['number']}")
|
|
303
|
+
|
|
304
|
+
|
|
305
|
+
@issue_group.command("delete")
|
|
306
|
+
@click.option("-R", "--repo", "repo_name", help="Select another repository using the [HOST/]OWNER/REPO format.")
|
|
307
|
+
@click.argument("identifier")
|
|
308
|
+
@click.confirmation_option(prompt="Are you sure you want to delete this issue?")
|
|
309
|
+
@click.pass_context
|
|
310
|
+
def issue_delete(ctx: click.Context, repo_name: str | None, identifier: str) -> None:
|
|
311
|
+
app = ctx.obj["app"]
|
|
312
|
+
url_owner, url_repo, number = resolve_issue_arg(identifier)
|
|
313
|
+
if url_owner:
|
|
314
|
+
assert url_repo is not None
|
|
315
|
+
owner, repo = url_owner, url_repo
|
|
316
|
+
else:
|
|
317
|
+
owner, repo = resolve_repo(repo_name or app.repo)
|
|
318
|
+
service = IssueService(app.client())
|
|
319
|
+
service.delete(owner, repo, number)
|
|
320
|
+
click.echo(f"Deleted issue #{number}")
|
|
321
|
+
|
|
322
|
+
|
|
323
|
+
@issue_group.command("status")
|
|
324
|
+
@click.option("-R", "--repo", "repo_name", help="Select another repository using the [HOST/]OWNER/REPO format.")
|
|
325
|
+
@click.pass_context
|
|
326
|
+
def issue_status(ctx: click.Context, repo_name: str | None) -> None:
|
|
327
|
+
app = ctx.obj["app"]
|
|
328
|
+
owner, repo = resolve_repo(repo_name or app.repo)
|
|
329
|
+
service = IssueService(app.client())
|
|
330
|
+
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}:")
|
|
333
|
+
for item in items:
|
|
334
|
+
click.echo(f" #{item['number']}\t{item['state']}\t{item['title']}")
|
|
335
|
+
|
|
336
|
+
|
|
337
|
+
issue_group.add_command(issue_list, name="ls")
|
|
338
|
+
issue_group.add_command(issue_create, name="new")
|