gckit:基于 Python 的 GitCode API 客户端与 CLI 项目

用户可通过该项目便捷地与 GitCode API 交互,实现仓库、问题、拉取请求等管理操作。提供异步 API 客户端、类型安全模型、自动重试机制及直观的命令行界面,支持 token 认证与配置持久化。【此简介由AI生成】

分支3Tags9

gckit

适用于 Python 的 GitCode API 客户端与命令行界面。

这是一个用于 GitCode API 的 Python 客户端库和命令行界面。它提供了异步接口,可用于操作仓库、问题、拉取请求、分支和用户。项目仓库:https://gitcode.com/pimenovdmitry/gckit。

特性

  • 异步 API 客户端:基于 httpx 构建的异步 GitCode 客户端
  • V4 + V5 API 支持:分别为不同的 GitCode API 版本提供 V5Client 和 V4Client
  • 类型安全模型:针对仓库、分支、问题、拉取请求、发布、标签、Webhook 和内容的类型化数据类模型
  • 带退避策略的重试:遇到速率限制和服务器错误时自动重试
  • 根级 CLI 命令:直接命令,如 gckit repo ...、gckit issue ... 和 gckit auth ...
  • 认证支持:基于令牌的认证,配置文件持久化存储
  • 搜索功能:搜索仓库、问题和用户
  • MCP 服务器:将所有 CLI 命令公开为 MCP 工具,供 opencode.ai 等 AI 代理使用(gckit mcp)

API 客户端

gckit 通过单独的客户端类支持两个 GitCode API 版本:

  • V5Client:使用 v5 API(/repos/{owner}/{repo}/...),采用 Bearer 令牌认证。这是默认客户端,为保持向后兼容性,也可通过 Client 使用。
  • V4Client:使用 v4 GitLab 兼容 API(/projects/{owner}%2F{repo}/...),采用 PRIVATE-TOKEN 认证。当需要使用仅在 v4 版本中可用的端点时,请使用此客户端。
from gckit import V5Client, V4Client

# v5 API (default)
async with V5Client(token='your-token') as client:
    user = await client.user('username')

# v4 API (GitLab-compatible) — for v4-only endpoints
async with V4Client(token='your-token') as client:
    discussions = await client.repo('owner', 'repo').pulls.discussions(42)

两个客户端共享相同的重试、日志记录和错误处理基础架构。

安装

开发环境安装:

git clone https://gitcode.com/pimenovdmitry/gckit.git
cd gckit
uv sync --extra dev

从检出的仓库中运行 CLI,命令如下:

./scripts/gckit --help

辅助脚本期望项目虚拟环境位于 .venv。

认证

首先,从您的账户设置获取 GitCode 访问令牌。然后进行认证:

# Save token for future use
gckit auth login <your-token>

# Check authentication status
gckit auth status

# Show the authenticated user
gckit auth whoami

# Remove saved token
gckit auth logout

CLI 的令牌查找优先级如下:

  1. --token
  2. GITCODE_TOKEN
  3. GITCODE_ACCESS_TOKEN
  4. ~/.config/gitcode/credentials

示例:

gckit --token <your-token> repo list
export GITCODE_TOKEN=<your-token>
export GITCODE_ACCESS_TOKEN=<your-token>

CLI 用法

默认 CLI 输出会打开 Textual 应用,供用户进行交互式操作。人工可读输出需要 TTY 环境。在非 TTY 会话中,请使用 --json 或 --yaml 参数以获取适合自动化的机器可读输出。

全局选项:

gckit --token <token> --debug ...
./scripts/gckit --token <token> --debug ...
gckit --help-all

如果在列表式命令中省略 --page,CLI 会自动分页遍历所有可用结果。

交互式 TUI

当命令有可浏览的行时,列表式的人性化输出会打开一个交互式 Textual 表格。使用 ↑/↓ 移动,Enter 打开所选行,Esc 返回,q 或 Ctrl+C 退出,r 重试加载失败的详情。表头显示部分名称和行数;详情屏幕在后台加载相关数据时,会保持主要项目可见。

可对仓库、分支、问题、拉取请求、标签、发布、搜索结果、用户和仓库内容目录项进行深度导航。问题和拉取请求详情屏幕会明确显示空的相关部分,并呈现部分相关数据加载失败的情况,而非将其隐藏。

命令

在交互式终端中运行时,人性化可读的命令会启动 Textual 应用:

$ gckit issue get owner/repo 123
# Opens an interactive Textual issue view.

自动化和重定向输出应明确要求结构化格式:

gckit issue get owner/repo 123 --json
gckit pr get owner/repo 456 --issues --yaml

在没有 TTY 或结构化输出标志的情况下,人工命令会快速失败,而非将面向屏幕的输出写入管道。

# Authentication
gckit auth login <token>
gckit auth logout
gckit auth status
gckit auth whoami
gckit auth whoami --json
gckit auth whoami --yaml

# Repositories
gckit repo list [owner]
gckit repo list [owner] --page 1 --per-page 50
gckit repo list [owner] --json
gckit repo list [owner] --yaml
gckit repo info <owner/repo>
gckit repo info <owner/repo> --json
gckit repo info <owner/repo> --yaml
gckit repo create <name> --description "demo" --private --auto-init
gckit repo create <name> --json
gckit repo create <name> --yaml
gckit repo update <owner/repo> --name "new-name" --description "new desc"
gckit repo update <owner/repo> --v4 --visibility public    # --v4 routes through V4Client
gckit repo delete <owner/repo> --yes
gckit repo fork <owner/repo> --organization my-org
gckit repo forks <owner/repo>
gckit repo stargazers <owner/repo>
gckit repo subscribers <owner/repo>
gckit repo contributors <owner/repo>
gckit repo languages <owner/repo>
gckit repo activity <owner/repo>
gckit repo transfer <owner/repo> <new_owner> --yes
gckit repo archive <owner/repo> --yes
gckit repo unarchive <owner/repo>
gckit repo settings get <owner/repo>
gckit repo settings update <owner/repo> --has-issues/--no-has-issues
gckit repo pr-settings get <owner/repo>
gckit repo pr-settings update <owner/repo> --merge-method merge
gckit repo push-config get <owner/repo>
gckit repo push-config set <owner/repo> --restrict-push/--no-restrict-push
gckit repo transition get <owner/repo>
gckit repo transition set <owner/repo> --enable-transition/--no-enable-transition
gckit repo module <owner/repo> --enable-module/--no-enable-module
gckit repo reviewer <owner/repo> --enable-reviewer --reviewer-count 3
gckit repo member update <owner/repo> <username> --permission admin
gckit repo roles <owner/repo>
gckit repo file-list <owner/repo>
gckit repo blob <owner/repo> <sha>
gckit repo raw <owner/repo> <path>
gckit repo trees <owner/repo> <sha>
gckit repo upload-image <owner/repo> <file>
gckit repo upload-file <owner/repo> <file>

# Branches
gckit branch list <owner/repo>
gckit branch list <owner/repo> --page 1 --per-page 50
gckit branch list <owner/repo> --json
gckit branch list <owner/repo> --yaml

# Content
gckit content get <owner/repo> <path>
gckit content get <owner/repo> <path> --ref develop
gckit content get <owner/repo> <path> --json
gckit content get <owner/repo> <path> --yaml
gckit content create <owner/repo> <path> --message "Add file" --content "Hello" --branch main
gckit content create <owner/repo> <path> --message "Add file" --content "Hello" --json
gckit content update <owner/repo> <path> --message "Update" --content "new" --sha <sha> --branch main
gckit content update <owner/repo> <path> --message "Update" --content "new" --sha <sha> --yaml
gckit content delete <owner/repo> <path> --message "Remove" --sha <sha> --yes

# Webhooks
gckit hook list <owner/repo>
gckit hook list <owner/repo> --page 1 --per-page 50
gckit hook list <owner/repo> --json
gckit hook list <owner/repo> --yaml
gckit hook get <owner/repo> <id>
gckit hook get <owner/repo> <id> --json
gckit hook get <owner/repo> <id> --yaml
gckit hook create <owner/repo> --url https://example.com/hook --events push,pull_request
gckit hook create <owner/repo> --url https://example.com/hook --events push --secret mys3cret --content-type json --insecure-ssl --json
gckit hook update <owner/repo> <id> --events push,pull_request
gckit hook update <owner/repo> <id> --url https://example.com/new-hook --active/--no-active --json
gckit hook delete <owner/repo> <id> --yes
gckit hook test <owner/repo> <id>
gckit hook test <owner/repo> <id> --json

# Tags
gckit tag list <owner/repo>
gckit tag list <owner/repo> --page 1 --per-page 50
gckit tag list <owner/repo> --json
gckit tag list <owner/repo> --yaml
gckit tag create <owner/repo> <tag_name> --ref main --message "v1.0.0"
gckit tag create <owner/repo> <tag_name> --ref main --json

# Releases
gckit release list <owner/repo>
gckit release list <owner/repo> --page 1 --per-page 50
gckit release list <owner/repo> --json
gckit release list <owner/repo> --yaml
gckit release create <owner/repo> <tag_name> --name "v1.0.0" --body "Release notes" --draft --prerelease
gckit release create <owner/repo> <tag_name> --name "v1.0.0" --json

# Issues
gckit issue list <owner/repo>
gckit issue list <owner/repo> --state all --page 1 --per-page 50
gckit issue list <owner/repo> --json
gckit issue list <owner/repo> --yaml
gckit issue get <owner/repo> <number>
gckit issue get <owner/repo> <number> --pull-requests
gckit issue get <owner/repo> <number> --comments --related-branches
gckit issue get <owner/repo> <number> --json
gckit issue get <owner/repo> <number> --yaml
gckit issue comments list <owner/repo> <number>
gckit issue branches list <owner/repo> <number>
gckit issue labels add <owner/repo> <number> bug "help wanted"
gckit issue labels add <owner/repo> <number> bug --json
gckit issue operate-logs <owner/repo> <number>
gckit issue create <owner/repo> <title> --body "details"
gckit issue create <owner/repo> <title> --json
gckit issue update <owner/repo> <number> --title "new title" --body "details"
gckit issue update <owner/repo> <number> --json
gckit issue close <owner/repo> <number>
gckit issue close <owner/repo> <number> --json
gckit issue close <owner/repo> <number> --yaml
gckit issue reopen <owner/repo> <number>
gckit issue reopen <owner/repo> <number> --json
gckit issue reopen <owner/repo> <number> --yaml
gckit issue pulls <owner/repo> <number>
gckit issue pulls <owner/repo> <number> --json
gckit issue pulls <owner/repo> <number> --yaml
gckit issue export <owner/repo> ./issues-export
gckit issue export <owner/repo> ./issues-export --state all --page 1 --per-page 50

# Issues — extended operations
gckit issue comments create <owner/repo> <number> --body "comment text"
gckit issue comments create <owner/repo> <number> --body "text" --json
gckit issue comments update <owner/repo> <number> <comment_id> --body "updated text"
gckit issue comments update <owner/repo> <number> <comment_id> --body "text" --yaml
gckit issue comments delete <owner/repo> <number> <comment_id> --yes
gckit issue comments delete <owner/repo> <number> <comment_id> --yes --json
gckit issue branches set <owner/repo> <number> <branch_name>...
gckit issue branches set <owner/repo> <number> main develop --json
gckit issue mine
gckit issue mine --state all --page 1 --per-page 50
gckit issue mine --json
gckit issue mine --yaml

# Pull Requests
gckit pr list <owner/repo>
gckit pr list <owner/repo> --state all --page 1 --per-page 50
gckit pr list <owner/repo> --search "Change-Id: I<change-id>"
gckit pr list <owner/repo> --json
gckit pr list <owner/repo> --yaml
gckit pr owner <owner> list
gckit pr owner <owner> list --state all --search "Change-Id: I<change-id>"
gckit pr owner <owner> list --json
gckit pr owner <owner> list --yaml
gckit pr get <owner/repo> <number>
gckit pr get <owner/repo> <number> --issues --comments --files --commits --merge-status
gckit pr get <owner/repo> <number> --json
gckit pr create <owner/repo> <title> --head feature --base main --body "details"
gckit pr create <owner/repo> <title> --head feature --base main --head-repo fork-owner --body "Cross-fork PR"
gckit pr create <owner/repo> <title> --yaml
gckit pr merge <owner/repo> <number> --merge-method squash --title "Merge title" --message "Merge body"
gckit pr merge <owner/repo> <number> --json
gckit pr merge <owner/repo> <number> --yaml
gckit pr close <owner/repo> <number>
gckit pr close <owner/repo> <number> --yaml

