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.
Files changed (38) hide show
  1. pygitcode-0.1.3/PKG-INFO +110 -0
  2. pygitcode-0.1.3/README.md +70 -0
  3. {pygitcode-0.1.2 → pygitcode-0.1.3}/pyproject.toml +4 -4
  4. {pygitcode-0.1.2 → pygitcode-0.1.3}/src/gitcode_cli/__init__.py +1 -1
  5. {pygitcode-0.1.2 → pygitcode-0.1.3}/src/gitcode_cli/adapters/capabilities.py +5 -2
  6. {pygitcode-0.1.2 → pygitcode-0.1.3}/src/gitcode_cli/adapters/issues.py +152 -18
  7. {pygitcode-0.1.2 → pygitcode-0.1.3}/src/gitcode_cli/adapters/pulls.py +44 -14
  8. {pygitcode-0.1.2 → pygitcode-0.1.3}/src/gitcode_cli/client.py +2 -2
  9. {pygitcode-0.1.2 → pygitcode-0.1.3}/src/gitcode_cli/commands/auth.py +14 -1
  10. {pygitcode-0.1.2 → pygitcode-0.1.3}/src/gitcode_cli/commands/issue.py +233 -50
  11. {pygitcode-0.1.2 → pygitcode-0.1.3}/src/gitcode_cli/commands/pr.py +208 -22
  12. {pygitcode-0.1.2 → pygitcode-0.1.3}/src/gitcode_cli/formatters.py +52 -36
  13. {pygitcode-0.1.2 → pygitcode-0.1.3}/src/gitcode_cli/services/issues.py +12 -0
  14. {pygitcode-0.1.2 → pygitcode-0.1.3}/src/gitcode_cli/utils.py +7 -0
  15. pygitcode-0.1.3/src/pygitcode.egg-info/PKG-INFO +110 -0
  16. pygitcode-0.1.2/PKG-INFO +0 -286
  17. pygitcode-0.1.2/README.md +0 -246
  18. pygitcode-0.1.2/src/pygitcode.egg-info/PKG-INFO +0 -286
  19. {pygitcode-0.1.2 → pygitcode-0.1.3}/LICENSE +0 -0
  20. {pygitcode-0.1.2 → pygitcode-0.1.3}/setup.cfg +0 -0
  21. {pygitcode-0.1.2 → pygitcode-0.1.3}/src/gitcode_cli/adapters/__init__.py +0 -0
  22. {pygitcode-0.1.2 → pygitcode-0.1.3}/src/gitcode_cli/adapters/base.py +0 -0
  23. {pygitcode-0.1.2 → pygitcode-0.1.3}/src/gitcode_cli/cli.py +0 -0
  24. {pygitcode-0.1.2 → pygitcode-0.1.3}/src/gitcode_cli/cli_compat.py +0 -0
  25. {pygitcode-0.1.2 → pygitcode-0.1.3}/src/gitcode_cli/commands/__init__.py +0 -0
  26. {pygitcode-0.1.2 → pygitcode-0.1.3}/src/gitcode_cli/config.py +0 -0
  27. {pygitcode-0.1.2 → pygitcode-0.1.3}/src/gitcode_cli/context.py +0 -0
  28. {pygitcode-0.1.2 → pygitcode-0.1.3}/src/gitcode_cli/errors.py +0 -0
  29. {pygitcode-0.1.2 → pygitcode-0.1.3}/src/gitcode_cli/helptext.py +0 -0
  30. {pygitcode-0.1.2 → pygitcode-0.1.3}/src/gitcode_cli/repo.py +0 -0
  31. {pygitcode-0.1.2 → pygitcode-0.1.3}/src/gitcode_cli/services/__init__.py +0 -0
  32. {pygitcode-0.1.2 → pygitcode-0.1.3}/src/gitcode_cli/services/pulls.py +0 -0
  33. {pygitcode-0.1.2 → pygitcode-0.1.3}/src/gitcode_cli/services/users.py +0 -0
  34. {pygitcode-0.1.2 → pygitcode-0.1.3}/src/pygitcode.egg-info/SOURCES.txt +0 -0
  35. {pygitcode-0.1.2 → pygitcode-0.1.3}/src/pygitcode.egg-info/dependency_links.txt +0 -0
  36. {pygitcode-0.1.2 → pygitcode-0.1.3}/src/pygitcode.egg-info/entry_points.txt +0 -0
  37. {pygitcode-0.1.2 → pygitcode-0.1.3}/src/pygitcode.egg-info/requires.txt +0 -0
  38. {pygitcode-0.1.2 → pygitcode-0.1.3}/src/pygitcode.egg-info/top_level.txt +0 -0
@@ -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
+ [![PyPI](https://img.shields.io/pypi/v/pygitcode)](https://pypi.org/project/pygitcode/)
46
+ [![Python](https://img.shields.io/pypi/pyversions/pygitcode)](https://pypi.org/project/pygitcode/)
47
+ [![License](https://img.shields.io/pypi/l/pygitcode)](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
+ [![PyPI](https://img.shields.io/pypi/v/pygitcode)](https://pypi.org/project/pygitcode/)
6
+ [![Python](https://img.shields.io/pypi/pyversions/pygitcode)](https://pypi.org/project/pygitcode/)
7
+ [![License](https://img.shields.io/pypi/l/pygitcode)](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/yourusername/pygitcode"
50
- Documentation = "https://github.com/yourusername/pygitcode#readme"
51
- Repository = "https://github.com/yourusername/pygitcode.git"
52
- Issues = "https://github.com/yourusername/pygitcode/issues"
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"
@@ -1,3 +1,3 @@
1
1
  __all__ = ["__version__"]
2
2
 
3
- __version__ = "0.1.2"
3
+ __version__ = "0.1.3"
@@ -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
- "ISSUE_STATUS_GH_SEMANTICS": "GitCode-limited approximation of gh issue status",
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, unsupported
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(labels),
106
- milestone=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: # noqa: ARG002
185
- raise unsupported("ISSUE_DELETE")
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
- milestone: str | None,
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
- items = self.service.list(owner, repo, state="open")
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
- items=items,
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
- items = self.service.list(
36
- owner,
37
- repo,
38
- state=state,
39
- author=author,
40
- base=base,
41
- assignee=assignee,
42
- draft=draft,
43
- head=head,
44
- labels=_normalize_multi_values(labels),
45
- search=search,
46
- )
47
- if limit is not None:
48
- return items[:limit]
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
- def auth_status() -> None:
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: