pygitcode 0.1.2__tar.gz → 0.1.3__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.3/PKG-INFO +110 -0
- pygitcode-0.1.3/README.md +70 -0
- {pygitcode-0.1.2 → pygitcode-0.1.3}/pyproject.toml +4 -4
- {pygitcode-0.1.2 → pygitcode-0.1.3}/src/gitcode_cli/__init__.py +1 -1
- {pygitcode-0.1.2 → pygitcode-0.1.3}/src/gitcode_cli/adapters/capabilities.py +5 -2
- {pygitcode-0.1.2 → pygitcode-0.1.3}/src/gitcode_cli/adapters/issues.py +152 -18
- {pygitcode-0.1.2 → pygitcode-0.1.3}/src/gitcode_cli/adapters/pulls.py +44 -14
- {pygitcode-0.1.2 → pygitcode-0.1.3}/src/gitcode_cli/client.py +2 -2
- {pygitcode-0.1.2 → pygitcode-0.1.3}/src/gitcode_cli/commands/auth.py +14 -1
- {pygitcode-0.1.2 → pygitcode-0.1.3}/src/gitcode_cli/commands/issue.py +233 -50
- {pygitcode-0.1.2 → pygitcode-0.1.3}/src/gitcode_cli/commands/pr.py +208 -22
- {pygitcode-0.1.2 → pygitcode-0.1.3}/src/gitcode_cli/formatters.py +52 -36
- {pygitcode-0.1.2 → pygitcode-0.1.3}/src/gitcode_cli/services/issues.py +12 -0
- {pygitcode-0.1.2 → pygitcode-0.1.3}/src/gitcode_cli/utils.py +7 -0
- pygitcode-0.1.3/src/pygitcode.egg-info/PKG-INFO +110 -0
- pygitcode-0.1.2/PKG-INFO +0 -286
- pygitcode-0.1.2/README.md +0 -246
- pygitcode-0.1.2/src/pygitcode.egg-info/PKG-INFO +0 -286
- {pygitcode-0.1.2 → pygitcode-0.1.3}/LICENSE +0 -0
- {pygitcode-0.1.2 → pygitcode-0.1.3}/setup.cfg +0 -0
- {pygitcode-0.1.2 → pygitcode-0.1.3}/src/gitcode_cli/adapters/__init__.py +0 -0
- {pygitcode-0.1.2 → pygitcode-0.1.3}/src/gitcode_cli/adapters/base.py +0 -0
- {pygitcode-0.1.2 → pygitcode-0.1.3}/src/gitcode_cli/cli.py +0 -0
- {pygitcode-0.1.2 → pygitcode-0.1.3}/src/gitcode_cli/cli_compat.py +0 -0
- {pygitcode-0.1.2 → pygitcode-0.1.3}/src/gitcode_cli/commands/__init__.py +0 -0
- {pygitcode-0.1.2 → pygitcode-0.1.3}/src/gitcode_cli/config.py +0 -0
- {pygitcode-0.1.2 → pygitcode-0.1.3}/src/gitcode_cli/context.py +0 -0
- {pygitcode-0.1.2 → pygitcode-0.1.3}/src/gitcode_cli/errors.py +0 -0
- {pygitcode-0.1.2 → pygitcode-0.1.3}/src/gitcode_cli/helptext.py +0 -0
- {pygitcode-0.1.2 → pygitcode-0.1.3}/src/gitcode_cli/repo.py +0 -0
- {pygitcode-0.1.2 → pygitcode-0.1.3}/src/gitcode_cli/services/__init__.py +0 -0
- {pygitcode-0.1.2 → pygitcode-0.1.3}/src/gitcode_cli/services/pulls.py +0 -0
- {pygitcode-0.1.2 → pygitcode-0.1.3}/src/gitcode_cli/services/users.py +0 -0
- {pygitcode-0.1.2 → pygitcode-0.1.3}/src/pygitcode.egg-info/SOURCES.txt +0 -0
- {pygitcode-0.1.2 → pygitcode-0.1.3}/src/pygitcode.egg-info/dependency_links.txt +0 -0
- {pygitcode-0.1.2 → pygitcode-0.1.3}/src/pygitcode.egg-info/entry_points.txt +0 -0
- {pygitcode-0.1.2 → pygitcode-0.1.3}/src/pygitcode.egg-info/requires.txt +0 -0
- {pygitcode-0.1.2 → pygitcode-0.1.3}/src/pygitcode.egg-info/top_level.txt +0 -0
pygitcode-0.1.3/PKG-INFO
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: pygitcode
|
|
3
|
+
Version: 0.1.3
|
|
4
|
+
Summary: A CLI tool for GitCode (api.gitcode.com), modeled after GitHub CLI.
|
|
5
|
+
Author-email: codeasier <825770651@qq.com>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/codeasier/gitcode-cli
|
|
8
|
+
Project-URL: Documentation, https://github.com/codeasier/gitcode-cli#readme
|
|
9
|
+
Project-URL: Repository, https://github.com/codeasier/gitcode-cli.git
|
|
10
|
+
Project-URL: Issues, https://github.com/codeasier/gitcode-cli/issues
|
|
11
|
+
Keywords: gitcode,cli,git,api,vcs,developer-tools
|
|
12
|
+
Classifier: Development Status :: 4 - Beta
|
|
13
|
+
Classifier: Environment :: Console
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: Operating System :: OS Independent
|
|
16
|
+
Classifier: Programming Language :: Python :: 3
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
22
|
+
Classifier: Topic :: Software Development :: Version Control :: Git
|
|
23
|
+
Classifier: Topic :: Utilities
|
|
24
|
+
Classifier: Typing :: Typed
|
|
25
|
+
Requires-Python: >=3.9
|
|
26
|
+
Description-Content-Type: text/markdown
|
|
27
|
+
License-File: LICENSE
|
|
28
|
+
Requires-Dist: click>=8.0
|
|
29
|
+
Requires-Dist: httpx>=0.24
|
|
30
|
+
Provides-Extra: dev
|
|
31
|
+
Requires-Dist: pytest>=8.0; extra == "dev"
|
|
32
|
+
Requires-Dist: pytest-cov>=5.0; extra == "dev"
|
|
33
|
+
Requires-Dist: pytest-mock>=3.14; extra == "dev"
|
|
34
|
+
Requires-Dist: respx>=0.21; extra == "dev"
|
|
35
|
+
Requires-Dist: ruff>=0.6; extra == "dev"
|
|
36
|
+
Requires-Dist: black>=24.0; extra == "dev"
|
|
37
|
+
Requires-Dist: basedpyright>=1.19; extra == "dev"
|
|
38
|
+
Requires-Dist: pre-commit>=3.0; extra == "dev"
|
|
39
|
+
Dynamic: license-file
|
|
40
|
+
|
|
41
|
+
# pygitcode
|
|
42
|
+
|
|
43
|
+
> A CLI tool for [GitCode](https://gitcode.com/) (`api.gitcode.com`), modeled after GitHub CLI (`gh`).
|
|
44
|
+
|
|
45
|
+
[](https://pypi.org/project/pygitcode/)
|
|
46
|
+
[](https://pypi.org/project/pygitcode/)
|
|
47
|
+
[](https://github.com/codeasier/gitcode-cli/blob/main/LICENSE)
|
|
48
|
+
|
|
49
|
+
English | [中文](README.zh-CN.md)
|
|
50
|
+
|
|
51
|
+
For agent-assisted setup, usage, upgrades, or troubleshooting, read [docs/en/USING_WITH_AGENT.md](docs/en/USING_WITH_AGENT.md) first.
|
|
52
|
+
|
|
53
|
+
## Installation
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
pip install pygitcode
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
This exposes both `gc` and `gitcode` commands in your shell.
|
|
60
|
+
|
|
61
|
+
## Quick Start
|
|
62
|
+
|
|
63
|
+
If you already know GitHub CLI (`gh`), you can start with the same familiar flows:
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
gc auth login
|
|
67
|
+
gc issue list
|
|
68
|
+
gc issue view 42
|
|
69
|
+
gc pr list
|
|
70
|
+
gc pr create --fill
|
|
71
|
+
gc pr merge 42 --squash
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
Use `gitcode` instead of `gc` on Windows PowerShell because `gc` is a built-in alias for `Get-Content`.
|
|
75
|
+
|
|
76
|
+
## Documentation
|
|
77
|
+
|
|
78
|
+
- [Documentation index](docs/en/index.md)
|
|
79
|
+
- [Development guide](docs/en/development.md)
|
|
80
|
+
- [Compatibility notes](docs/en/gh-compatibility.md)
|
|
81
|
+
|
|
82
|
+
## Repository Selection
|
|
83
|
+
|
|
84
|
+
Commands operate on the current Git repository by default. You can also select a repository explicitly:
|
|
85
|
+
|
|
86
|
+
```bash
|
|
87
|
+
gc issue list -R owner/repo
|
|
88
|
+
gc pr list -R owner/repo
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
## Authentication
|
|
92
|
+
|
|
93
|
+
`gc` resolves tokens in this order:
|
|
94
|
+
|
|
95
|
+
1. `--token` option
|
|
96
|
+
2. `GC_TOKEN` environment variable
|
|
97
|
+
3. `~/.config/gc/config.json`, written by `gc auth login`
|
|
98
|
+
|
|
99
|
+
Useful commands:
|
|
100
|
+
|
|
101
|
+
```bash
|
|
102
|
+
gc auth login
|
|
103
|
+
gc auth status
|
|
104
|
+
gc auth token
|
|
105
|
+
gc auth logout
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
## License
|
|
109
|
+
|
|
110
|
+
MIT
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# pygitcode
|
|
2
|
+
|
|
3
|
+
> A CLI tool for [GitCode](https://gitcode.com/) (`api.gitcode.com`), modeled after GitHub CLI (`gh`).
|
|
4
|
+
|
|
5
|
+
[](https://pypi.org/project/pygitcode/)
|
|
6
|
+
[](https://pypi.org/project/pygitcode/)
|
|
7
|
+
[](https://github.com/codeasier/gitcode-cli/blob/main/LICENSE)
|
|
8
|
+
|
|
9
|
+
English | [中文](README.zh-CN.md)
|
|
10
|
+
|
|
11
|
+
For agent-assisted setup, usage, upgrades, or troubleshooting, read [docs/en/USING_WITH_AGENT.md](docs/en/USING_WITH_AGENT.md) first.
|
|
12
|
+
|
|
13
|
+
## Installation
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
pip install pygitcode
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
This exposes both `gc` and `gitcode` commands in your shell.
|
|
20
|
+
|
|
21
|
+
## Quick Start
|
|
22
|
+
|
|
23
|
+
If you already know GitHub CLI (`gh`), you can start with the same familiar flows:
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
gc auth login
|
|
27
|
+
gc issue list
|
|
28
|
+
gc issue view 42
|
|
29
|
+
gc pr list
|
|
30
|
+
gc pr create --fill
|
|
31
|
+
gc pr merge 42 --squash
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Use `gitcode` instead of `gc` on Windows PowerShell because `gc` is a built-in alias for `Get-Content`.
|
|
35
|
+
|
|
36
|
+
## Documentation
|
|
37
|
+
|
|
38
|
+
- [Documentation index](docs/en/index.md)
|
|
39
|
+
- [Development guide](docs/en/development.md)
|
|
40
|
+
- [Compatibility notes](docs/en/gh-compatibility.md)
|
|
41
|
+
|
|
42
|
+
## Repository Selection
|
|
43
|
+
|
|
44
|
+
Commands operate on the current Git repository by default. You can also select a repository explicitly:
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
gc issue list -R owner/repo
|
|
48
|
+
gc pr list -R owner/repo
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## Authentication
|
|
52
|
+
|
|
53
|
+
`gc` resolves tokens in this order:
|
|
54
|
+
|
|
55
|
+
1. `--token` option
|
|
56
|
+
2. `GC_TOKEN` environment variable
|
|
57
|
+
3. `~/.config/gc/config.json`, written by `gc auth login`
|
|
58
|
+
|
|
59
|
+
Useful commands:
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
gc auth login
|
|
63
|
+
gc auth status
|
|
64
|
+
gc auth token
|
|
65
|
+
gc auth logout
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
## License
|
|
69
|
+
|
|
70
|
+
MIT
|
|
@@ -46,10 +46,10 @@ dev = [
|
|
|
46
46
|
]
|
|
47
47
|
|
|
48
48
|
[project.urls]
|
|
49
|
-
Homepage = "https://github.com/
|
|
50
|
-
Documentation = "https://github.com/
|
|
51
|
-
Repository = "https://github.com/
|
|
52
|
-
Issues = "https://github.com/
|
|
49
|
+
Homepage = "https://github.com/codeasier/gitcode-cli"
|
|
50
|
+
Documentation = "https://github.com/codeasier/gitcode-cli#readme"
|
|
51
|
+
Repository = "https://github.com/codeasier/gitcode-cli.git"
|
|
52
|
+
Issues = "https://github.com/codeasier/gitcode-cli/issues"
|
|
53
53
|
|
|
54
54
|
[project.scripts]
|
|
55
55
|
gc = "gitcode_cli.cli:main"
|
|
@@ -8,10 +8,13 @@ CAPABILITY_MESSAGES = {
|
|
|
8
8
|
"Unable to verify the current user's issue comments on GitCode; refusing to edit or delete comments safely."
|
|
9
9
|
),
|
|
10
10
|
"ISSUE_CREATE_IF_NONE_REQUIRES_EDIT_LAST": "--create-if-none can only be used together with --edit-last.",
|
|
11
|
-
"ISSUE_DELETE": "GitCode API does not support deleting issues.",
|
|
12
11
|
"ISSUE_DEVELOP_BASE": "--base and --name are not supported by 'gc issue develop'",
|
|
13
12
|
"ISSUE_DEVELOP_NAME": "--base and --name are not supported by 'gc issue develop'",
|
|
14
|
-
"
|
|
13
|
+
"ISSUE_LIST_APP": "GitCode issue API does not support --app filtering.",
|
|
14
|
+
"ISSUE_STATUS_GH_SEMANTICS": (
|
|
15
|
+
"GitCode-limited approximation of gh issue status: assigned and opened issues use user-level "
|
|
16
|
+
"GitCode results; mentioned issues use repository mention filtering."
|
|
17
|
+
),
|
|
15
18
|
"PR_MERGE_AUTHOR_EMAIL": "GitCode merge API does not support --author-email.",
|
|
16
19
|
"PR_MERGE_AUTO": "GitCode merge API does not support --auto.",
|
|
17
20
|
"PR_REVIEW_REQUEST_CHANGES": (
|
|
@@ -7,7 +7,7 @@ import click
|
|
|
7
7
|
from ..errors import APIError
|
|
8
8
|
from ..services import IssueService, UserService
|
|
9
9
|
from .base import AdapterActionResult
|
|
10
|
-
from .capabilities import capability_message
|
|
10
|
+
from .capabilities import capability_message
|
|
11
11
|
|
|
12
12
|
|
|
13
13
|
def _normalize_multi_values(values: tuple[str, ...] | None) -> str | None:
|
|
@@ -16,6 +16,13 @@ def _normalize_multi_values(values: tuple[str, ...] | None) -> str | None:
|
|
|
16
16
|
return ",".join(values)
|
|
17
17
|
|
|
18
18
|
|
|
19
|
+
def _normalize_labels(labels: tuple[str, ...] | None) -> tuple[str, ...] | None:
|
|
20
|
+
if not labels:
|
|
21
|
+
return None
|
|
22
|
+
normalized = tuple(label.strip() for label in labels if label.strip())
|
|
23
|
+
return normalized or None
|
|
24
|
+
|
|
25
|
+
|
|
19
26
|
def _extract_label_names(issue: dict[str, Any] | None) -> list[str]:
|
|
20
27
|
labels = issue.get("labels") if isinstance(issue, dict) else None
|
|
21
28
|
if not labels:
|
|
@@ -35,6 +42,37 @@ def _extract_label_names(issue: dict[str, Any] | None) -> list[str]:
|
|
|
35
42
|
return names
|
|
36
43
|
|
|
37
44
|
|
|
45
|
+
def _extract_assignee_names(issue: dict[str, Any] | None) -> list[str]:
|
|
46
|
+
assignees = issue.get("assignees") if isinstance(issue, dict) else None
|
|
47
|
+
if isinstance(assignees, str):
|
|
48
|
+
return [part.strip() for part in assignees.split(",") if part.strip()]
|
|
49
|
+
if isinstance(assignees, list):
|
|
50
|
+
names: list[str] = []
|
|
51
|
+
for assignee in assignees:
|
|
52
|
+
if isinstance(assignee, dict):
|
|
53
|
+
name = _extract_login(assignee)
|
|
54
|
+
if name:
|
|
55
|
+
names.append(name)
|
|
56
|
+
elif assignee:
|
|
57
|
+
names.append(str(assignee))
|
|
58
|
+
return names
|
|
59
|
+
|
|
60
|
+
assignee = issue.get("assignee") if isinstance(issue, dict) else None
|
|
61
|
+
if isinstance(assignee, str):
|
|
62
|
+
return [part.strip() for part in assignee.split(",") if part.strip()]
|
|
63
|
+
if isinstance(assignee, dict):
|
|
64
|
+
name = _extract_login(assignee)
|
|
65
|
+
return [name] if name else []
|
|
66
|
+
return []
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def _matches_all_labels(issue: dict[str, Any], labels: tuple[str, ...] | None) -> bool:
|
|
70
|
+
if not labels or len(labels) < 2:
|
|
71
|
+
return True
|
|
72
|
+
actual = {label.lower() for label in _extract_label_names(issue)}
|
|
73
|
+
return all(label.lower() in actual for label in labels)
|
|
74
|
+
|
|
75
|
+
|
|
38
76
|
def _extract_login(data: dict[str, Any] | None) -> str | None:
|
|
39
77
|
if not isinstance(data, dict):
|
|
40
78
|
return None
|
|
@@ -51,11 +89,73 @@ def _extract_login(data: dict[str, Any] | None) -> str | None:
|
|
|
51
89
|
return None
|
|
52
90
|
|
|
53
91
|
|
|
92
|
+
def _as_list(items: Any) -> list[Any]:
|
|
93
|
+
return items if isinstance(items, list) else []
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def _extract_label_options(items: list[Any]) -> set[str]:
|
|
97
|
+
names: set[str] = set()
|
|
98
|
+
for item in items:
|
|
99
|
+
if not isinstance(item, dict):
|
|
100
|
+
continue
|
|
101
|
+
name = item.get("name")
|
|
102
|
+
if isinstance(name, str) and name.strip():
|
|
103
|
+
names.add(name.strip())
|
|
104
|
+
return names
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def _resolve_milestone_number(items: list[Any], milestone: str | None) -> int | None:
|
|
108
|
+
if milestone is None:
|
|
109
|
+
return None
|
|
110
|
+
value = milestone.strip()
|
|
111
|
+
if not value:
|
|
112
|
+
return None
|
|
113
|
+
if value.isdigit():
|
|
114
|
+
return int(value)
|
|
115
|
+
matches: list[int] = []
|
|
116
|
+
for item in items:
|
|
117
|
+
if not isinstance(item, dict):
|
|
118
|
+
continue
|
|
119
|
+
title = item.get("title")
|
|
120
|
+
number = item.get("number")
|
|
121
|
+
if title != value:
|
|
122
|
+
continue
|
|
123
|
+
if isinstance(number, int):
|
|
124
|
+
matches.append(number)
|
|
125
|
+
elif isinstance(number, str) and number.isdigit():
|
|
126
|
+
matches.append(int(number))
|
|
127
|
+
if len(matches) == 1:
|
|
128
|
+
return matches[0]
|
|
129
|
+
if len(matches) > 1:
|
|
130
|
+
raise click.ClickException(f"could not resolve milestone: '{value}' matched multiple milestones")
|
|
131
|
+
raise click.ClickException(f"could not resolve milestone: '{value}' not found")
|
|
132
|
+
|
|
133
|
+
|
|
54
134
|
class IssueAdapter:
|
|
55
135
|
def __init__(self, service: IssueService, user_service: UserService | None = None):
|
|
56
136
|
self.service = service
|
|
57
137
|
self.user_service = user_service
|
|
58
138
|
|
|
139
|
+
def _list_all_labels(self, owner: str, repo: str) -> list[Any]:
|
|
140
|
+
items: list[Any] = []
|
|
141
|
+
page = 1
|
|
142
|
+
while True:
|
|
143
|
+
page_items = _as_list(self.service.list_labels(owner, repo, page=page, per_page=100))
|
|
144
|
+
items.extend(page_items)
|
|
145
|
+
if len(page_items) < 100:
|
|
146
|
+
return items
|
|
147
|
+
page += 1
|
|
148
|
+
|
|
149
|
+
def _list_all_milestones(self, owner: str, repo: str) -> list[Any]:
|
|
150
|
+
items: list[Any] = []
|
|
151
|
+
page = 1
|
|
152
|
+
while True:
|
|
153
|
+
page_items = _as_list(self.service.list_milestones(owner, repo, state="all", page=page, per_page=100))
|
|
154
|
+
items.extend(page_items)
|
|
155
|
+
if len(page_items) < 100:
|
|
156
|
+
return items
|
|
157
|
+
page += 1
|
|
158
|
+
|
|
59
159
|
def list_issues(
|
|
60
160
|
self,
|
|
61
161
|
owner: str,
|
|
@@ -70,6 +170,13 @@ class IssueAdapter:
|
|
|
70
170
|
search: str | None,
|
|
71
171
|
limit: int | None,
|
|
72
172
|
) -> Any:
|
|
173
|
+
if author == "@me":
|
|
174
|
+
if self.user_service is None:
|
|
175
|
+
raise click.ClickException("failed to resolve @me: current user lookup is unavailable")
|
|
176
|
+
current_login = _extract_login(self.user_service.current())
|
|
177
|
+
if not current_login:
|
|
178
|
+
raise click.ClickException("failed to resolve @me: current user has no login")
|
|
179
|
+
author = current_login
|
|
73
180
|
items = self.service.list(
|
|
74
181
|
owner,
|
|
75
182
|
repo,
|
|
@@ -81,6 +188,8 @@ class IssueAdapter:
|
|
|
81
188
|
mention=mention,
|
|
82
189
|
search=search,
|
|
83
190
|
)
|
|
191
|
+
if isinstance(items, list) and labels and len(labels) > 1:
|
|
192
|
+
items = [item for item in items if _matches_all_labels(item, labels)]
|
|
84
193
|
if limit is not None:
|
|
85
194
|
return items[:limit]
|
|
86
195
|
return items
|
|
@@ -96,14 +205,26 @@ class IssueAdapter:
|
|
|
96
205
|
labels: tuple[str, ...] | None,
|
|
97
206
|
milestone: str | None,
|
|
98
207
|
) -> dict[str, Any] | None:
|
|
208
|
+
normalized_labels = _normalize_labels(labels)
|
|
209
|
+
if normalized_labels:
|
|
210
|
+
available_labels = _extract_label_options(self._list_all_labels(owner, repo))
|
|
211
|
+
for label in normalized_labels:
|
|
212
|
+
if label not in available_labels:
|
|
213
|
+
raise click.ClickException(f"could not add label: '{label}' not found")
|
|
214
|
+
milestone_number = None
|
|
215
|
+
if milestone is not None:
|
|
216
|
+
milestones = []
|
|
217
|
+
if not milestone.strip().isdigit():
|
|
218
|
+
milestones = self._list_all_milestones(owner, repo)
|
|
219
|
+
milestone_number = _resolve_milestone_number(milestones, milestone)
|
|
99
220
|
return self.service.create(
|
|
100
221
|
owner,
|
|
101
222
|
repo,
|
|
102
223
|
title=title,
|
|
103
224
|
body=body,
|
|
104
225
|
assignee=assignee,
|
|
105
|
-
labels=_normalize_multi_values(
|
|
106
|
-
milestone=
|
|
226
|
+
labels=_normalize_multi_values(normalized_labels),
|
|
227
|
+
milestone=milestone_number,
|
|
107
228
|
)
|
|
108
229
|
|
|
109
230
|
def close_issue(
|
|
@@ -174,15 +295,18 @@ class IssueAdapter:
|
|
|
174
295
|
return comment
|
|
175
296
|
return None
|
|
176
297
|
|
|
177
|
-
def reopen_issue(self, owner: str, repo: str, number: str) -> AdapterActionResult:
|
|
298
|
+
def reopen_issue(self, owner: str, repo: str, number: str, *, comment: str | None = None) -> AdapterActionResult:
|
|
178
299
|
current = self.service.get(owner, repo, number)
|
|
179
300
|
if current and current.get("state") == "open":
|
|
180
301
|
return AdapterActionResult(item=current, message="already_open")
|
|
181
302
|
item = self.service.update(owner, repo, number, state="reopen")
|
|
303
|
+
if comment:
|
|
304
|
+
self.service.comment(owner, repo, number, comment)
|
|
182
305
|
return AdapterActionResult(item=item, message="reopened")
|
|
183
306
|
|
|
184
|
-
def delete_issue(self, owner: str, repo: str, number: str) -> AdapterActionResult:
|
|
185
|
-
|
|
307
|
+
def delete_issue(self, owner: str, repo: str, number: str) -> AdapterActionResult:
|
|
308
|
+
item = self.service.delete(owner, repo, number)
|
|
309
|
+
return AdapterActionResult(item=item, message="deleted")
|
|
186
310
|
|
|
187
311
|
def edit_issue(
|
|
188
312
|
self,
|
|
@@ -194,22 +318,29 @@ class IssueAdapter:
|
|
|
194
318
|
body: str | None,
|
|
195
319
|
add_assignee: str | None,
|
|
196
320
|
add_labels: tuple[str, ...] | None,
|
|
197
|
-
|
|
321
|
+
remove_assignee: str | None,
|
|
322
|
+
remove_labels: tuple[str, ...] | None,
|
|
323
|
+
milestone: int | None,
|
|
198
324
|
remove_milestone: bool,
|
|
199
325
|
) -> dict[str, Any] | None:
|
|
200
|
-
data = {k: v for k, v in {"title": title, "body": body}.items() if v is not None}
|
|
326
|
+
data: dict[str, Any] = {k: v for k, v in {"title": title, "body": body}.items() if v is not None}
|
|
201
327
|
|
|
202
328
|
if add_assignee is not None:
|
|
203
329
|
data["assignee"] = add_assignee
|
|
330
|
+
if remove_assignee is not None:
|
|
331
|
+
current = self.service.get(owner, repo, number)
|
|
332
|
+
remaining = [name for name in _extract_assignee_names(current) if name != remove_assignee]
|
|
333
|
+
data["assignee"] = ",".join(remaining)
|
|
204
334
|
|
|
205
|
-
if add_labels:
|
|
335
|
+
if add_labels or remove_labels:
|
|
206
336
|
current = self.service.get(owner, repo, number)
|
|
207
337
|
existing = _extract_label_names(current)
|
|
338
|
+
removed = {label.strip() for label in remove_labels or () if label.strip()}
|
|
208
339
|
merged: list[str] = []
|
|
209
340
|
seen: set[str] = set()
|
|
210
|
-
for label in [*existing, *list(add_labels)]:
|
|
341
|
+
for label in [*existing, *list(add_labels or ())]:
|
|
211
342
|
normalized = label.strip()
|
|
212
|
-
if normalized and normalized not in seen:
|
|
343
|
+
if normalized and normalized not in removed and normalized not in seen:
|
|
213
344
|
seen.add(normalized)
|
|
214
345
|
merged.append(normalized)
|
|
215
346
|
data["labels"] = ",".join(merged)
|
|
@@ -217,22 +348,25 @@ class IssueAdapter:
|
|
|
217
348
|
if milestone is not None:
|
|
218
349
|
data["milestone"] = milestone
|
|
219
350
|
if remove_milestone:
|
|
220
|
-
data["milestone"] =
|
|
351
|
+
data["milestone"] = 0
|
|
221
352
|
return self.service.update(owner, repo, number, **data)
|
|
222
353
|
|
|
223
354
|
def status(self, owner: str, repo: str) -> AdapterActionResult:
|
|
224
|
-
|
|
355
|
+
if self.user_service is None:
|
|
356
|
+
raise click.ClickException("failed to resolve current user: current user lookup is unavailable")
|
|
357
|
+
current_login = _extract_login(self.user_service.current())
|
|
358
|
+
if not current_login:
|
|
359
|
+
raise click.ClickException("failed to resolve current user: current user has no login")
|
|
360
|
+
assigned = self.service.list_user_issues(filter="assigned", state="open") or []
|
|
361
|
+
created = self.service.list_user_issues(filter="created", state="open") or []
|
|
362
|
+
mentioned = self.service.list(owner, repo, state="open", mention=current_login) or []
|
|
225
363
|
return AdapterActionResult(
|
|
226
|
-
|
|
364
|
+
item={"assigned": assigned, "mentioned": mentioned, "createdBy": created},
|
|
227
365
|
message=capability_message("ISSUE_STATUS_GH_SEMANTICS"),
|
|
228
366
|
approximated=True,
|
|
229
367
|
)
|
|
230
368
|
|
|
231
369
|
def develop(self, owner: str, repo: str, number: str, *, base: str | None, name: str | None) -> AdapterActionResult: # noqa: ARG002
|
|
232
|
-
if base is not None:
|
|
233
|
-
raise unsupported("ISSUE_DEVELOP_BASE")
|
|
234
|
-
if name is not None:
|
|
235
|
-
raise unsupported("ISSUE_DEVELOP_NAME")
|
|
236
370
|
return AdapterActionResult(
|
|
237
371
|
message=f"Opening issue #{number} in the browser instead.",
|
|
238
372
|
warning="Note: 'issue develop' does not create a local branch on GitCode.",
|
|
@@ -13,6 +13,16 @@ def _normalize_multi_values(values: tuple[str, ...] | None) -> str | None:
|
|
|
13
13
|
return ",".join(values)
|
|
14
14
|
|
|
15
15
|
|
|
16
|
+
def _normalize_limit(limit: int | None) -> tuple[int, int | None]:
|
|
17
|
+
if limit is None:
|
|
18
|
+
return 30, 30
|
|
19
|
+
if limit < 0:
|
|
20
|
+
return 100, None
|
|
21
|
+
if limit == 0:
|
|
22
|
+
return 0, 0
|
|
23
|
+
return min(limit, 100), limit
|
|
24
|
+
|
|
25
|
+
|
|
16
26
|
class PullRequestAdapter:
|
|
17
27
|
def __init__(self, service: PullRequestService):
|
|
18
28
|
self.service = service
|
|
@@ -31,21 +41,41 @@ class PullRequestAdapter:
|
|
|
31
41
|
labels: tuple[str, ...] | None,
|
|
32
42
|
search: str | None,
|
|
33
43
|
limit: int | None,
|
|
44
|
+
merged_after: str | None = None,
|
|
45
|
+
merged_before: str | None = None,
|
|
34
46
|
) -> Any:
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
search=search
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
47
|
+
params = {
|
|
48
|
+
"state": state,
|
|
49
|
+
"author": author,
|
|
50
|
+
"base": base,
|
|
51
|
+
"assignee": assignee,
|
|
52
|
+
"draft": draft,
|
|
53
|
+
"head": head,
|
|
54
|
+
"labels": _normalize_multi_values(labels),
|
|
55
|
+
}
|
|
56
|
+
if search is not None:
|
|
57
|
+
params["search"] = search
|
|
58
|
+
if merged_after is not None:
|
|
59
|
+
params["merged_after"] = merged_after
|
|
60
|
+
if merged_before is not None:
|
|
61
|
+
params["merged_before"] = merged_before
|
|
62
|
+
|
|
63
|
+
per_page, remaining = _normalize_limit(limit)
|
|
64
|
+
if remaining == 0:
|
|
65
|
+
return []
|
|
66
|
+
|
|
67
|
+
items = []
|
|
68
|
+
page = 1
|
|
69
|
+
while True:
|
|
70
|
+
page_items = self.service.list(owner, repo, **params, page=page, per_page=per_page)
|
|
71
|
+
if not page_items:
|
|
72
|
+
break
|
|
73
|
+
items.extend(page_items)
|
|
74
|
+
if remaining is not None and len(items) >= remaining:
|
|
75
|
+
return items[:remaining]
|
|
76
|
+
if len(page_items) < per_page:
|
|
77
|
+
break
|
|
78
|
+
page += 1
|
|
49
79
|
return items
|
|
50
80
|
|
|
51
81
|
def create_pr(
|
|
@@ -46,14 +46,14 @@ class GitCodeClient:
|
|
|
46
46
|
if response.status_code == 401:
|
|
47
47
|
try:
|
|
48
48
|
data = response.json()
|
|
49
|
-
original = data.get("message") or str(data)
|
|
49
|
+
original = data.get("error_message") or data.get("message") or str(data)
|
|
50
50
|
except Exception:
|
|
51
51
|
original = response.text
|
|
52
52
|
raise APIError(f"Authentication failed: {original}. Run 'gc auth login' to authenticate.", 401)
|
|
53
53
|
if response.status_code >= 400:
|
|
54
54
|
try:
|
|
55
55
|
data = response.json()
|
|
56
|
-
message = data.get("message") or str(data)
|
|
56
|
+
message = data.get("error_message") or data.get("message") or str(data)
|
|
57
57
|
except Exception:
|
|
58
58
|
message = response.text
|
|
59
59
|
raise APIError(message or "GitCode API request failed", response.status_code)
|
|
@@ -7,6 +7,10 @@ from ..helptext import GCSectionGroup
|
|
|
7
7
|
from ..utils import safe_echo
|
|
8
8
|
|
|
9
9
|
|
|
10
|
+
def _pending_gh_compat(name: str) -> None:
|
|
11
|
+
raise click.ClickException(f"gh-compatible command/flag '{name}' is recognized but not implemented yet.")
|
|
12
|
+
|
|
13
|
+
|
|
10
14
|
@click.group("auth", cls=GCSectionGroup, help="Authenticate gc with GitCode.")
|
|
11
15
|
def auth_group() -> None:
|
|
12
16
|
pass
|
|
@@ -36,7 +40,16 @@ def auth_logout() -> None:
|
|
|
36
40
|
|
|
37
41
|
|
|
38
42
|
@auth_group.command("status", short_help="Show authentication status", help="Show authentication status.")
|
|
39
|
-
|
|
43
|
+
@click.option("--json", "json_fields")
|
|
44
|
+
@click.option("-q", "--jq", "jq_query")
|
|
45
|
+
@click.option("-t", "--template")
|
|
46
|
+
def auth_status(json_fields: str | None, jq_query: str | None, template: str | None) -> None:
|
|
47
|
+
if json_fields is not None:
|
|
48
|
+
_pending_gh_compat("auth status --json")
|
|
49
|
+
if jq_query is not None:
|
|
50
|
+
_pending_gh_compat("auth status --jq")
|
|
51
|
+
if template is not None:
|
|
52
|
+
_pending_gh_compat("auth status --template")
|
|
40
53
|
try:
|
|
41
54
|
token = get_token()
|
|
42
55
|
except Exception:
|