# Pull Requests — extended operations
gckit pr update <owner/repo> <number> --title "new title" --body "description"
gckit pr update <owner/repo> <number> --title "new title" --json
gckit pr comments create <owner/repo> <number> --body "comment text"
gckit pr comments create <owner/repo> <number> --body "inline review" --path src/file.py --commit-id abc123
gckit pr comments create <owner/repo> <number> --body "comment" --json
gckit pr comments update <owner/repo> <number> <comment_id> --body "updated text"
gckit pr comments update <owner/repo> <number> <comment_id> --body "text" --yaml
gckit pr comments delete <owner/repo> <number> <comment_id> --yes
gckit pr comments delete <owner/repo> <number> <comment_id> --yes --json
gckit pr review create <owner/repo> <number> --body "LGTM" --event APPROVED
gckit pr review create <owner/repo> <number> --body "needs work" --event REQUEST_CHANGES --json
gckit pr operate-logs <owner/repo> <number>
gckit pr operate-logs <owner/repo> <number> --json
gckit pr link <owner/repo> <number> <issue_number>...
gckit pr link <owner/repo> <number> 42 43 44 --json
gckit pr unlink <owner/repo> <number> <issue_number> --yes
gckit pr unlink <owner/repo> <number> 42 --yes --yaml

# Pull Requests — discussions (V4Client, v4 GitLab API)
gckit pr discussions <owner/repo> <number>
gckit pr discussions <owner/repo> <number> --per-page 50 --json
gckit pr discussions <owner/repo> <number> --yaml

# Search
gckit search repos <query>
gckit search repos <query> --sort stars --order desc --page 1 --json
gckit search issues <query>
gckit search issues <query> --sort created --order asc --yaml
gckit search users <query>
gckit search users <query> --sort followers --json

# Users
gckit user <username>
gckit user <username> --json
gckit user <username> --yaml

MCP 服务器

gckit 附带了一个 Model Context Protocol(MCP)服务器,该服务器提供基于 CLI 和仅 API 的工具。这使得 AI 代理(如 opencode.ai)能够通过标准工具界面与 GitCode 仓库、问题、PR 等进行交互。

启动服务器

gckit mcp

服务器采用标准输入输出(stdio)传输方式,不会向标准输出(stdout)打印任何内容,仅通过标准输入(stdin)监听 MCP JSON-RPC 消息。这是 AI 智能体集成的标准协议。

可用工具

MCP 服务器在 11 个资源组中提供了 92 个工具:

组 工具数量 描述
auth 1 whoami
repo 33 list, info, create, delete, update, fork, forks, stargazers, subscribers, contributors, languages, activity, transfer, archive, unarchive, settings (get/update), pr-settings (get/update), push-config (get/set), transition (get/set), module, reviewer, member update, roles, file-list, blob, raw, trees, upload-image, upload-file
branch 1 list
content 5 get, create, update, delete, readme
hook 6 list, get, create, update, delete, test
issue 16 list, get, create, update, close, reopen, mine, pulls, comments (list/create/update/delete), labels add, operate-logs, branches (list/set)
pr 21 list, get, create, merge, close, update, comments (create/update/delete), review create, operate-logs, link, unlink, discussions (list/get/create/reply/note update/note delete/resolve), owner list
release 3 list, create, update
tag 2 list, create
search 3 repos, issues, users
user 1 Fetch user by username

破坏性操作(delete、close、unlink)在其工具描述前会加上 [DESTRUCTIVE] 前缀,以便 AI 智能体在执行前进行标记。

身份验证

MCP 服务器通过与 CLI 相同的链来解析令牌:

  1. GITCODE_TOKEN 环境变量
  2. ~/.config/gitcode/credentials(通过 gckit auth login 保存)

您也可以在工具调用中直接将令牌作为 token 参数传递(大多数 MCP 客户端支持此方式)。

针对 opencode.ai 的配置

要在 opencode.ai 中使用 gckit 作为 MCP 工具提供方,请将其添加到 .opencode/opencode.json:

{
  // gckit MCP server — 92 tools for GitCode API
  "mcp": {
    "gckit": {
      "type": "local",
      "command": ["gckit", "mcp"],
      "enabled": true,
      "timeout": 30000
    }
  }
}

确保在环境中设置了 GITCODE_TOKEN,或者在启动代理前运行了 gckit auth login。

