pygitcode 0.1.1__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 (39) hide show
  1. {pygitcode-0.1.1 → pygitcode-0.1.2}/PKG-INFO +28 -11
  2. {pygitcode-0.1.1 → pygitcode-0.1.2}/README.md +27 -10
  3. {pygitcode-0.1.1 → pygitcode-0.1.2}/pyproject.toml +4 -1
  4. {pygitcode-0.1.1 → 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.1 → pygitcode-0.1.2}/src/gitcode_cli/cli_compat.py +2 -0
  12. {pygitcode-0.1.1 → 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.2/src/gitcode_cli/commands/pr.py +725 -0
  16. {pygitcode-0.1.1 → pygitcode-0.1.2}/src/gitcode_cli/errors.py +4 -0
  17. {pygitcode-0.1.1 → 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.1 → pygitcode-0.1.2}/src/gitcode_cli/services/__init__.py +2 -1
  20. {pygitcode-0.1.1 → pygitcode-0.1.2}/src/gitcode_cli/services/issues.py +5 -2
  21. {pygitcode-0.1.1 → 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.1 → pygitcode-0.1.2}/src/gitcode_cli/utils.py +34 -5
  24. {pygitcode-0.1.1 → pygitcode-0.1.2}/src/pygitcode.egg-info/PKG-INFO +28 -11
  25. {pygitcode-0.1.1 → pygitcode-0.1.2}/src/pygitcode.egg-info/SOURCES.txt +7 -0
  26. pygitcode-0.1.1/src/gitcode_cli/cli.py +0 -44
  27. pygitcode-0.1.1/src/gitcode_cli/commands/auth.py +0 -23
  28. pygitcode-0.1.1/src/gitcode_cli/commands/issue.py +0 -338
  29. pygitcode-0.1.1/src/gitcode_cli/commands/pr.py +0 -496
  30. {pygitcode-0.1.1 → pygitcode-0.1.2}/LICENSE +0 -0
  31. {pygitcode-0.1.1 → pygitcode-0.1.2}/setup.cfg +0 -0
  32. {pygitcode-0.1.1 → pygitcode-0.1.2}/src/gitcode_cli/commands/__init__.py +0 -0
  33. {pygitcode-0.1.1 → pygitcode-0.1.2}/src/gitcode_cli/config.py +0 -0
  34. {pygitcode-0.1.1 → pygitcode-0.1.2}/src/gitcode_cli/context.py +0 -0
  35. {pygitcode-0.1.1 → pygitcode-0.1.2}/src/gitcode_cli/repo.py +0 -0
  36. {pygitcode-0.1.1 → pygitcode-0.1.2}/src/pygitcode.egg-info/dependency_links.txt +0 -0
  37. {pygitcode-0.1.1 → pygitcode-0.1.2}/src/pygitcode.egg-info/entry_points.txt +0 -0
  38. {pygitcode-0.1.1 → pygitcode-0.1.2}/src/pygitcode.egg-info/requires.txt +0 -0
  39. {pygitcode-0.1.1 → 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.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
  [![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/codeasier/gitcode-cli/workflows/Tests/badge.svg)](https://github.com/yourusername/pygitcode/actions)
49
48
 
50
49
  ## Installation
51
50
 
@@ -83,6 +82,15 @@ Set-Alias -Name gc -Value 'C:\Users\<user>\AppData\Roaming\Python\Python313\Scri
83
82
  # Login with your GitCode personal access token
84
83
  gc auth login
85
84
 
85
+ # Check auth status
86
+ gc auth status
87
+
88
+ # Print the current auth token
89
+ gc auth token
90
+
91
+ # Logout
92
+ gc auth logout
93
+
86
94
  # Or set environment variable
87
95
  export GC_TOKEN=your_token_here
88
96
  ```
@@ -103,21 +111,24 @@ gc issue view 42 --web
103
111
 
104
112
  # Create an issue
105
113
  gc issue create -t "Bug report" -b "Something is broken"
114
+ gc issue create --label bug --label "help wanted" # Multiple labels
106
115
  gc issue create --web # Create in browser
107
116
 
108
- # Edit an issue (add/remove labels, assignees)
117
+ # Edit an issue (title, labels, assignee, milestone)
109
118
  gc issue edit 42 -t "Updated title"
110
- gc issue edit 42 --add-label bug --remove-label "in progress"
111
- 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
112
122
 
113
123
  # Comment on an issue
114
124
  gc issue comment 42 -b "Thanks for the report!"
115
125
  gc issue comment 42 --editor # Use system editor
116
126
 
117
- # Close / reopen / delete
127
+ gc issue delete 42 # Unsupported by GitCode API
128
+
129
+ # Close / reopen
118
130
  gc issue close 42
119
- gc issue reopen 42
120
- gc issue delete 42
131
+ gc issue close 42 -c "Fixed in #50" --reason completed
121
132
  ```
122
133
 
123
134
  ### Pull Requests
@@ -132,6 +143,7 @@ gc pr list --web # Open in browser
132
143
  # View a PR (identifier optional - infers from current branch)
133
144
  gc pr view # View PR for current branch
134
145
  gc pr view 42 # By number
146
+ gc pr view 42 --comments # Include comments
135
147
  gc pr view https://gitcode.com/owner/repo/pulls/42
136
148
  gc pr view feature-branch
137
149
 
@@ -160,11 +172,13 @@ gc pr close --delete-branch # Delete remote branch too
160
172
  gc pr merge # Merge PR for current branch
161
173
  gc pr merge 42 -s # Squash merge
162
174
  gc pr merge --rebase
175
+ gc pr merge --delete-branch # Delete remote branch after merge
163
176
 
164
- # Edit a PR (add/remove labels, assignees, reviewers)
177
+ # Edit a PR (add/remove labels, assignees, reviewers, milestones)
165
178
  gc pr edit 42 -t "New title"
166
179
  gc pr edit --add-label bug --remove-label "needs review"
167
180
  gc pr edit --add-reviewer @me --remove-reviewer otheruser
181
+ gc pr edit --milestone v1.0 --remove-milestone
168
182
 
169
183
  # Mark as ready or convert to draft
170
184
  gc pr ready # Mark current branch's PR as ready
@@ -173,6 +187,8 @@ gc pr ready --undo # Convert back to draft
173
187
  # Comment / review / diff (identifier optional)
174
188
  gc pr comment -b "LGTM"
175
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
176
192
 
177
193
  gc pr review --approve
178
194
  gc pr review --comment -b "Looks good but needs tests"
@@ -224,7 +240,7 @@ gc pr view -w
224
240
  | `--web` (create/view in browser) | ✅ | ✅ |
225
241
  | `--remove-*` flags for edit commands | ✅ | ✅ |
226
242
  | `pr ready --undo` (convert to draft) | ✅ | ✅ |
227
- | `pr review --comment` / `--request-changes` | ✅ | ✅ (fallback) |
243
+ | `pr review --request-changes` | ✅ | ✅ (fallback) |
228
244
  | `--json fields` | ✅ | ✅ |
229
245
  | `-q jq` filtering | ✅ | ✅ |
230
246
  | `-t template` formatting | ✅ | ✅ |
@@ -234,7 +250,8 @@ gc pr view -w
234
250
  ### Known Limitations (GitCode API differences)
235
251
 
236
252
  - **PR comment model**: GitCode uses `path + position`, not GitHub's `line/side/commit`
237
- - **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
238
255
  - **Issue create/update API**: GitCode puts `repo` in request body, not URL path
239
256
 
240
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/codeasier/gitcode-cli/workflows/Tests/badge.svg)](https://github.com/yourusername/pygitcode/actions)
9
8
 
10
9
  ## Installation
11
10
 
@@ -43,6 +42,15 @@ Set-Alias -Name gc -Value 'C:\Users\<user>\AppData\Roaming\Python\Python313\Scri
43
42
  # Login with your GitCode personal access token
44
43
  gc auth login
45
44
 
45
+ # Check auth status
46
+ gc auth status
47
+
48
+ # Print the current auth token
49
+ gc auth token
50
+
51
+ # Logout
52
+ gc auth logout
53
+
46
54
  # Or set environment variable
47
55
  export GC_TOKEN=your_token_here
48
56
  ```
@@ -63,21 +71,24 @@ gc issue view 42 --web
63
71
 
64
72
  # Create an issue
65
73
  gc issue create -t "Bug report" -b "Something is broken"
74
+ gc issue create --label bug --label "help wanted" # Multiple labels
66
75
  gc issue create --web # Create in browser
67
76
 
68
- # Edit an issue (add/remove labels, assignees)
77
+ # Edit an issue (title, labels, assignee, milestone)
69
78
  gc issue edit 42 -t "Updated title"
70
- gc issue edit 42 --add-label bug --remove-label "in progress"
71
- 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
72
82
 
73
83
  # Comment on an issue
74
84
  gc issue comment 42 -b "Thanks for the report!"
75
85
  gc issue comment 42 --editor # Use system editor
76
86
 
77
- # Close / reopen / delete
87
+ gc issue delete 42 # Unsupported by GitCode API
88
+
89
+ # Close / reopen
78
90
  gc issue close 42
79
- gc issue reopen 42
80
- gc issue delete 42
91
+ gc issue close 42 -c "Fixed in #50" --reason completed
81
92
  ```
82
93
 
83
94
  ### Pull Requests
@@ -92,6 +103,7 @@ gc pr list --web # Open in browser
92
103
  # View a PR (identifier optional - infers from current branch)
93
104
  gc pr view # View PR for current branch
94
105
  gc pr view 42 # By number
106
+ gc pr view 42 --comments # Include comments
95
107
  gc pr view https://gitcode.com/owner/repo/pulls/42
96
108
  gc pr view feature-branch
97
109
 
@@ -120,11 +132,13 @@ gc pr close --delete-branch # Delete remote branch too
120
132
  gc pr merge # Merge PR for current branch
121
133
  gc pr merge 42 -s # Squash merge
122
134
  gc pr merge --rebase
135
+ gc pr merge --delete-branch # Delete remote branch after merge
123
136
 
124
- # Edit a PR (add/remove labels, assignees, reviewers)
137
+ # Edit a PR (add/remove labels, assignees, reviewers, milestones)
125
138
  gc pr edit 42 -t "New title"
126
139
  gc pr edit --add-label bug --remove-label "needs review"
127
140
  gc pr edit --add-reviewer @me --remove-reviewer otheruser
141
+ gc pr edit --milestone v1.0 --remove-milestone
128
142
 
129
143
  # Mark as ready or convert to draft
130
144
  gc pr ready # Mark current branch's PR as ready
@@ -133,6 +147,8 @@ gc pr ready --undo # Convert back to draft
133
147
  # Comment / review / diff (identifier optional)
134
148
  gc pr comment -b "LGTM"
135
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
136
152
 
137
153
  gc pr review --approve
138
154
  gc pr review --comment -b "Looks good but needs tests"
@@ -184,7 +200,7 @@ gc pr view -w
184
200
  | `--web` (create/view in browser) | ✅ | ✅ |
185
201
  | `--remove-*` flags for edit commands | ✅ | ✅ |
186
202
  | `pr ready --undo` (convert to draft) | ✅ | ✅ |
187
- | `pr review --comment` / `--request-changes` | ✅ | ✅ (fallback) |
203
+ | `pr review --request-changes` | ✅ | ✅ (fallback) |
188
204
  | `--json fields` | ✅ | ✅ |
189
205
  | `-q jq` filtering | ✅ | ✅ |
190
206
  | `-t template` formatting | ✅ | ✅ |
@@ -194,7 +210,8 @@ gc pr view -w
194
210
  ### Known Limitations (GitCode API differences)
195
211
 
196
212
  - **PR comment model**: GitCode uses `path + position`, not GitHub's `line/side/commit`
197
- - **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
198
215
  - **Issue create/update API**: GitCode puts `repo` in request body, not URL path
199
216
 
200
217
  ## Development
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "pygitcode"
7
- version = "0.1.1"
7
+ dynamic = ["version"]
8
8
  description = "A CLI tool for GitCode (api.gitcode.com), modeled after GitHub CLI."
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -58,6 +58,9 @@ gitcode = "gitcode_cli.cli:main"
58
58
  [tool.setuptools.packages.find]
59
59
  where = ["src"]
60
60
 
61
+ [tool.setuptools.dynamic]
62
+ version = {attr = "gitcode_cli.__version__"}
63
+
61
64
  # --- pytest ---
62
65
  [tool.pytest.ini_options]
63
66
  testpaths = ["tests"]
@@ -1,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
+ )