pygitcode 0.1.0__py3-none-any.whl

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.
@@ -0,0 +1,163 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import re
5
+ import shutil
6
+ import subprocess
7
+
8
+ import click
9
+
10
+
11
+ def dump_json(data, fields: list[str] | None = None) -> str:
12
+ if fields:
13
+ if isinstance(data, list):
14
+ data = [_filter_fields(item, fields) for item in data]
15
+ else:
16
+ data = _filter_fields(data, fields)
17
+ return json.dumps(data, ensure_ascii=False, indent=2)
18
+
19
+
20
+ def _filter_fields(item: dict, fields: list[str]) -> dict:
21
+ result = {}
22
+ for field in fields:
23
+ if "." in field:
24
+ parts = field.split(".")
25
+ value = item
26
+ for part in parts:
27
+ if isinstance(value, dict):
28
+ value = value.get(part)
29
+ else:
30
+ value = None
31
+ break
32
+ result[field] = value
33
+ else:
34
+ result[field] = item.get(field)
35
+ return result
36
+
37
+
38
+ def _login_from(item: dict, *keys: str) -> str:
39
+ for key in keys:
40
+ value = item.get(key)
41
+ if isinstance(value, dict):
42
+ login = value.get("login") or value.get("username") or value.get("name")
43
+ if login:
44
+ return str(login)
45
+ elif value:
46
+ return str(value)
47
+ return ""
48
+
49
+
50
+ def _list_row(item: dict, *, author_keys: tuple[str, ...]) -> str:
51
+ return "\t".join(
52
+ [
53
+ f"#{item.get('number', '')}",
54
+ str(item.get("state") or ""),
55
+ str(item.get("title") or ""),
56
+ _login_from(item, *author_keys),
57
+ ]
58
+ )
59
+
60
+
61
+ def format_issue_list(items: list[dict]) -> str:
62
+ return "\n".join(_list_row(item, author_keys=("author", "user", "creator")) for item in items)
63
+
64
+
65
+ def format_pr_list(items: list[dict]) -> str:
66
+ return "\n".join(_list_row(item, author_keys=("user", "author", "creator")) for item in items)
67
+
68
+
69
+ def _format_detail(item: dict, *, author_keys: tuple[str, ...], branch: str | None = None) -> str:
70
+ number = item.get("number", "")
71
+ title = item.get("title") or ""
72
+ lines = [f"#{number} {title}", "", f"Title:\t{title}"]
73
+
74
+ state = item.get("state")
75
+ if state:
76
+ lines.append(f"State:\t{state}")
77
+
78
+ author = _login_from(item, *author_keys)
79
+ if author:
80
+ lines.append(f"Author:\t{author}")
81
+
82
+ if branch:
83
+ lines.append(f"Branch:\t{branch}")
84
+
85
+ body = item.get("body") or ""
86
+ lines.extend(["", "Body:", body])
87
+ return "\n".join(lines)
88
+
89
+
90
+ def format_issue_detail(item: dict) -> str:
91
+ return _format_detail(item, author_keys=("author", "user", "creator"))
92
+
93
+
94
+ def _branch_label(value) -> str:
95
+ if isinstance(value, dict):
96
+ label = value.get("label") or value.get("ref")
97
+ return str(label) if label else ""
98
+ return str(value) if value else ""
99
+
100
+
101
+ def format_pr_detail(item: dict) -> str:
102
+ head = _branch_label(item.get("head"))
103
+ base = _branch_label(item.get("base"))
104
+ branch = f"{head} -> {base}" if head and base else None
105
+ return _format_detail(item, author_keys=("user", "author", "creator"), branch=branch)
106
+
107
+
108
+ def apply_jq(data, query: str):
109
+ try:
110
+ import jq # noqa: PLC0415
111
+
112
+ return jq.compile(query).input(data).all()
113
+ except ImportError:
114
+ pass
115
+ jq_bin = shutil.which("jq")
116
+ if jq_bin:
117
+ proc = subprocess.run(
118
+ [jq_bin, query],
119
+ input=json.dumps(data),
120
+ capture_output=True,
121
+ text=True,
122
+ check=True,
123
+ )
124
+ return json.loads(proc.stdout)
125
+ raise click.ClickException("jq is required for --jq. Install with: pip install pyjq or install jq CLI.")
126
+
127
+
128
+ def render_template(data, template: str) -> str:
129
+ def replacer(match):
130
+ key = match.group(1)
131
+ if "." in key:
132
+ parts = key.split(".")
133
+ value = data
134
+ for part in parts:
135
+ if isinstance(value, dict):
136
+ value = value.get(part)
137
+ else:
138
+ value = None
139
+ break
140
+ else:
141
+ value = data.get(key) if isinstance(data, dict) else None
142
+ return str(value) if value is not None else ""
143
+
144
+ return re.sub(r"\{\{\.(\w+(?:\.\w+)*)\}\}", replacer, template)
145
+
146
+
147
+ def output_result(data, json_fields: str | None, jq_query: str | None, template: str | None, default_formatter):
148
+ if jq_query:
149
+ data = apply_jq(data, jq_query)
150
+ click.echo(dump_json(data))
151
+ return
152
+ if json_fields:
153
+ fields = [f.strip() for f in json_fields.split(",")]
154
+ click.echo(dump_json(data, fields=fields))
155
+ return
156
+ if template:
157
+ if isinstance(data, list):
158
+ for item in data:
159
+ click.echo(render_template(item, template))
160
+ else:
161
+ click.echo(render_template(data, template))
162
+ return
163
+ default_formatter(data)
gitcode_cli/repo.py ADDED
@@ -0,0 +1,43 @@
1
+ from __future__ import annotations
2
+
3
+ import re
4
+ import subprocess
5
+
6
+ from .errors import RepoResolutionError
7
+
8
+ HTTPS_RE = re.compile(r"https?://[^/]+/(?P<owner>[^/]+)/(?P<repo>[^/.]+?)(?:\.git)?$")
9
+ SSH_RE = re.compile(r"git@[^:]+:(?P<owner>[^/]+)/(?P<repo>[^/.]+?)(?:\.git)?$")
10
+
11
+
12
+ def parse_repo(repo: str) -> tuple[str, str]:
13
+ parts = repo.split("/")
14
+ if len(parts) == 3:
15
+ _, owner, name = parts
16
+ elif len(parts) == 2:
17
+ owner, name = parts
18
+ else:
19
+ raise RepoResolutionError("Invalid repo format. Use OWNER/REPO or HOST/OWNER/REPO.")
20
+ return owner, name
21
+
22
+
23
+ def parse_remote_url(url: str) -> tuple[str, str]:
24
+ for pattern in (HTTPS_RE, SSH_RE):
25
+ match = pattern.match(url.strip())
26
+ if match:
27
+ return match.group("owner"), match.group("repo")
28
+ raise RepoResolutionError(f"Unsupported remote URL: {url}")
29
+
30
+
31
+ def resolve_repo(explicit_repo: str | None = None) -> tuple[str, str]:
32
+ if explicit_repo:
33
+ return parse_repo(explicit_repo)
34
+ try:
35
+ result = subprocess.run(
36
+ ["git", "remote", "get-url", "origin"],
37
+ capture_output=True,
38
+ text=True,
39
+ check=True,
40
+ )
41
+ except subprocess.CalledProcessError as exc:
42
+ raise RepoResolutionError("Unable to infer repo from current directory. Use -R OWNER/REPO.") from exc
43
+ return parse_remote_url(result.stdout.strip())
@@ -0,0 +1,6 @@
1
+ from __future__ import annotations
2
+
3
+ from .issues import IssueService
4
+ from .pulls import PullRequestService
5
+
6
+ __all__ = ["IssueService", "PullRequestService"]
@@ -0,0 +1,33 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any
4
+
5
+ from ..client import GitCodeClient
6
+
7
+
8
+ class IssueService:
9
+ def __init__(self, client: GitCodeClient):
10
+ self.client = client
11
+
12
+ def list(self, owner: str, repo: str, **params: Any) -> Any | None:
13
+ return self.client.get(f"/repos/{owner}/{repo}/issues", params=params)
14
+
15
+ def get(self, owner: str, repo: str, number: str) -> Any | None:
16
+ return self.client.get(f"/repos/{owner}/{repo}/issues/{number}")
17
+
18
+ def list_comments(self, owner: str, repo: str, number: str) -> Any | None:
19
+ return self.client.get(f"/repos/{owner}/{repo}/issues/{number}/comments")
20
+
21
+ def create(self, owner: str, repo: str, **data: Any) -> Any | None:
22
+ payload: dict[str, Any] = {"repo": repo, **{k: v for k, v in data.items() if v is not None}}
23
+ return self.client.post(f"/repos/{owner}/issues", json=payload)
24
+
25
+ def update(self, owner: str, repo: str, number: str, **data: Any) -> Any | None:
26
+ payload: dict[str, Any] = {"repo": repo, **{k: v for k, v in data.items() if v is not None}}
27
+ return self.client.patch(f"/repos/{owner}/issues/{number}", json=payload)
28
+
29
+ def comment(self, owner: str, repo: str, number: str, body: str) -> Any | None:
30
+ return self.client.post(f"/repos/{owner}/{repo}/issues/{number}/comments", json={"body": body})
31
+
32
+ def delete(self, owner: str, repo: str, number: str) -> Any | None:
33
+ return self.client.delete(f"/repos/{owner}/{repo}/issues/{number}")
@@ -0,0 +1,51 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any
4
+
5
+ from ..client import GitCodeClient
6
+
7
+
8
+ class PullRequestService:
9
+ def __init__(self, client: GitCodeClient):
10
+ self.client = client
11
+
12
+ def list(self, owner: str, repo: str, **params: Any) -> Any | None:
13
+ return self.client.get(f"/repos/{owner}/{repo}/pulls", params=params)
14
+
15
+ def get(self, owner: str, repo: str, number: int) -> Any | None:
16
+ return self.client.get(f"/repos/{owner}/{repo}/pulls/{number}")
17
+
18
+ def create(self, owner: str, repo: str, **data: Any) -> Any | None:
19
+ return self.client.post(f"/repos/{owner}/{repo}/pulls", json={k: v for k, v in data.items() if v is not None})
20
+
21
+ def update(self, owner: str, repo: str, number: int, **data: Any) -> Any | None:
22
+ return self.client.patch(
23
+ f"/repos/{owner}/{repo}/pulls/{number}", json={k: v for k, v in data.items() if v is not None}
24
+ )
25
+
26
+ def merge(self, owner: str, repo: str, number: int, **data: Any) -> Any | None:
27
+ return self.client.put(
28
+ f"/repos/{owner}/{repo}/pulls/{number}/merge", json={k: v for k, v in data.items() if v is not None}
29
+ )
30
+
31
+ def comment(
32
+ self, owner: str, repo: str, number: int, body: str, path: str | None = None, position: int | None = None
33
+ ) -> Any | None:
34
+ payload: dict[str, Any] = {"body": body, "path": path, "position": position}
35
+ return self.client.post(
36
+ f"/repos/{owner}/{repo}/pulls/{number}/comments", json={k: v for k, v in payload.items() if v is not None}
37
+ )
38
+
39
+ def review(self, owner: str, repo: str, number: int, body: str | None = None, force: bool = False) -> Any | None:
40
+ payload = {"body": body, "force": force}
41
+ filtered_payload = {k: v for k, v in payload.items() if v is not None}
42
+ return self.client.post(f"/repos/{owner}/{repo}/pulls/{number}/review", json=filtered_payload)
43
+
44
+ def diff(self, owner: str, repo: str, number: int) -> str:
45
+ response = self.client.request(
46
+ "GET",
47
+ f"/repos/{owner}/{repo}/pulls/{number}/diff",
48
+ accept="text/plain",
49
+ response_format="text",
50
+ )
51
+ return response or ""
gitcode_cli/utils.py ADDED
@@ -0,0 +1,118 @@
1
+ from __future__ import annotations
2
+
3
+ import re
4
+ import subprocess
5
+ import webbrowser
6
+ from pathlib import Path
7
+ from typing import TYPE_CHECKING
8
+
9
+ import click
10
+
11
+ if TYPE_CHECKING:
12
+ from .services import PullRequestService
13
+
14
+
15
+ def prompt_if_missing(value: str | None, prompt_text: str, hide_input: bool = False) -> str:
16
+ """If value is None or empty, use click.prompt interactively."""
17
+ if not value:
18
+ return click.prompt(prompt_text, hide_input=hide_input)
19
+ return value
20
+
21
+
22
+ def get_current_git_branch() -> str | None:
23
+ """Get current git branch name, return None on failure."""
24
+ try:
25
+ result = subprocess.run(
26
+ ["git", "rev-parse", "--abbrev-ref", "HEAD"],
27
+ capture_output=True,
28
+ text=True,
29
+ check=True,
30
+ )
31
+ return result.stdout.strip()
32
+ except subprocess.CalledProcessError:
33
+ return None
34
+
35
+
36
+ def get_default_git_branch() -> str | None:
37
+ """Try to get remote default branch (e.g., origin/HEAD), return None on failure."""
38
+ try:
39
+ result = subprocess.run(
40
+ ["git", "rev-parse", "--abbrev-ref", "origin/HEAD"],
41
+ capture_output=True,
42
+ text=True,
43
+ check=True,
44
+ )
45
+ branch = result.stdout.strip()
46
+ if branch.startswith("origin/"):
47
+ return branch[len("origin/") :]
48
+ return branch
49
+ except subprocess.CalledProcessError:
50
+ return None
51
+
52
+
53
+ def read_body_file(path: str) -> str:
54
+ """Read body content from a file."""
55
+ return Path(path).read_text(encoding="utf-8")
56
+
57
+
58
+ def open_in_browser(url: str) -> None:
59
+ """Open URL in default browser."""
60
+ webbrowser.open(url)
61
+
62
+
63
+ # --- Issue / PR identifier resolvers ---
64
+
65
+ ISSUE_URL_RE = re.compile(r"https?://[^/]+/(?P<owner>[^/]+)/(?P<repo>[^/]+)/issues/(?P<number>\d+)")
66
+ PR_URL_RE = re.compile(r"https?://[^/]+/(?P<owner>[^/]+)/(?P<repo>[^/]+)/pulls?/(?P<number>\d+)")
67
+
68
+
69
+ def parse_issue_url(url: str) -> tuple[str, str, str] | None:
70
+ """Parse an issue URL into (owner, repo, number). Returns None if not a match."""
71
+ match = ISSUE_URL_RE.match(url.strip())
72
+ if match:
73
+ return match.group("owner"), match.group("repo"), match.group("number")
74
+ return None
75
+
76
+
77
+ def parse_pr_url(url: str) -> tuple[str, str, str] | None:
78
+ """Parse a PR URL into (owner, repo, number). Returns None if not a match."""
79
+ match = PR_URL_RE.match(url.strip())
80
+ if match:
81
+ return match.group("owner"), match.group("repo"), match.group("number")
82
+ return None
83
+
84
+
85
+ def resolve_issue_arg(identifier: str):
86
+ """Resolve an issue identifier (number or URL) into (owner, repo, number).
87
+
88
+ Returns (None, None, number) if it's just a number, or (owner, repo, number) if a URL.
89
+ """
90
+ url_result = parse_issue_url(identifier)
91
+ if url_result:
92
+ return url_result
93
+ return None, None, identifier
94
+
95
+
96
+ def resolve_pr_arg(identifier: str, owner: str, repo: str, service: PullRequestService) -> tuple[str, str, str]:
97
+ """Resolve a PR identifier (number, URL, or branch) into (owner, repo, number).
98
+
99
+ Returns (owner, repo, number). If branch is given, queries the API to find the PR.
100
+ Raises click.ClickException if not found.
101
+ """
102
+ # Try URL first
103
+ url_result = parse_pr_url(identifier)
104
+ if url_result:
105
+ return url_result
106
+
107
+ # Try pure number
108
+ if identifier.isdigit():
109
+ return owner, repo, identifier
110
+
111
+ # Treat as branch name — search open PRs with this head branch
112
+ items = service.list(owner, repo, state="open", head=identifier)
113
+ for item in items:
114
+ head_ref = item.get("head", {}).get("ref", "")
115
+ if head_ref == identifier:
116
+ return owner, repo, str(item["number"])
117
+
118
+ raise click.ClickException(f"No open pull request found for branch '{identifier}'.")
@@ -0,0 +1,249 @@
1
+ Metadata-Version: 2.4
2
+ Name: pygitcode
3
+ Version: 0.1.0
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/yourusername/pygitcode
8
+ Project-URL: Documentation, https://github.com/yourusername/pygitcode#readme
9
+ Project-URL: Repository, https://github.com/yourusername/pygitcode.git
10
+ Project-URL: Issues, https://github.com/yourusername/pygitcode/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/yourusername/pygitcode/blob/main/LICENSE)
48
+ [![Tests](https://github.com/yourusername/pygitcode/workflows/Tests/badge.svg)](https://github.com/yourusername/pygitcode/actions)
49
+
50
+ ## Installation
51
+
52
+ ```bash
53
+ pip install pygitcode
54
+ ```
55
+
56
+ This exposes the `gc` command in your shell.
57
+
58
+ ## Quick Start
59
+
60
+ ### Authentication
61
+
62
+ ```bash
63
+ # Login with your GitCode personal access token
64
+ gc auth login
65
+
66
+ # Or set environment variable
67
+ export GC_TOKEN=your_token_here
68
+ ```
69
+
70
+ ### Issues
71
+
72
+ ```bash
73
+ # List issues (with filtering options)
74
+ gc issue list
75
+ gc issue list --state closed --author @me
76
+ gc issue list --label bug --label "help wanted"
77
+ gc issue list --web # Open in browser
78
+
79
+ # View an issue (with comments)
80
+ gc issue view 42
81
+ gc issue view 42 --comments
82
+ gc issue view 42 --web
83
+
84
+ # Create an issue
85
+ gc issue create -t "Bug report" -b "Something is broken"
86
+ gc issue create --web # Create in browser
87
+
88
+ # Edit an issue (add/remove labels, assignees)
89
+ 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
92
+
93
+ # Comment on an issue
94
+ gc issue comment 42 -b "Thanks for the report!"
95
+ gc issue comment 42 --editor # Use system editor
96
+
97
+ # Close / reopen / delete
98
+ gc issue close 42
99
+ gc issue reopen 42
100
+ gc issue delete 42
101
+ ```
102
+
103
+ ### Pull Requests
104
+
105
+ ```bash
106
+ # List PRs (with filtering options)
107
+ gc pr list
108
+ gc pr list --state merged --author @me
109
+ gc pr list --base main --draft
110
+ gc pr list --web # Open in browser
111
+
112
+ # View a PR (identifier optional - infers from current branch)
113
+ gc pr view # View PR for current branch
114
+ gc pr view 42 # By number
115
+ gc pr view https://gitcode.com/owner/repo/pulls/42
116
+ gc pr view feature-branch
117
+
118
+ # Create a PR (auto-detects current branch and default base)
119
+ gc pr create -t "Add new feature"
120
+
121
+ # Create with auto-fill from commits
122
+ gc pr create --fill # Use latest commit
123
+ gc pr create --fill-first # Use first commit
124
+ gc pr create --fill-verbose # Use all commits for body
125
+
126
+ # Create with editor
127
+ gc pr create --editor
128
+
129
+ # Preview without creating
130
+ gc pr create --dry-run
131
+
132
+ # Create in browser
133
+ gc pr create --web
134
+
135
+ # Close / merge / reopen (identifier optional)
136
+ gc pr close # Close PR for current branch
137
+ gc pr close 42 -c "Closing as stale"
138
+ gc pr close --delete-branch # Delete remote branch too
139
+
140
+ gc pr merge # Merge PR for current branch
141
+ gc pr merge 42 -s # Squash merge
142
+ gc pr merge --rebase
143
+
144
+ # Edit a PR (add/remove labels, assignees, reviewers)
145
+ gc pr edit 42 -t "New title"
146
+ gc pr edit --add-label bug --remove-label "needs review"
147
+ gc pr edit --add-reviewer @me --remove-reviewer otheruser
148
+
149
+ # Mark as ready or convert to draft
150
+ gc pr ready # Mark current branch's PR as ready
151
+ gc pr ready --undo # Convert back to draft
152
+
153
+ # Comment / review / diff (identifier optional)
154
+ gc pr comment -b "LGTM"
155
+ gc pr comment --path src/file.py --position 5 -b "Suggestion"
156
+
157
+ gc pr review --approve
158
+ gc pr review --comment -b "Looks good but needs tests"
159
+ gc pr review --request-changes -b "Missing documentation"
160
+
161
+ gc pr diff # View diff for current branch's PR
162
+ gc pr diff 42
163
+
164
+ # Checkout a PR (identifier optional)
165
+ gc pr checkout # Checkout PR for current branch? (prompts if ambiguous)
166
+ gc pr checkout 42
167
+ gc pr checkout -b local-branch-name
168
+ ```
169
+
170
+ ### Global Options
171
+
172
+ ```bash
173
+ # Use a different repo without cd-ing into it
174
+ gc issue list -R owner/repo
175
+ gc pr list -R owner/repo
176
+
177
+ # Output as JSON with field selection
178
+ gc issue list --json number,title,state,author
179
+ gc pr list --json number,title,state,head,base
180
+
181
+ # Filter with jq
182
+ gc issue list -q '.[] | select(.state == "open")'
183
+ gc pr list -q '.[] | select(.draft == true)'
184
+
185
+ # Format with Go-style templates
186
+ gc issue list -t '{{.number}} {{.title}} ({{.state}})'
187
+ gc pr view -t 'PR #{{.number}}: {{.title}}\n{{.body}}'
188
+
189
+ # Open in browser
190
+ gc issue view 42 -w
191
+ gc pr view -w
192
+ ```
193
+
194
+ ## Alignment with `gh` CLI
195
+
196
+ `pygitcode` aims to be as familiar as possible to `gh` users:
197
+
198
+ | Feature | `gh` | `gc` |
199
+ |---------|------|------|
200
+ | PR identifier optional (current branch inference) | ✅ | ✅ |
201
+ | `--fill` / `--fill-first` / `--fill-verbose` | ✅ | ✅ |
202
+ | `--editor` | ✅ | ✅ |
203
+ | `--dry-run` | ✅ | ✅ |
204
+ | `--web` (create/view in browser) | ✅ | ✅ |
205
+ | `--remove-*` flags for edit commands | ✅ | ✅ |
206
+ | `pr ready --undo` (convert to draft) | ✅ | ✅ |
207
+ | `pr review --comment` / `--request-changes` | ✅ | ✅ (fallback) |
208
+ | `--json fields` | ✅ | ✅ |
209
+ | `-q jq` filtering | ✅ | ✅ |
210
+ | `-t template` formatting | ✅ | ✅ |
211
+ | Command aliases (`ls` → `list`, `new` → `create`) | ✅ | ✅ |
212
+ | Multi-value options (`--label bug --label feature`) | ✅ | ✅ |
213
+
214
+ ### Known Limitations (GitCode API differences)
215
+
216
+ - **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
218
+ - **Issue create/update API**: GitCode puts `repo` in request body, not URL path
219
+
220
+ ## Development
221
+
222
+ ```bash
223
+ # Clone
224
+ git clone https://github.com/yourusername/pygitcode.git
225
+ cd pygitcode
226
+
227
+ # Install in editable mode with dev dependencies
228
+ pip install -e ".[dev]"
229
+
230
+ # Install pre-commit hooks
231
+ pre-commit install
232
+
233
+ # Run all checks manually
234
+ pre-commit run --all-files
235
+
236
+ # Or run individually
237
+ python -m pytest tests/unit/ --cov=gitcode_cli
238
+ python -m ruff check src/ tests/
239
+ python -m ruff format src/ tests/
240
+ python -m basedpyright src/
241
+ ```
242
+
243
+ ## Roadmap
244
+
245
+ See [ROADMAP.md](ROADMAP.md) for planned features and current gaps.
246
+
247
+ ## License
248
+
249
+ MIT
@@ -0,0 +1,23 @@
1
+ gitcode_cli/__init__.py,sha256=4t_crzhrLum--oyowUMxtjBTzUtWp7oRTF22ewEvJG4,49
2
+ gitcode_cli/cli.py,sha256=ryDkhUr4rSi7lk2231pNVsOCfu70N_qZ5ETbXrzxhrI,1265
3
+ gitcode_cli/cli_compat.py,sha256=wHQazS-GWqDa7biLq95Bizfj0lKsb-CwJL-Urii6geI,3769
4
+ gitcode_cli/client.py,sha256=yTzbueKJO8Zxd4y9e_mO4b5YBq7uRHPm7y_jHXbJH0c,2502
5
+ gitcode_cli/config.py,sha256=c3IuVrQ6BN2Y-LKoLM5dzmvmg80z31iINbtnR77PxsA,1037
6
+ gitcode_cli/context.py,sha256=VLYmN3kUwI_txDpsTwif27M9pOScK9jOQLOpPdnrJ3s,259
7
+ gitcode_cli/errors.py,sha256=9apEzj7k9WtDST8s7KNkOHfPQKwMRmRNLTVZKkIG2aM,361
8
+ gitcode_cli/formatters.py,sha256=dA8hv1KR9DwkRVzowejQ6FKljFk8e5suQKzkflBt1G4,4785
9
+ gitcode_cli/repo.py,sha256=nCXTMOGFX1FoOYZSA-uaw5Vml4HaDwE9XBltE4C6Yjw,1392
10
+ gitcode_cli/utils.py,sha256=PHkTxMD6w_dfo5qCNil8-R9kUOdre6PAUvvf-KkkOA0,3774
11
+ gitcode_cli/commands/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
12
+ gitcode_cli/commands/auth.py,sha256=ovK308_GXAyYIS8ssZv9v6WtqaOTqvay9mTh4SdmY8Y,609
13
+ gitcode_cli/commands/issue.py,sha256=fOR44m5V7dYKsSIZ4i0rqrL7oXsrf2okruiR8AaQ0Rk,11845
14
+ gitcode_cli/commands/pr.py,sha256=5XKaCq8OnC3Y3l3TmMXmIkeE4LLYNbldzePvUFKCZxE,19271
15
+ gitcode_cli/services/__init__.py,sha256=mLdxGZzPzLCXPEjYmUPmeHxVpf3MqXBURhiwFObEE2Q,157
16
+ gitcode_cli/services/issues.py,sha256=cA4LdFn-u2Bm5Na4L6PapqaXRUFk8SVY32gWQYwyDPk,1503
17
+ gitcode_cli/services/pulls.py,sha256=OuGpJGHnegKs-XsdgA7HkB1uo0zUpxZfLRjR7fY3l5Y,2201
18
+ pygitcode-0.1.0.dist-info/licenses/LICENSE,sha256=J2vlMgWrjCSYp3bRVoTkK5HNyfqvNLJIRG2Q7qVI1Io,1079
19
+ pygitcode-0.1.0.dist-info/METADATA,sha256=7mY8G9fjDMy2fBYDgfS3OqVQps7jbXbJuzlWhpxscqA,7596
20
+ pygitcode-0.1.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
21
+ pygitcode-0.1.0.dist-info/entry_points.txt,sha256=y7KnILZ9i90n805mpqkvGE_BgOgnZ0g8FDa68qHaDv8,44
22
+ pygitcode-0.1.0.dist-info/top_level.txt,sha256=Q4TjfscIckcgCfrL7_GwY1A9mpfGtuFyytgqCRngcjY,12
23
+ pygitcode-0.1.0.dist-info/RECORD,,