pygitcode 0.1.0__tar.gz → 0.1.2__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.0 → pygitcode-0.1.2}/PKG-INFO +49 -12
- {pygitcode-0.1.0 → pygitcode-0.1.2}/README.md +49 -12
- {pygitcode-0.1.0 → pygitcode-0.1.2}/pyproject.toml +5 -1
- {pygitcode-0.1.0 → pygitcode-0.1.2}/src/gitcode_cli/__init__.py +1 -1
- pygitcode-0.1.2/src/gitcode_cli/adapters/__init__.py +7 -0
- pygitcode-0.1.2/src/gitcode_cli/adapters/base.py +14 -0
- pygitcode-0.1.2/src/gitcode_cli/adapters/capabilities.py +29 -0
- pygitcode-0.1.2/src/gitcode_cli/adapters/issues.py +239 -0
- pygitcode-0.1.2/src/gitcode_cli/adapters/pulls.py +170 -0
- pygitcode-0.1.2/src/gitcode_cli/cli.py +109 -0
- {pygitcode-0.1.0 → pygitcode-0.1.2}/src/gitcode_cli/cli_compat.py +2 -0
- {pygitcode-0.1.0 → pygitcode-0.1.2}/src/gitcode_cli/client.py +22 -8
- pygitcode-0.1.2/src/gitcode_cli/commands/auth.py +55 -0
- pygitcode-0.1.2/src/gitcode_cli/commands/issue.py +595 -0
- {pygitcode-0.1.0 → pygitcode-0.1.2}/src/gitcode_cli/commands/pr.py +439 -211
- {pygitcode-0.1.0 → pygitcode-0.1.2}/src/gitcode_cli/errors.py +4 -0
- {pygitcode-0.1.0 → pygitcode-0.1.2}/src/gitcode_cli/formatters.py +15 -7
- pygitcode-0.1.2/src/gitcode_cli/helptext.py +220 -0
- {pygitcode-0.1.0 → pygitcode-0.1.2}/src/gitcode_cli/services/__init__.py +2 -1
- {pygitcode-0.1.0 → pygitcode-0.1.2}/src/gitcode_cli/services/issues.py +5 -2
- {pygitcode-0.1.0 → pygitcode-0.1.2}/src/gitcode_cli/services/pulls.py +3 -0
- pygitcode-0.1.2/src/gitcode_cli/services/users.py +13 -0
- {pygitcode-0.1.0 → pygitcode-0.1.2}/src/gitcode_cli/utils.py +34 -5
- {pygitcode-0.1.0 → pygitcode-0.1.2}/src/pygitcode.egg-info/PKG-INFO +49 -12
- {pygitcode-0.1.0 → pygitcode-0.1.2}/src/pygitcode.egg-info/SOURCES.txt +7 -0
- {pygitcode-0.1.0 → pygitcode-0.1.2}/src/pygitcode.egg-info/entry_points.txt +1 -0
- pygitcode-0.1.0/src/gitcode_cli/cli.py +0 -44
- pygitcode-0.1.0/src/gitcode_cli/commands/auth.py +0 -23
- pygitcode-0.1.0/src/gitcode_cli/commands/issue.py +0 -338
- {pygitcode-0.1.0 → pygitcode-0.1.2}/LICENSE +0 -0
- {pygitcode-0.1.0 → pygitcode-0.1.2}/setup.cfg +0 -0
- {pygitcode-0.1.0 → pygitcode-0.1.2}/src/gitcode_cli/commands/__init__.py +0 -0
- {pygitcode-0.1.0 → pygitcode-0.1.2}/src/gitcode_cli/config.py +0 -0
- {pygitcode-0.1.0 → pygitcode-0.1.2}/src/gitcode_cli/context.py +0 -0
- {pygitcode-0.1.0 → pygitcode-0.1.2}/src/gitcode_cli/repo.py +0 -0
- {pygitcode-0.1.0 → pygitcode-0.1.2}/src/pygitcode.egg-info/dependency_links.txt +0 -0
- {pygitcode-0.1.0 → pygitcode-0.1.2}/src/pygitcode.egg-info/requires.txt +0 -0
- {pygitcode-0.1.0 → pygitcode-0.1.2}/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.2
|
|
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
|
|
|
@@ -53,7 +52,27 @@ Dynamic: license-file
|
|
|
53
52
|
pip install pygitcode
|
|
54
53
|
```
|
|
55
54
|
|
|
56
|
-
This exposes the `gc` command in your shell.
|
|
55
|
+
This exposes the `gc` (or `gitcode`) command in your shell.
|
|
56
|
+
|
|
57
|
+
## Windows PowerShell Users
|
|
58
|
+
|
|
59
|
+
On Windows PowerShell, `gc` is a built-in alias for the `Get-Content` cmdlet, which shadows the GitCode CLI. Use `gitcode` instead:
|
|
60
|
+
|
|
61
|
+
```powershell
|
|
62
|
+
gitcode auth login
|
|
63
|
+
gitcode issue list
|
|
64
|
+
gitcode pr list
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
Alternatively, you can remove or override the alias in your PowerShell profile:
|
|
68
|
+
|
|
69
|
+
```powershell
|
|
70
|
+
# Remove for the current session
|
|
71
|
+
Remove-Item Alias:gc -Force
|
|
72
|
+
|
|
73
|
+
# Or persist the override in your profile
|
|
74
|
+
Set-Alias -Name gc -Value 'C:\Users\<user>\AppData\Roaming\Python\Python313\Scripts\gc.exe'
|
|
75
|
+
```
|
|
57
76
|
|
|
58
77
|
## Quick Start
|
|
59
78
|
|
|
@@ -63,6 +82,15 @@ This exposes the `gc` command in your shell.
|
|
|
63
82
|
# Login with your GitCode personal access token
|
|
64
83
|
gc auth login
|
|
65
84
|
|
|
85
|
+
# Check auth status
|
|
86
|
+
gc auth status
|
|
87
|
+
|
|
88
|
+
# Print the current auth token
|
|
89
|
+
gc auth token
|
|
90
|
+
|
|
91
|
+
# Logout
|
|
92
|
+
gc auth logout
|
|
93
|
+
|
|
66
94
|
# Or set environment variable
|
|
67
95
|
export GC_TOKEN=your_token_here
|
|
68
96
|
```
|
|
@@ -83,21 +111,24 @@ gc issue view 42 --web
|
|
|
83
111
|
|
|
84
112
|
# Create an issue
|
|
85
113
|
gc issue create -t "Bug report" -b "Something is broken"
|
|
114
|
+
gc issue create --label bug --label "help wanted" # Multiple labels
|
|
86
115
|
gc issue create --web # Create in browser
|
|
87
116
|
|
|
88
|
-
# Edit an issue (
|
|
117
|
+
# Edit an issue (title, labels, assignee, milestone)
|
|
89
118
|
gc issue edit 42 -t "Updated title"
|
|
90
|
-
gc issue edit 42 --add-label bug --
|
|
91
|
-
gc issue edit 42 --add-assignee @me
|
|
119
|
+
gc issue edit 42 --add-label bug --add-label docs
|
|
120
|
+
gc issue edit 42 --add-assignee @me
|
|
121
|
+
gc issue edit 42 --milestone v1.0 --remove-milestone
|
|
92
122
|
|
|
93
123
|
# Comment on an issue
|
|
94
124
|
gc issue comment 42 -b "Thanks for the report!"
|
|
95
125
|
gc issue comment 42 --editor # Use system editor
|
|
96
126
|
|
|
97
|
-
#
|
|
127
|
+
gc issue delete 42 # Unsupported by GitCode API
|
|
128
|
+
|
|
129
|
+
# Close / reopen
|
|
98
130
|
gc issue close 42
|
|
99
|
-
gc issue
|
|
100
|
-
gc issue delete 42
|
|
131
|
+
gc issue close 42 -c "Fixed in #50" --reason completed
|
|
101
132
|
```
|
|
102
133
|
|
|
103
134
|
### Pull Requests
|
|
@@ -112,6 +143,7 @@ gc pr list --web # Open in browser
|
|
|
112
143
|
# View a PR (identifier optional - infers from current branch)
|
|
113
144
|
gc pr view # View PR for current branch
|
|
114
145
|
gc pr view 42 # By number
|
|
146
|
+
gc pr view 42 --comments # Include comments
|
|
115
147
|
gc pr view https://gitcode.com/owner/repo/pulls/42
|
|
116
148
|
gc pr view feature-branch
|
|
117
149
|
|
|
@@ -140,11 +172,13 @@ gc pr close --delete-branch # Delete remote branch too
|
|
|
140
172
|
gc pr merge # Merge PR for current branch
|
|
141
173
|
gc pr merge 42 -s # Squash merge
|
|
142
174
|
gc pr merge --rebase
|
|
175
|
+
gc pr merge --delete-branch # Delete remote branch after merge
|
|
143
176
|
|
|
144
|
-
# Edit a PR (add/remove labels, assignees, reviewers)
|
|
177
|
+
# Edit a PR (add/remove labels, assignees, reviewers, milestones)
|
|
145
178
|
gc pr edit 42 -t "New title"
|
|
146
179
|
gc pr edit --add-label bug --remove-label "needs review"
|
|
147
180
|
gc pr edit --add-reviewer @me --remove-reviewer otheruser
|
|
181
|
+
gc pr edit --milestone v1.0 --remove-milestone
|
|
148
182
|
|
|
149
183
|
# Mark as ready or convert to draft
|
|
150
184
|
gc pr ready # Mark current branch's PR as ready
|
|
@@ -153,6 +187,8 @@ gc pr ready --undo # Convert back to draft
|
|
|
153
187
|
# Comment / review / diff (identifier optional)
|
|
154
188
|
gc pr comment -b "LGTM"
|
|
155
189
|
gc pr comment --path src/file.py --position 5 -b "Suggestion"
|
|
190
|
+
gc pr comment --body-file comment.txt
|
|
191
|
+
gc pr comment --editor
|
|
156
192
|
|
|
157
193
|
gc pr review --approve
|
|
158
194
|
gc pr review --comment -b "Looks good but needs tests"
|
|
@@ -204,7 +240,7 @@ gc pr view -w
|
|
|
204
240
|
| `--web` (create/view in browser) | ✅ | ✅ |
|
|
205
241
|
| `--remove-*` flags for edit commands | ✅ | ✅ |
|
|
206
242
|
| `pr ready --undo` (convert to draft) | ✅ | ✅ |
|
|
207
|
-
| `pr review --
|
|
243
|
+
| `pr review --request-changes` | ✅ | ✅ (fallback) |
|
|
208
244
|
| `--json fields` | ✅ | ✅ |
|
|
209
245
|
| `-q jq` filtering | ✅ | ✅ |
|
|
210
246
|
| `-t template` formatting | ✅ | ✅ |
|
|
@@ -214,7 +250,8 @@ gc pr view -w
|
|
|
214
250
|
### Known Limitations (GitCode API differences)
|
|
215
251
|
|
|
216
252
|
- **PR comment model**: GitCode uses `path + position`, not GitHub's `line/side/commit`
|
|
217
|
-
- **PR review**: GitCode review API differs from GitHub; `--
|
|
253
|
+
- **PR review**: GitCode review API differs from GitHub; `--request-changes` falls back to PR comments
|
|
254
|
+
- **Issue deletion**: `gc issue delete` is exposed for CLI parity, but GitCode API does not support deleting issues
|
|
218
255
|
- **Issue create/update API**: GitCode puts `repo` in request body, not URL path
|
|
219
256
|
|
|
220
257
|
## Development
|
|
@@ -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
|
|
|
@@ -13,7 +12,27 @@
|
|
|
13
12
|
pip install pygitcode
|
|
14
13
|
```
|
|
15
14
|
|
|
16
|
-
This exposes the `gc` command in your shell.
|
|
15
|
+
This exposes the `gc` (or `gitcode`) command in your shell.
|
|
16
|
+
|
|
17
|
+
## Windows PowerShell Users
|
|
18
|
+
|
|
19
|
+
On Windows PowerShell, `gc` is a built-in alias for the `Get-Content` cmdlet, which shadows the GitCode CLI. Use `gitcode` instead:
|
|
20
|
+
|
|
21
|
+
```powershell
|
|
22
|
+
gitcode auth login
|
|
23
|
+
gitcode issue list
|
|
24
|
+
gitcode pr list
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Alternatively, you can remove or override the alias in your PowerShell profile:
|
|
28
|
+
|
|
29
|
+
```powershell
|
|
30
|
+
# Remove for the current session
|
|
31
|
+
Remove-Item Alias:gc -Force
|
|
32
|
+
|
|
33
|
+
# Or persist the override in your profile
|
|
34
|
+
Set-Alias -Name gc -Value 'C:\Users\<user>\AppData\Roaming\Python\Python313\Scripts\gc.exe'
|
|
35
|
+
```
|
|
17
36
|
|
|
18
37
|
## Quick Start
|
|
19
38
|
|
|
@@ -23,6 +42,15 @@ This exposes the `gc` command in your shell.
|
|
|
23
42
|
# Login with your GitCode personal access token
|
|
24
43
|
gc auth login
|
|
25
44
|
|
|
45
|
+
# Check auth status
|
|
46
|
+
gc auth status
|
|
47
|
+
|
|
48
|
+
# Print the current auth token
|
|
49
|
+
gc auth token
|
|
50
|
+
|
|
51
|
+
# Logout
|
|
52
|
+
gc auth logout
|
|
53
|
+
|
|
26
54
|
# Or set environment variable
|
|
27
55
|
export GC_TOKEN=your_token_here
|
|
28
56
|
```
|
|
@@ -43,21 +71,24 @@ gc issue view 42 --web
|
|
|
43
71
|
|
|
44
72
|
# Create an issue
|
|
45
73
|
gc issue create -t "Bug report" -b "Something is broken"
|
|
74
|
+
gc issue create --label bug --label "help wanted" # Multiple labels
|
|
46
75
|
gc issue create --web # Create in browser
|
|
47
76
|
|
|
48
|
-
# Edit an issue (
|
|
77
|
+
# Edit an issue (title, labels, assignee, milestone)
|
|
49
78
|
gc issue edit 42 -t "Updated title"
|
|
50
|
-
gc issue edit 42 --add-label bug --
|
|
51
|
-
gc issue edit 42 --add-assignee @me
|
|
79
|
+
gc issue edit 42 --add-label bug --add-label docs
|
|
80
|
+
gc issue edit 42 --add-assignee @me
|
|
81
|
+
gc issue edit 42 --milestone v1.0 --remove-milestone
|
|
52
82
|
|
|
53
83
|
# Comment on an issue
|
|
54
84
|
gc issue comment 42 -b "Thanks for the report!"
|
|
55
85
|
gc issue comment 42 --editor # Use system editor
|
|
56
86
|
|
|
57
|
-
#
|
|
87
|
+
gc issue delete 42 # Unsupported by GitCode API
|
|
88
|
+
|
|
89
|
+
# Close / reopen
|
|
58
90
|
gc issue close 42
|
|
59
|
-
gc issue
|
|
60
|
-
gc issue delete 42
|
|
91
|
+
gc issue close 42 -c "Fixed in #50" --reason completed
|
|
61
92
|
```
|
|
62
93
|
|
|
63
94
|
### Pull Requests
|
|
@@ -72,6 +103,7 @@ gc pr list --web # Open in browser
|
|
|
72
103
|
# View a PR (identifier optional - infers from current branch)
|
|
73
104
|
gc pr view # View PR for current branch
|
|
74
105
|
gc pr view 42 # By number
|
|
106
|
+
gc pr view 42 --comments # Include comments
|
|
75
107
|
gc pr view https://gitcode.com/owner/repo/pulls/42
|
|
76
108
|
gc pr view feature-branch
|
|
77
109
|
|
|
@@ -100,11 +132,13 @@ gc pr close --delete-branch # Delete remote branch too
|
|
|
100
132
|
gc pr merge # Merge PR for current branch
|
|
101
133
|
gc pr merge 42 -s # Squash merge
|
|
102
134
|
gc pr merge --rebase
|
|
135
|
+
gc pr merge --delete-branch # Delete remote branch after merge
|
|
103
136
|
|
|
104
|
-
# Edit a PR (add/remove labels, assignees, reviewers)
|
|
137
|
+
# Edit a PR (add/remove labels, assignees, reviewers, milestones)
|
|
105
138
|
gc pr edit 42 -t "New title"
|
|
106
139
|
gc pr edit --add-label bug --remove-label "needs review"
|
|
107
140
|
gc pr edit --add-reviewer @me --remove-reviewer otheruser
|
|
141
|
+
gc pr edit --milestone v1.0 --remove-milestone
|
|
108
142
|
|
|
109
143
|
# Mark as ready or convert to draft
|
|
110
144
|
gc pr ready # Mark current branch's PR as ready
|
|
@@ -113,6 +147,8 @@ gc pr ready --undo # Convert back to draft
|
|
|
113
147
|
# Comment / review / diff (identifier optional)
|
|
114
148
|
gc pr comment -b "LGTM"
|
|
115
149
|
gc pr comment --path src/file.py --position 5 -b "Suggestion"
|
|
150
|
+
gc pr comment --body-file comment.txt
|
|
151
|
+
gc pr comment --editor
|
|
116
152
|
|
|
117
153
|
gc pr review --approve
|
|
118
154
|
gc pr review --comment -b "Looks good but needs tests"
|
|
@@ -164,7 +200,7 @@ gc pr view -w
|
|
|
164
200
|
| `--web` (create/view in browser) | ✅ | ✅ |
|
|
165
201
|
| `--remove-*` flags for edit commands | ✅ | ✅ |
|
|
166
202
|
| `pr ready --undo` (convert to draft) | ✅ | ✅ |
|
|
167
|
-
| `pr review --
|
|
203
|
+
| `pr review --request-changes` | ✅ | ✅ (fallback) |
|
|
168
204
|
| `--json fields` | ✅ | ✅ |
|
|
169
205
|
| `-q jq` filtering | ✅ | ✅ |
|
|
170
206
|
| `-t template` formatting | ✅ | ✅ |
|
|
@@ -174,7 +210,8 @@ gc pr view -w
|
|
|
174
210
|
### Known Limitations (GitCode API differences)
|
|
175
211
|
|
|
176
212
|
- **PR comment model**: GitCode uses `path + position`, not GitHub's `line/side/commit`
|
|
177
|
-
- **PR review**: GitCode review API differs from GitHub; `--
|
|
213
|
+
- **PR review**: GitCode review API differs from GitHub; `--request-changes` falls back to PR comments
|
|
214
|
+
- **Issue deletion**: `gc issue delete` is exposed for CLI parity, but GitCode API does not support deleting issues
|
|
178
215
|
- **Issue create/update API**: GitCode puts `repo` in request body, not URL path
|
|
179
216
|
|
|
180
217
|
## Development
|
|
@@ -206,4 +243,4 @@ See [ROADMAP.md](ROADMAP.md) for planned features and current gaps.
|
|
|
206
243
|
|
|
207
244
|
## License
|
|
208
245
|
|
|
209
|
-
MIT
|
|
246
|
+
MIT
|
|
@@ -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"
|
|
@@ -53,10 +53,14 @@ Issues = "https://github.com/yourusername/pygitcode/issues"
|
|
|
53
53
|
|
|
54
54
|
[project.scripts]
|
|
55
55
|
gc = "gitcode_cli.cli:main"
|
|
56
|
+
gitcode = "gitcode_cli.cli:main"
|
|
56
57
|
|
|
57
58
|
[tool.setuptools.packages.find]
|
|
58
59
|
where = ["src"]
|
|
59
60
|
|
|
61
|
+
[tool.setuptools.dynamic]
|
|
62
|
+
version = {attr = "gitcode_cli.__version__"}
|
|
63
|
+
|
|
60
64
|
# --- pytest ---
|
|
61
65
|
[tool.pytest.ini_options]
|
|
62
66
|
testpaths = ["tests"]
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
@dataclass
|
|
8
|
+
class AdapterActionResult:
|
|
9
|
+
item: dict[str, Any] | None = None
|
|
10
|
+
items: list[dict[str, Any]] | None = None
|
|
11
|
+
message: str | None = None
|
|
12
|
+
warning: str | None = None
|
|
13
|
+
degraded: bool = False
|
|
14
|
+
approximated: bool = False
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import click
|
|
4
|
+
|
|
5
|
+
CAPABILITY_MESSAGES = {
|
|
6
|
+
"ISSUE_COMMENT_LAST_NOT_FOUND": "No editable issue comment from the current user was found.",
|
|
7
|
+
"ISSUE_COMMENT_OWNERSHIP_UNVERIFIABLE": (
|
|
8
|
+
"Unable to verify the current user's issue comments on GitCode; refusing to edit or delete comments safely."
|
|
9
|
+
),
|
|
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
|
+
"ISSUE_DEVELOP_BASE": "--base and --name are not supported by 'gc issue develop'",
|
|
13
|
+
"ISSUE_DEVELOP_NAME": "--base and --name are not supported by 'gc issue develop'",
|
|
14
|
+
"ISSUE_STATUS_GH_SEMANTICS": "GitCode-limited approximation of gh issue status",
|
|
15
|
+
"PR_MERGE_AUTHOR_EMAIL": "GitCode merge API does not support --author-email.",
|
|
16
|
+
"PR_MERGE_AUTO": "GitCode merge API does not support --auto.",
|
|
17
|
+
"PR_REVIEW_REQUEST_CHANGES": (
|
|
18
|
+
"GitCode review API does not support request-changes reviews; the pull request comment was posted instead."
|
|
19
|
+
),
|
|
20
|
+
"PR_STATUS_GH_SEMANTICS": ("GitCode API approximation -- user-specific filtering is not available"),
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def capability_message(feature: str) -> str:
|
|
25
|
+
return CAPABILITY_MESSAGES[feature]
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def unsupported(feature: str) -> click.ClickException:
|
|
29
|
+
return click.ClickException(capability_message(feature))
|
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
import click
|
|
6
|
+
|
|
7
|
+
from ..errors import APIError
|
|
8
|
+
from ..services import IssueService, UserService
|
|
9
|
+
from .base import AdapterActionResult
|
|
10
|
+
from .capabilities import capability_message, unsupported
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _normalize_multi_values(values: tuple[str, ...] | None) -> str | None:
|
|
14
|
+
if not values:
|
|
15
|
+
return None
|
|
16
|
+
return ",".join(values)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _extract_label_names(issue: dict[str, Any] | None) -> list[str]:
|
|
20
|
+
labels = issue.get("labels") if isinstance(issue, dict) else None
|
|
21
|
+
if not labels:
|
|
22
|
+
return []
|
|
23
|
+
if isinstance(labels, str):
|
|
24
|
+
return [part.strip() for part in labels.split(",") if part.strip()]
|
|
25
|
+
|
|
26
|
+
names: list[str] = []
|
|
27
|
+
if isinstance(labels, list):
|
|
28
|
+
for label in labels:
|
|
29
|
+
if isinstance(label, dict):
|
|
30
|
+
name = label.get("name") or label.get("title")
|
|
31
|
+
if name:
|
|
32
|
+
names.append(str(name))
|
|
33
|
+
elif label:
|
|
34
|
+
names.append(str(label))
|
|
35
|
+
return names
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _extract_login(data: dict[str, Any] | None) -> str | None:
|
|
39
|
+
if not isinstance(data, dict):
|
|
40
|
+
return None
|
|
41
|
+
for key in ("login", "username", "name"):
|
|
42
|
+
value = data.get(key)
|
|
43
|
+
if isinstance(value, str) and value.strip():
|
|
44
|
+
return value.strip()
|
|
45
|
+
user = data.get("user")
|
|
46
|
+
if isinstance(user, dict):
|
|
47
|
+
return _extract_login(user)
|
|
48
|
+
author = data.get("author")
|
|
49
|
+
if isinstance(author, dict):
|
|
50
|
+
return _extract_login(author)
|
|
51
|
+
return None
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class IssueAdapter:
|
|
55
|
+
def __init__(self, service: IssueService, user_service: UserService | None = None):
|
|
56
|
+
self.service = service
|
|
57
|
+
self.user_service = user_service
|
|
58
|
+
|
|
59
|
+
def list_issues(
|
|
60
|
+
self,
|
|
61
|
+
owner: str,
|
|
62
|
+
repo: str,
|
|
63
|
+
*,
|
|
64
|
+
state: str | None,
|
|
65
|
+
labels: tuple[str, ...] | None,
|
|
66
|
+
author: str | None,
|
|
67
|
+
assignee: str | None,
|
|
68
|
+
milestone: str | None,
|
|
69
|
+
mention: str | None,
|
|
70
|
+
search: str | None,
|
|
71
|
+
limit: int | None,
|
|
72
|
+
) -> Any:
|
|
73
|
+
items = self.service.list(
|
|
74
|
+
owner,
|
|
75
|
+
repo,
|
|
76
|
+
state=state,
|
|
77
|
+
labels=_normalize_multi_values(labels),
|
|
78
|
+
creator=author,
|
|
79
|
+
assignee=assignee,
|
|
80
|
+
milestone=milestone,
|
|
81
|
+
mention=mention,
|
|
82
|
+
search=search,
|
|
83
|
+
)
|
|
84
|
+
if limit is not None:
|
|
85
|
+
return items[:limit]
|
|
86
|
+
return items
|
|
87
|
+
|
|
88
|
+
def create_issue(
|
|
89
|
+
self,
|
|
90
|
+
owner: str,
|
|
91
|
+
repo: str,
|
|
92
|
+
*,
|
|
93
|
+
title: str,
|
|
94
|
+
body: str | None,
|
|
95
|
+
assignee: str | None,
|
|
96
|
+
labels: tuple[str, ...] | None,
|
|
97
|
+
milestone: str | None,
|
|
98
|
+
) -> dict[str, Any] | None:
|
|
99
|
+
return self.service.create(
|
|
100
|
+
owner,
|
|
101
|
+
repo,
|
|
102
|
+
title=title,
|
|
103
|
+
body=body,
|
|
104
|
+
assignee=assignee,
|
|
105
|
+
labels=_normalize_multi_values(labels),
|
|
106
|
+
milestone=milestone,
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
def close_issue(
|
|
110
|
+
self,
|
|
111
|
+
owner: str,
|
|
112
|
+
repo: str,
|
|
113
|
+
number: str,
|
|
114
|
+
*,
|
|
115
|
+
comment: str | None,
|
|
116
|
+
reason: str | None,
|
|
117
|
+
) -> AdapterActionResult:
|
|
118
|
+
current = self.service.get(owner, repo, number)
|
|
119
|
+
if current and current.get("state") == "closed":
|
|
120
|
+
if comment:
|
|
121
|
+
self.service.comment(owner, repo, number, comment)
|
|
122
|
+
return AdapterActionResult(item=current, message="already_closed_commented")
|
|
123
|
+
return AdapterActionResult(item=current, message="already_closed")
|
|
124
|
+
if comment:
|
|
125
|
+
self.service.comment(owner, repo, number, comment)
|
|
126
|
+
payload: dict[str, Any] = {"state": "close"}
|
|
127
|
+
if reason:
|
|
128
|
+
payload["state_reason"] = reason
|
|
129
|
+
item = self.service.update(owner, repo, number, **payload)
|
|
130
|
+
return AdapterActionResult(item=item, message="closed")
|
|
131
|
+
|
|
132
|
+
def comment_issue(self, owner: str, repo: str, number: str, *, body: str) -> dict[str, Any] | None:
|
|
133
|
+
return self.service.comment(owner, repo, number, body)
|
|
134
|
+
|
|
135
|
+
def manage_comment_history(
|
|
136
|
+
self,
|
|
137
|
+
owner: str,
|
|
138
|
+
repo: str,
|
|
139
|
+
number: str,
|
|
140
|
+
*,
|
|
141
|
+
body: str | None,
|
|
142
|
+
delete_last: bool,
|
|
143
|
+
create_if_none: bool,
|
|
144
|
+
) -> AdapterActionResult:
|
|
145
|
+
comment = self._find_last_owned_comment(owner, repo, number)
|
|
146
|
+
if delete_last:
|
|
147
|
+
if comment is None:
|
|
148
|
+
raise click.ClickException(capability_message("ISSUE_COMMENT_LAST_NOT_FOUND"))
|
|
149
|
+
self.service.delete_comment(owner, repo, comment["id"])
|
|
150
|
+
return AdapterActionResult(item=comment, message="deleted")
|
|
151
|
+
if comment is None:
|
|
152
|
+
if create_if_none:
|
|
153
|
+
item = self.service.comment(owner, repo, number, body or "")
|
|
154
|
+
return AdapterActionResult(item=item, message="created")
|
|
155
|
+
raise click.ClickException(capability_message("ISSUE_COMMENT_LAST_NOT_FOUND"))
|
|
156
|
+
item = self.service.update_comment(owner, repo, comment["id"], body or "")
|
|
157
|
+
return AdapterActionResult(item=item, message="edited")
|
|
158
|
+
|
|
159
|
+
def _find_last_owned_comment(self, owner: str, repo: str, number: str) -> dict[str, Any] | None:
|
|
160
|
+
if self.user_service is None:
|
|
161
|
+
raise click.ClickException(capability_message("ISSUE_COMMENT_OWNERSHIP_UNVERIFIABLE"))
|
|
162
|
+
try:
|
|
163
|
+
current_user = self.user_service.current()
|
|
164
|
+
except APIError as exc:
|
|
165
|
+
raise click.ClickException(capability_message("ISSUE_COMMENT_OWNERSHIP_UNVERIFIABLE")) from exc
|
|
166
|
+
current_login = _extract_login(current_user)
|
|
167
|
+
if not current_login:
|
|
168
|
+
raise click.ClickException(capability_message("ISSUE_COMMENT_OWNERSHIP_UNVERIFIABLE"))
|
|
169
|
+
comments = self.service.list_comments(owner, repo, number) or []
|
|
170
|
+
for comment in reversed(comments):
|
|
171
|
+
if not isinstance(comment, dict):
|
|
172
|
+
continue
|
|
173
|
+
if _extract_login(comment) == current_login:
|
|
174
|
+
return comment
|
|
175
|
+
return None
|
|
176
|
+
|
|
177
|
+
def reopen_issue(self, owner: str, repo: str, number: str) -> AdapterActionResult:
|
|
178
|
+
current = self.service.get(owner, repo, number)
|
|
179
|
+
if current and current.get("state") == "open":
|
|
180
|
+
return AdapterActionResult(item=current, message="already_open")
|
|
181
|
+
item = self.service.update(owner, repo, number, state="reopen")
|
|
182
|
+
return AdapterActionResult(item=item, message="reopened")
|
|
183
|
+
|
|
184
|
+
def delete_issue(self, owner: str, repo: str, number: str) -> AdapterActionResult: # noqa: ARG002
|
|
185
|
+
raise unsupported("ISSUE_DELETE")
|
|
186
|
+
|
|
187
|
+
def edit_issue(
|
|
188
|
+
self,
|
|
189
|
+
owner: str,
|
|
190
|
+
repo: str,
|
|
191
|
+
number: str,
|
|
192
|
+
*,
|
|
193
|
+
title: str | None,
|
|
194
|
+
body: str | None,
|
|
195
|
+
add_assignee: str | None,
|
|
196
|
+
add_labels: tuple[str, ...] | None,
|
|
197
|
+
milestone: str | None,
|
|
198
|
+
remove_milestone: bool,
|
|
199
|
+
) -> dict[str, Any] | None:
|
|
200
|
+
data = {k: v for k, v in {"title": title, "body": body}.items() if v is not None}
|
|
201
|
+
|
|
202
|
+
if add_assignee is not None:
|
|
203
|
+
data["assignee"] = add_assignee
|
|
204
|
+
|
|
205
|
+
if add_labels:
|
|
206
|
+
current = self.service.get(owner, repo, number)
|
|
207
|
+
existing = _extract_label_names(current)
|
|
208
|
+
merged: list[str] = []
|
|
209
|
+
seen: set[str] = set()
|
|
210
|
+
for label in [*existing, *list(add_labels)]:
|
|
211
|
+
normalized = label.strip()
|
|
212
|
+
if normalized and normalized not in seen:
|
|
213
|
+
seen.add(normalized)
|
|
214
|
+
merged.append(normalized)
|
|
215
|
+
data["labels"] = ",".join(merged)
|
|
216
|
+
|
|
217
|
+
if milestone is not None:
|
|
218
|
+
data["milestone"] = milestone
|
|
219
|
+
if remove_milestone:
|
|
220
|
+
data["milestone"] = ""
|
|
221
|
+
return self.service.update(owner, repo, number, **data)
|
|
222
|
+
|
|
223
|
+
def status(self, owner: str, repo: str) -> AdapterActionResult:
|
|
224
|
+
items = self.service.list(owner, repo, state="open")
|
|
225
|
+
return AdapterActionResult(
|
|
226
|
+
items=items,
|
|
227
|
+
message=capability_message("ISSUE_STATUS_GH_SEMANTICS"),
|
|
228
|
+
approximated=True,
|
|
229
|
+
)
|
|
230
|
+
|
|
231
|
+
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
|
+
return AdapterActionResult(
|
|
237
|
+
message=f"Opening issue #{number} in the browser instead.",
|
|
238
|
+
warning="Note: 'issue develop' does not create a local branch on GitCode.",
|
|
239
|
+
)
|