API 用法

Client 是 V5Client 的别名。两者可以互换使用。

import asyncio

from gckit import Client
from gckit.gitcode import GitCodeError, NotFoundError
from gckit.gitcode._retry import RetryConfig


async def main() -> None:
    async with Client(
        token='your-token',
        retry_config=RetryConfig(max_retries=5, backoff_factor=2.0, initial_delay=1.0),
    ) as client:
        user = await client.user('username')
        print(f'{user.login}: {user.name}')

        current_user = await client.get_current_user()
        print(current_user.login)

        repos = await client.list_repos(owner='organization', per_page=50)
        for repo in repos:
            print(f'{repo.full_name}: {repo.description}')

        async for repo in client.iter_repos(owner='organization', per_page=100):
            print(repo.full_name)

        repo_resource = client.repo('owner', 'repository')

        repo = await repo_resource.get_info()
        print(repo.default_branch)

        branches = await repo_resource.branches.list(per_page=50)
        issues = await repo_resource.issues.list(state='open', per_page=50)
        prs = await repo_resource.pulls.list(state='open', per_page=50)

        for branch in branches:
            print(branch.name)
        for issue in issues:
            print(f'#{issue.number}: {issue.title}')
        for pr in prs:
            print(f'#{pr.number}: {pr.title}')

        # Tags
        tags = await repo_resource.tags.list(per_page=50)
        async for tag in repo_resource.tags.iter_all():
            print(tag.name)

        # Releases
        releases = await repo_resource.releases.list(per_page=50)
        new_release = await repo_resource.releases.create(
            tag_name='v1.0.0',
            name='Version 1.0.0',
            body='Release notes',
        )

        # Content
        readme = await repo_resource.content.get_readme()
        file_content = await repo_resource.content.get('src/main.py')
        dir_listing = await repo_resource.content.get('src')
        created = await repo_resource.content.create(
            'new_file.py',
            message='Add file',
            content='print("hello")',
            branch='main',
        )
        updated = await repo_resource.content.update(
            'README.md',
            message='Update README',
            content='# Updated',
            sha='abc123',
            branch='main',
        )
        await repo_resource.content.delete(
            'old_file.py',
            message='Remove file',
            sha='def456',
            branch='main',
        )

        # Webhooks
        hooks = await repo_resource.hooks.list()
        async for hook in repo_resource.hooks.iter_all():
            print(f'Hook {hook.id}: {hook.url}')

        new_hook = await repo_resource.hooks.create(
            'https://example.com/hook',
            ['push', 'pull_request'],
            config={'content_type': 'json', 'secret': 's3cret'},
        )
        print(f'Created hook {new_hook.id}')

        updated_hook = await repo_resource.hooks.update(
            new_hook.id, events=['push']
        )
        print(f'Updated hook {updated_hook.id}')

        result = await repo_resource.hooks.test(new_hook.id)
        print(f'Test result: {result}')

        await repo_resource.hooks.delete(new_hook.id)

        # Search
        repo_results = await client.search_repositories('gckit', sort='stars', order='desc')
        issue_results = await client.search_issues('bug', state='open')
        user_results = await client.search_users('alice', sort='followers')

        created_issue = await repo_resource.issues.create(
            title='Bug report',
            body='Something is wrong',
        )
        await repo_resource.issues.comments(created_issue.number)
        await repo_resource.issues.related_branches(created_issue.number)
        await repo_resource.issues.pull_requests(created_issue.number)

        created_pr = await repo_resource.pulls.create(
            title='Fix bug',
            head='fix-branch',
            base='main',
            body='Fixes the reported bug.',
        )
        await repo_resource.pulls.issues(created_pr.number)

        try:
            await client.user('nonexistent-user')
        except NotFoundError:
            print('User not found')
        except GitCodeError as exc:
            print(f'API error: {exc}')


asyncio.run(main())

对于仅 v4 版本的端点(如 PR 讨论),请使用 V4Client:

import asyncio

from gckit import V4Client


async def main() -> None:
    async with V4Client(token='your-token') as client:
        repo = client.repo('owner', 'repository')

        # PR discussions (v4 GitLab-compatible API)
        discussions = await repo.pulls.discussions(42)
        for discussion in discussions:
            print(f'Discussion: {discussion.id}')

        # Update a repo through v4 API (e.g. visibility)
        updated = await repo.update(visibility='public')
        print(f'Updated: {updated.name}')


