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.
Files changed (38) hide show
  1. {pygitcode-0.1.0 → pygitcode-0.1.2}/PKG-INFO +49 -12
  2. {pygitcode-0.1.0 → pygitcode-0.1.2}/README.md +49 -12
  3. {pygitcode-0.1.0 → pygitcode-0.1.2}/pyproject.toml +5 -1
  4. {pygitcode-0.1.0 → pygitcode-0.1.2}/src/gitcode_cli/__init__.py +1 -1
  5. pygitcode-0.1.2/src/gitcode_cli/adapters/__init__.py +7 -0
  6. pygitcode-0.1.2/src/gitcode_cli/adapters/base.py +14 -0
  7. pygitcode-0.1.2/src/gitcode_cli/adapters/capabilities.py +29 -0
  8. pygitcode-0.1.2/src/gitcode_cli/adapters/issues.py +239 -0
  9. pygitcode-0.1.2/src/gitcode_cli/adapters/pulls.py +170 -0
  10. pygitcode-0.1.2/src/gitcode_cli/cli.py +109 -0
  11. {pygitcode-0.1.0 → pygitcode-0.1.2}/src/gitcode_cli/cli_compat.py +2 -0
  12. {pygitcode-0.1.0 → pygitcode-0.1.2}/src/gitcode_cli/client.py +22 -8
  13. pygitcode-0.1.2/src/gitcode_cli/commands/auth.py +55 -0
  14. pygitcode-0.1.2/src/gitcode_cli/commands/issue.py +595 -0
  15. {pygitcode-0.1.0 → pygitcode-0.1.2}/src/gitcode_cli/commands/pr.py +439 -211
  16. {pygitcode-0.1.0 → pygitcode-0.1.2}/src/gitcode_cli/errors.py +4 -0
  17. {pygitcode-0.1.0 → pygitcode-0.1.2}/src/gitcode_cli/formatters.py +15 -7
  18. pygitcode-0.1.2/src/gitcode_cli/helptext.py +220 -0
  19. {pygitcode-0.1.0 → pygitcode-0.1.2}/src/gitcode_cli/services/__init__.py +2 -1
  20. {pygitcode-0.1.0 → pygitcode-0.1.2}/src/gitcode_cli/services/issues.py +5 -2
  21. {pygitcode-0.1.0 → pygitcode-0.1.2}/src/gitcode_cli/services/pulls.py +3 -0
  22. pygitcode-0.1.2/src/gitcode_cli/services/users.py +13 -0
  23. {pygitcode-0.1.0 → pygitcode-0.1.2}/src/gitcode_cli/utils.py +34 -5
  24. {pygitcode-0.1.0 → pygitcode-0.1.2}/src/pygitcode.egg-info/PKG-INFO +49 -12
  25. {pygitcode-0.1.0 → pygitcode-0.1.2}/src/pygitcode.egg-info/SOURCES.txt +7 -0
  26. {pygitcode-0.1.0 → pygitcode-0.1.2}/src/pygitcode.egg-info/entry_points.txt +1 -0
  27. pygitcode-0.1.0/src/gitcode_cli/cli.py +0 -44
  28. pygitcode-0.1.0/src/gitcode_cli/commands/auth.py +0 -23
  29. pygitcode-0.1.0/src/gitcode_cli/commands/issue.py +0 -338
  30. {pygitcode-0.1.0 → pygitcode-0.1.2}/LICENSE +0 -0
  31. {pygitcode-0.1.0 → pygitcode-0.1.2}/setup.cfg +0 -0
  32. {pygitcode-0.1.0 → pygitcode-0.1.2}/src/gitcode_cli/commands/__init__.py +0 -0
  33. {pygitcode-0.1.0 → pygitcode-0.1.2}/src/gitcode_cli/config.py +0 -0
  34. {pygitcode-0.1.0 → pygitcode-0.1.2}/src/gitcode_cli/context.py +0 -0
  35. {pygitcode-0.1.0 → pygitcode-0.1.2}/src/gitcode_cli/repo.py +0 -0
  36. {pygitcode-0.1.0 → pygitcode-0.1.2}/src/pygitcode.egg-info/dependency_links.txt +0 -0
  37. {pygitcode-0.1.0 → pygitcode-0.1.2}/src/pygitcode.egg-info/requires.txt +0 -0
  38. {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.0
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
  [![PyPI](https://img.shields.io/pypi/v/pygitcode)](https://pypi.org/project/pygitcode/)
46
46
  [![Python](https://img.shields.io/pypi/pyversions/pygitcode)](https://pypi.org/project/pygitcode/)
47
47
  [![License](https://img.shields.io/pypi/l/pygitcode)](https://github.com/yourusername/pygitcode/blob/main/LICENSE)
48
- [![Tests](https://github.com/yourusername/pygitcode/workflows/Tests/badge.svg)](https://github.com/yourusername/pygitcode/actions)
49
48
 
50
49
  ## Installation
51
50
 
@@ -53,7 +52,27 @@ Dynamic: license-file
53
52
  pip install pygitcode
54
53
  ```
55
54
 
56
- This exposes the `gc` command in your shell.
55
+ This exposes the `gc` (or `gitcode`) command in your shell.
56
+
57
+ ## Windows PowerShell Users
58
+
59
+ On Windows PowerShell, `gc` is a built-in alias for the `Get-Content` cmdlet, which shadows the GitCode CLI. Use `gitcode` instead:
60
+
61
+ ```powershell
62
+ gitcode auth login
63
+ gitcode issue list
64
+ gitcode pr list
65
+ ```
66
+
67
+ Alternatively, you can remove or override the alias in your PowerShell profile:
68
+
69
+ ```powershell
70
+ # Remove for the current session
71
+ Remove-Item Alias:gc -Force
72
+
73
+ # Or persist the override in your profile
74
+ Set-Alias -Name gc -Value 'C:\Users\<user>\AppData\Roaming\Python\Python313\Scripts\gc.exe'
75
+ ```
57
76
 
58
77
  ## Quick Start
59
78
 
@@ -63,6 +82,15 @@ This exposes the `gc` command in your shell.
63
82
  # Login with your GitCode personal access token
64
83
  gc auth login
65
84
 
85
+ # Check auth status
86
+ gc auth status
87
+
88
+ # Print the current auth token
89
+ gc auth token
90
+
91
+ # Logout
92
+ gc auth logout
93
+
66
94
  # Or set environment variable
67
95
  export GC_TOKEN=your_token_here
68
96
  ```
@@ -83,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 (add/remove labels, assignees)
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 --remove-label "in progress"
91
- gc issue edit 42 --add-assignee @me --remove-assignee otheruser
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
- # Close / reopen / delete
127
+ gc issue delete 42 # Unsupported by GitCode API
128
+
129
+ # Close / reopen
98
130
  gc issue close 42
99
- gc issue reopen 42
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 --comment` / `--request-changes` | ✅ | ✅ (fallback) |
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; `--comment` and `--request-changes` fall back to PR comments
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
  [![PyPI](https://img.shields.io/pypi/v/pygitcode)](https://pypi.org/project/pygitcode/)
6
6
  [![Python](https://img.shields.io/pypi/pyversions/pygitcode)](https://pypi.org/project/pygitcode/)
7
7
  [![License](https://img.shields.io/pypi/l/pygitcode)](https://github.com/yourusername/pygitcode/blob/main/LICENSE)
8
- [![Tests](https://github.com/yourusername/pygitcode/workflows/Tests/badge.svg)](https://github.com/yourusername/pygitcode/actions)
9
8
 
10
9
  ## Installation
11
10
 
@@ -13,7 +12,27 @@
13
12
  pip install pygitcode
14
13
  ```
15
14
 
16
- This exposes the `gc` command in your shell.
15
+ This exposes the `gc` (or `gitcode`) command in your shell.
16
+
17
+ ## Windows PowerShell Users
18
+
19
+ On Windows PowerShell, `gc` is a built-in alias for the `Get-Content` cmdlet, which shadows the GitCode CLI. Use `gitcode` instead:
20
+
21
+ ```powershell
22
+ gitcode auth login
23
+ gitcode issue list
24
+ gitcode pr list
25
+ ```
26
+
27
+ Alternatively, you can remove or override the alias in your PowerShell profile:
28
+
29
+ ```powershell
30
+ # Remove for the current session
31
+ Remove-Item Alias:gc -Force
32
+
33
+ # Or persist the override in your profile
34
+ Set-Alias -Name gc -Value 'C:\Users\<user>\AppData\Roaming\Python\Python313\Scripts\gc.exe'
35
+ ```
17
36
 
18
37
  ## Quick Start
19
38
 
@@ -23,6 +42,15 @@ This exposes the `gc` command in your shell.
23
42
  # Login with your GitCode personal access token
24
43
  gc auth login
25
44
 
45
+ # Check auth status
46
+ gc auth status
47
+
48
+ # Print the current auth token
49
+ gc auth token
50
+
51
+ # Logout
52
+ gc auth logout
53
+
26
54
  # Or set environment variable
27
55
  export GC_TOKEN=your_token_here
28
56
  ```
@@ -43,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 (add/remove labels, assignees)
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 --remove-label "in progress"
51
- gc issue edit 42 --add-assignee @me --remove-assignee otheruser
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
- # Close / reopen / delete
87
+ gc issue delete 42 # Unsupported by GitCode API
88
+
89
+ # Close / reopen
58
90
  gc issue close 42
59
- gc issue reopen 42
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 --comment` / `--request-changes` | ✅ | ✅ (fallback) |
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; `--comment` and `--request-changes` fall back to PR comments
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
- version = "0.1.0"
7
+ dynamic = ["version"]
8
8
  description = "A CLI tool for GitCode (api.gitcode.com), modeled after GitHub CLI."
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -53,10 +53,14 @@ Issues = "https://github.com/yourusername/pygitcode/issues"
53
53
 
54
54
  [project.scripts]
55
55
  gc = "gitcode_cli.cli:main"
56
+ gitcode = "gitcode_cli.cli:main"
56
57
 
57
58
  [tool.setuptools.packages.find]
58
59
  where = ["src"]
59
60
 
61
+ [tool.setuptools.dynamic]
62
+ version = {attr = "gitcode_cli.__version__"}
63
+
60
64
  # --- pytest ---
61
65
  [tool.pytest.ini_options]
62
66
  testpaths = ["tests"]
@@ -1,3 +1,3 @@
1
1
  __all__ = ["__version__"]
2
2
 
3
- __version__ = "0.1.0"
3
+ __version__ = "0.1.2"
@@ -0,0 +1,7 @@
1
+ from __future__ import annotations
2
+
3
+ from .base import AdapterActionResult
4
+ from .issues import IssueAdapter
5
+ from .pulls import PullRequestAdapter
6
+
7
+ __all__ = ["AdapterActionResult", "IssueAdapter", "PullRequestAdapter"]
@@ -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
+ )