asyncio.run(main())

开发

设置开发环境

git clone https://gitcode.com/pimenovdmitry/gckit.git
cd gckit
./scripts/setup-dev.sh

该脚本会安装依赖项、预提交钩子,还可选择为 direnv 自动激活功能创建 .envrc 文件,并验证设置是否正确。若要跳过交互式提示或直接运行命令行界面:

uv sync --extra dev
uv run pre-commit install
./scripts/gckit --help

运行测试

uv run pytest
uv run pytest --cov=gckit
uv run pytest tests/unit/
uv run pytest tests/integration/ -m integration
uv run pytest tests/quality/test_unit_functional_overlap.py::test_unit_tests_do_not_duplicate_functional_coverage
uv run pytest -k "test_user"

默认情况下,pytest 会从 pyproject.toml 中读取配置 -m 'not slow and not real_api',因此执行 uv run pytest 会包含单元测试和模拟集成测试。实时 real_api 测试保持独立,必须显式指定才能运行。若要针对模拟集成测试覆盖率,可使用 uv run pytest tests/integration/ -m integration;若要使用默认标记选择运行集成文件,可执行 uv run pytest tests/integration/。

重复单元测试重叠检查(tests/quality/test_unit_functional_overlap.py::test_unit_tests_do_not_duplicate_functional_coverage)会将每个单元测试的覆盖率与功能测试覆盖率进行比较。如果检查报告某个单元测试已被功能测试完全覆盖,请删除该冗余单元测试,而非将其移至 tests/integration/ 目录。

CLI 测试涵盖全局选项(--token、--debug)、repo 等直接根命令、用于人工输出的 Textual 启动和非 TTY 拒绝机制,以及输出/分页标志,包括 --page、--per-page、--state、--json、--yaml、--pull-requests、--comments、--related-branches、--issues、--files、--commits 和 --merge-status。

代码质量

uv run ty check
uv run ruff check
uv run ruff check --fix
uv run ruff format
uv run ruff format --check

API 发现

除了已文档化的 v5 REST API 外,GitCode 还公开了多个 API 版本和网关。

API 基础 URL 路径格式 文档
v5 REST api.gitcode.com/api/v5 /repos/{owner}/{repo}/... docs.gitcode.com
v4 GitLab api.gitcode.com/api/v4 /projects/{owner}%2F{repo}/merge_requests/... 未文档化
Web API web-api.gitcode.com 内部(SPA) 未文档化

查找隐藏端点的方法

  1. JS 包:SPA 会加载包含 API 路径的供应商代码块:

    curl -sL https://gitcode.com | grep -oP 'assets/index-[^.]+\.js' | head -1
    # 然后在 JS 中搜索 API 模式:
    curl -s https://cdn-static.gitcode.com/assets/index-*.js | grep -oP '/api/v\\d/[^"]+' | sort -u
    
  2. API 版本探测:如果 v5 没有所需端点,可以尝试使用相同的 token 访问 v4、v3、v1:

    curl -s --header "PRIVATE-TOKEN: $GITCODE_TOKEN" \
      "https://api.gitcode.com/api/v4/projects/{owner}%2F{repo}/merge_requests/{iid}/discussions"
    
  3. 域名变体:api.gitcode.com 与 web-api.gitcode.com

  4. 路径格式变体:GitHub 风格(/repos/o/r/pulls/n)与 GitLab 风格(/projects/o%2Fr/merge_requests/n)

  5. 资源名称变体:pulls 与 merge_requests,单数与复数(/review 与 /reviews)

示例:pr discussions 功能是通过在 JS 包中找到 diff_review_record_list 发现的,然后探测 API 版本(v4 可行),接着切换到 GitLab 路径格式(/projects/{owner}%2F{repo}/merge_requests/{iid}/discussions)。

许可证

MIT 许可证 - 详情参见 LICENSE 文件。

项目介绍

用户可通过该项目便捷地与 GitCode API 交互,实现仓库、问题、拉取请求等管理操作。提供异步 API 客户端、类型安全模型、自动重试机制及直观的命令行界面,支持 token 认证与配置持久化。【此简介由AI生成】

定制我的领域