Merge PR #142: 新增 PR 本地检出命令

# Conflicts:
#	internal/i18n/locales/en-US.json
#	internal/i18n/locales/zh-CN.json
#	shortcuts/pr/pr_test.go
#	skills/README.md
This commit is contained in:
wbtiger 2026-07-14 22:50:07 +08:00
commit 62c44bf7b4
11 changed files with 471 additions and 375 deletions

View File

@ -432,6 +432,9 @@ gitlink-cli pr +create --owner Gitlink --repo forgeplus -t "feat: New feature" -
# View a PR
gitlink-cli pr +view --owner Gitlink --repo forgeplus -i 42
# Fetch and checkout a PR branch locally
gitlink-cli pr +checkout --owner Gitlink --repo forgeplus -i 42 --branch review/pr-42
# Merge a PR
gitlink-cli pr +merge --owner Gitlink --repo forgeplus -i 42

View File

@ -442,6 +442,9 @@ gitlink-cli pr +create --owner Gitlink --repo forgeplus -t "feat: 新功能" --h
# 查看 PR
gitlink-cli pr +view --owner Gitlink --repo forgeplus -i 42
# 拉取并切换到 PR 分支
gitlink-cli pr +checkout --owner Gitlink --repo forgeplus -i 42 --branch review/pr-42
# 合并 PR
gitlink-cli pr +merge --owner Gitlink --repo forgeplus -i 42

View File

@ -0,0 +1,7 @@
# PR 本地检出命令
新增 `gitlink-cli pr +checkout`,用于把指定 PR 的源分支拉取到本地并切换到 review 分支。维护者和贡献者在审查 PR 时,经常需要从“查看 PR 元信息”进入“本地构建、测试、复现”的流程,之前只能手动查看 fork 信息、拼接远端地址、fetch 分支,再 checkout新命令把这套流程收敛为一个 Shortcut。
命令会读取 GitLink PR 详情,自动识别同仓库 PR 与 fork PR 的源仓库和源分支,不依赖本地 remote 必须叫 `origin`。默认创建 `pr-<id>` 本地分支,也支持 `--branch` 指定分支名、`--force` 重置已存在分支、`--dry-run` 预览将执行的 git 命令。
本次变更补充了单元测试、README 示例、PR Skill 说明和独立参考文档,覆盖 fork PR 解析、同仓库 head 解析、dry-run、force checkout 以及非法分支名校验。

View File

@ -5,13 +5,10 @@
"cmd.auth.logout.short": "Logout from GitLink",
"cmd.auth.short": "Authentication commands",
"cmd.auth.status.short": "Show authentication status",
"cmd.branch.all.short": "List all branches with archive URLs",
"cmd.branch.create.short": "Create a branch",
"cmd.branch.delete.short": "Delete a branch",
"cmd.branch.list.short": "List branches",
"cmd.branch.protect.short": "Set branch protection",
"cmd.branch.restore.short": "Restore a deleted branch",
"cmd.branch.set_default.short": "Set the default branch",
"cmd.branch.short": "Branch operations",
"cmd.branch.unprotect.short": "Remove branch protection",
"cmd.ci.builds.short": "List CI builds",
@ -24,17 +21,6 @@
"cmd.config.list.short": "List all configuration values",
"cmd.config.set.short": "Set a configuration value",
"cmd.config.short": "Manage gitlink-cli configuration",
"cmd.dataset.create.long": "Create the dataset of a repository with a title, description, optional license and research paper content.",
"cmd.dataset.create.short": "Create a repository's dataset",
"cmd.dataset.delete_attachment.long": "Delete a dataset attachment by its UUID. This is destructive: preview with --dry-run, then pass --yes to confirm.",
"cmd.dataset.delete_attachment.short": "Delete a dataset attachment by UUID",
"cmd.dataset.list.long": "List datasets for one or more GitLink projects by their numeric project IDs.",
"cmd.dataset.list.short": "List datasets by project IDs",
"cmd.dataset.short": "Dataset operations",
"cmd.dataset.update.long": "Update the dataset of a repository (title, description, optional license and research paper content).",
"cmd.dataset.update.short": "Update a repository's dataset",
"cmd.dataset.view.long": "View a repository's dataset and its attachments. Use --page/--limit to paginate attachments.",
"cmd.dataset.view.short": "View a repository's dataset",
"cmd.doctor.long": "Run local diagnostics for gitlink-cli configuration, authentication, repository context and API connectivity.",
"cmd.doctor.short": "Diagnose gitlink-cli environment problems",
"cmd.issue.batch_close.long": "Close filtered issues in bulk.\n\nThis command defaults to dry-run mode and only prints matching issues.\nPass --yes to execute remote close operations. Use restrictive filters and a small limit.\n\nExamples:\n gitlink-cli issue +batch-close --owner Gitlink --repo gitlink-cli --older-than-days 60 --limit 20\n gitlink-cli issue +batch-close --owner Gitlink --repo gitlink-cli --older-than-days 60 --limit 20 --yes",
@ -45,10 +31,6 @@
"cmd.issue.batch_list.short": "List issue batch maintenance candidates without changing remote data",
"cmd.issue.close.short": "Close an issue",
"cmd.issue.comment.short": "Add a comment to an issue",
"cmd.issue.comment_delete.short": "Delete an issue comment",
"cmd.issue.comment_edit.short": "Edit an issue comment",
"cmd.issue.comment_replies.short": "List replies to an issue comment",
"cmd.issue.comments.short": "List issue comments",
"cmd.issue.create.short": "Create a new issue",
"cmd.issue.list.short": "List issues",
"cmd.issue.short": "Issue operations",
@ -59,13 +41,8 @@
"cmd.org.list.short": "List organizations",
"cmd.org.members.short": "List organization members",
"cmd.org.short": "Organization operations",
"cmd.pm.action_runs.short": "List PM action run records",
"cmd.pm.dashboards.short": "List PM dashboards",
"cmd.pm.issue_tags.short": "List PM issue tags",
"cmd.pm.pipelines.short": "List PM pipelines",
"cmd.pm.short": "Project management operations",
"cmd.pm.sprint_issues.short": "List PM sprint issues",
"cmd.pm.weekly_issues.short": "List PM weekly issues",
"cmd.pr.checkout.long": "Fetch a pull request head branch and check it out locally.\n\nThe command reads PR source repository information from GitLink, so it works for both same-repository PRs and fork PRs without requiring a local remote named origin.\n\nExamples:\n gitlink-cli pr +checkout -i 42\n gitlink-cli pr +checkout -i 42 --branch review/pr-42\n gitlink-cli pr +checkout -i 42 --force",
"cmd.pr.checkout.short": "Fetch and checkout a pull request locally",
"cmd.pr.close.short": "Close a pull request",
"cmd.pr.comment.short": "Add a comment to a pull request",
"cmd.pr.create.short": "Create a pull request",
@ -79,17 +56,6 @@
"cmd.pr.version_diff.short": "Show diff for a pull request patchset version",
"cmd.pr.versions.short": "List pull request patchset versions",
"cmd.pr.view.short": "View pull request details",
"cmd.profile.ability.long": "Show a user's development ability scores (influence, contribution, activity, experience, language) and language breakdown, sourced from the GitLink platform statistics API.",
"cmd.profile.ability.short": "Show a user's development ability scores",
"cmd.profile.activity.long": "Show a user's recent activity statistics: daily issues, pull requests, and commits over a time window.",
"cmd.profile.activity.short": "Show a user's recent activity statistics",
"cmd.profile.contribution.long": "Show a user's contribution heatmap (daily contribution counts) for a given year.",
"cmd.profile.contribution.short": "Show a user's contribution heatmap",
"cmd.profile.major.long": "Show a user's major/discipline categories (e.g. deep learning, quantum computing) inferred from their projects.",
"cmd.profile.major.short": "Show a user's major/discipline categories",
"cmd.profile.role.long": "Show a user's role positioning derived from the GitLink platform statistics API.",
"cmd.profile.role.short": "Show a user's role positioning",
"cmd.profile.short": "User profile and statistics operations",
"cmd.release.create.short": "Create a release",
"cmd.release.delete.short": "Delete a release",
"cmd.release.list.short": "List releases",
@ -97,25 +63,15 @@
"cmd.release.view.short": "View release details",
"cmd.repo.create.short": "Create a new repository",
"cmd.repo.delete.short": "Delete a repository",
"cmd.repo.file_exists.short": "Check whether a repository file exists",
"cmd.repo.fork.short": "Fork a repository",
"cmd.repo.info.short": "Show repository details",
"cmd.repo.list.short": "List repositories for a user or organization",
"cmd.repo.manifest.short": "Read a common dependency manifest",
"cmd.repo.raw.short": "Read raw repository file content",
"cmd.repo.short": "Repository operations",
"cmd.repo.tree.short": "List repository files and directories",
"cmd.root.long": "Manage repositories, issues, pull requests, releases, CI and workflows on GitLink.",
"cmd.root.short": "GitLink CLI - command-line tool for GitLink",
"cmd.search.repos.short": "Search repositories",
"cmd.search.short": "Search operations",
"cmd.search.users.short": "Search users",
"cmd.template.create.short": "Create a project template",
"cmd.template.delete.short": "Delete a project template",
"cmd.template.get.short": "Get a project template by id",
"cmd.template.list.short": "List project templates",
"cmd.template.short": "Project template operations",
"cmd.template.update.short": "Update a project template",
"cmd.user.info.short": "Show user profile",
"cmd.user.me.short": "Show current authenticated user",
"cmd.user.short": "User operations",
@ -132,12 +88,8 @@
"error.auth.login_failed": "login failed: {message}",
"error.auth.store_token_failed": "failed to store token: {message}",
"error.auth.token_empty": "token cannot be empty",
"error.branch.id_invalid": "invalid --branch-id: {value}; must be a positive integer",
"error.config.save_failed": "failed to save config: {message}",
"error.dataset.delete_confirm": "dataset attachment deletion is destructive; run --dry-run first, then pass --yes to confirm",
"error.missing_required_flag": "required flag --{name} is missing",
"error.profile.user_required": "could not determine target user; pass --user or run gitlink-cli auth login",
"error.template.invalid_type": "invalid --type {type}; expected one of {types}",
"error.unsupported_language": "unsupported language: {lang}",
"flag.api.batch_continue_on_error": "Continue running remaining batch requests after a failure",
"flag.api.batch_dry_run": "Preview batch requests without sending remote requests",
@ -150,25 +102,11 @@
"flag.api.query": "Query parameters (key=val&key2=val2)",
"flag.auth.token": "Login by pasting an existing token",
"flag.branch.from": "Source branch or commit",
"flag.branch.id": "Deleted branch ID",
"flag.branch.keyword": "Filter branches by keyword",
"flag.branch.name": "Branch name",
"flag.branch.state": "Branch visibility: visible (default), deleted, or all",
"flag.ci.build": "Build number",
"flag.ci.stage": "Stage number",
"flag.ci.step": "Step number",
"flag.comment.body": "Comment body",
"flag.dataset.description": "Dataset description",
"flag.dataset.dry_run": "Preview the request without writing the dataset",
"flag.dataset.dry_run_delete": "Preview the request without deleting the attachment",
"flag.dataset.ids": "Comma-separated project IDs to query datasets for",
"flag.dataset.license_id": "License ID",
"flag.dataset.limit": "Attachment page size",
"flag.dataset.page": "Attachment page number",
"flag.dataset.paper_content": "Research paper content",
"flag.dataset.title": "Dataset title",
"flag.dataset.uuid": "Attachment UUID",
"flag.dataset.yes": "Confirm the destructive deletion",
"flag.debug": "Enable debug output",
"flag.description": "Description",
"flag.doctor.skip_network": "Skip authenticated API connectivity checks",
@ -186,10 +124,6 @@
"flag.issue.batch_list.limit": "Maximum issues to return, capped at 100",
"flag.issue.batch_process.limit": "Maximum issues to process, capped at 100",
"flag.issue.body": "Issue description",
"flag.issue.comment_id": "Comment (journal) ID from issue +comments",
"flag.issue.comment_reply_to": "Parent comment (journal) ID to reply to",
"flag.issue.comments.category": "Filter: comment/operate (default: all)",
"flag.issue.comments.keyword": "Search keyword in comment content",
"flag.issue.label": "Label ID",
"flag.issue.label_filter": "Filter by existing label",
"flag.issue.milestone": "Milestone ID",
@ -210,10 +144,12 @@
"flag.org.name": "Organization name",
"flag.owner": "Repository owner (auto-detected from git remote)",
"flag.page": "Page number",
"flag.pm.project_id": "GitLink project ID",
"flag.pr.assignee_id": "Assignee user ID",
"flag.pr.base": "Target branch",
"flag.pr.body": "PR description",
"flag.pr.checkout_branch": "Local branch name to create (default: pr-<id>)",
"flag.pr.checkout_dry_run": "Print the git commands without modifying the local repository",
"flag.pr.checkout_force": "Reset the local branch if it already exists",
"flag.pr.file": "Filter diff by file path",
"flag.pr.head": "Source branch",
"flag.pr.id": "PR number",
@ -229,10 +165,6 @@
"flag.pr.tag_id": "Issue tag ID",
"flag.pr.title": "PR title",
"flag.pr.version_id": "Patchset version ID",
"flag.profile.end_time": "End time (Unix timestamp)",
"flag.profile.start_time": "Start time (Unix timestamp)",
"flag.profile.user": "Target user login (defaults to the authenticated user)",
"flag.profile.year": "Year for the contribution heatmap (e.g. 2025)",
"flag.release.body": "Release notes",
"flag.release.id": "Release ID",
"flag.release.id_or_tag": "Release ID or tag",
@ -243,14 +175,8 @@
"flag.repo": "Repository name (auto-detected from git remote)",
"flag.repo.category": "Filter: manage/mirror/sync/fork/all (default: manage)",
"flag.repo.description": "Repository description",
"flag.repo.file_exists.path": "File path to check",
"flag.repo.manifest.kind": "Manifest kind: go, node, python, rust, or java",
"flag.repo.name": "Repository name",
"flag.repo.private": "Make repository private (true/false)",
"flag.repo.raw.path": "File path in the repository",
"flag.repo.raw.ref": "Branch, tag, or commit ref",
"flag.repo.tree.path": "Directory path to list (default: repository root)",
"flag.repo.tree.ref": "Branch, tag, or commit ref",
"flag.search.keyword": "Search keyword",
"flag.sort_by": "Sort field",
"flag.sort_direction": "Sort direction: asc, desc",
@ -265,10 +191,6 @@
"flag.webhook.secret": "Webhook secret",
"flag.webhook.secret_update": "Webhook secret. Pass it again if the server does not return existing secrets.",
"flag.webhook.type": "Webhook type: gitea/slack/discord/dingtalk/telegram/msteams/feishu/matrix/jianmu/softbot",
"flag.template.content": "Template content",
"flag.template.id": "Template ID",
"flag.template.name": "Template name",
"flag.template.type": "Template type, for example: ProjectTemplates::Issue",
"flag.webhook.url": "Webhook target URL",
"output.auth.env_hint": " Or set {env} environment variable",
"output.auth.login_hint": " Run: gitlink-cli auth login",

View File

@ -5,13 +5,10 @@
"cmd.auth.logout.short": "退出 GitLink 登录",
"cmd.auth.short": "认证命令",
"cmd.auth.status.short": "显示认证状态",
"cmd.branch.all.short": "列出全部分支及归档链接",
"cmd.branch.create.short": "创建分支",
"cmd.branch.delete.short": "删除分支",
"cmd.branch.list.short": "列出分支",
"cmd.branch.protect.short": "设置分支保护",
"cmd.branch.restore.short": "恢复已删除分支",
"cmd.branch.set_default.short": "设置默认分支",
"cmd.branch.short": "分支操作",
"cmd.branch.unprotect.short": "移除分支保护",
"cmd.ci.builds.short": "列出 CI 构建",
@ -24,17 +21,6 @@
"cmd.config.list.short": "列出所有配置项",
"cmd.config.set.short": "设置配置项",
"cmd.config.short": "管理 gitlink-cli 配置",
"cmd.dataset.create.long": "为仓库创建数据集,包含标题、描述、可选许可证和研究论文内容。",
"cmd.dataset.create.short": "创建仓库数据集",
"cmd.dataset.delete_attachment.long": "按 UUID 删除数据集附件。该操作具有破坏性:先用 --dry-run 预览,再传 --yes 确认。",
"cmd.dataset.delete_attachment.short": "按 UUID 删除数据集附件",
"cmd.dataset.list.long": "按数字项目 ID 列出一个或多个 GitLink 项目的数据集。",
"cmd.dataset.list.short": "按项目 ID 列出数据集",
"cmd.dataset.short": "数据集操作",
"cmd.dataset.update.long": "更新仓库数据集(标题、描述、可选许可证和研究论文内容)。",
"cmd.dataset.update.short": "更新仓库数据集",
"cmd.dataset.view.long": "查看仓库的数据集及其附件。用 --page/--limit 对附件分页。",
"cmd.dataset.view.short": "查看仓库数据集",
"cmd.doctor.long": "诊断 gitlink-cli 的配置、认证、仓库上下文和 API 连通性问题。",
"cmd.doctor.short": "诊断 gitlink-cli 环境问题",
"cmd.issue.batch_close.long": "批量关闭筛选后的议题。\n\n该命令默认处于 dry-run 模式,只打印匹配的议题。\n传入 --yes 后执行远端关闭操作。请使用严格筛选条件和较小 limit。\n\n示例\n gitlink-cli issue +batch-close --owner Gitlink --repo gitlink-cli --older-than-days 60 --limit 20\n gitlink-cli issue +batch-close --owner Gitlink --repo gitlink-cli --older-than-days 60 --limit 20 --yes",
@ -45,10 +31,6 @@
"cmd.issue.batch_list.short": "列出议题批量维护候选项,不修改远端数据",
"cmd.issue.close.short": "关闭议题",
"cmd.issue.comment.short": "给议题添加评论",
"cmd.issue.comment_delete.short": "删除议题评论",
"cmd.issue.comment_edit.short": "编辑议题评论",
"cmd.issue.comment_replies.short": "列出议题评论的回复",
"cmd.issue.comments.short": "列出议题评论",
"cmd.issue.create.short": "创建新议题",
"cmd.issue.list.short": "列出议题",
"cmd.issue.short": "议题操作",
@ -59,13 +41,8 @@
"cmd.org.list.short": "列出组织",
"cmd.org.members.short": "列出组织成员",
"cmd.org.short": "组织操作",
"cmd.pm.action_runs.short": "列出 PM Action 运行记录",
"cmd.pm.dashboards.short": "列出 PM 看板",
"cmd.pm.issue_tags.short": "列出 PM Issue 标签",
"cmd.pm.pipelines.short": "列出 PM 流水线",
"cmd.pm.short": "项目管理操作",
"cmd.pm.sprint_issues.short": "列出 PM Sprint Issue",
"cmd.pm.weekly_issues.short": "列出 PM 周报 Issue",
"cmd.pr.checkout.long": "拉取拉取请求的源分支并切换到本地分支。\n\n命令会从 GitLink 读取 PR 的源仓库信息,因此同仓库 PR 和 fork PR 都可以使用,不依赖本地 remote 必须命名为 origin。\n\n示例\n gitlink-cli pr +checkout -i 42\n gitlink-cli pr +checkout -i 42 --branch review/pr-42\n gitlink-cli pr +checkout -i 42 --force",
"cmd.pr.checkout.short": "拉取并切换到拉取请求分支",
"cmd.pr.close.short": "关闭拉取请求",
"cmd.pr.comment.short": "给拉取请求添加评论",
"cmd.pr.create.short": "创建拉取请求",
@ -79,17 +56,6 @@
"cmd.pr.version_diff.short": "显示拉取请求补丁集版本 diff",
"cmd.pr.versions.short": "列出拉取请求补丁集版本",
"cmd.pr.view.short": "查看拉取请求详情",
"cmd.profile.ability.long": "显示用户的开发能力评分(影响力、贡献度、活跃度、项目经验、语言能力)及语言分布,数据来自 GitLink 平台统计接口。",
"cmd.profile.ability.short": "显示用户开发能力评分",
"cmd.profile.activity.long": "显示用户近期活动统计:时间范围内每日的疑修、合并请求和提交数量。",
"cmd.profile.activity.short": "显示用户近期活动统计",
"cmd.profile.contribution.long": "显示用户在指定年份的贡献热力图(每日贡献次数)。",
"cmd.profile.contribution.short": "显示用户贡献热力图",
"cmd.profile.major.long": "显示根据用户项目推断的专业/学科领域(如深度学习、量子计算)。",
"cmd.profile.major.short": "显示用户专业/学科定位",
"cmd.profile.role.long": "显示来自 GitLink 平台统计接口的用户角色定位。",
"cmd.profile.role.short": "显示用户角色定位",
"cmd.profile.short": "用户画像与统计操作",
"cmd.release.create.short": "创建发布",
"cmd.release.delete.short": "删除发布",
"cmd.release.list.short": "列出发布",
@ -97,25 +63,15 @@
"cmd.release.view.short": "查看发布详情",
"cmd.repo.create.short": "创建新仓库",
"cmd.repo.delete.short": "删除仓库",
"cmd.repo.file_exists.short": "检查仓库文件是否存在",
"cmd.repo.fork.short": "Fork 仓库",
"cmd.repo.info.short": "显示仓库详情",
"cmd.repo.list.short": "列出用户或组织的仓库",
"cmd.repo.manifest.short": "读取常见依赖清单文件",
"cmd.repo.raw.short": "读取仓库原始文件内容",
"cmd.repo.short": "仓库操作",
"cmd.repo.tree.short": "列出仓库文件和目录",
"cmd.root.long": "用于管理 GitLink 上的仓库、议题、拉取请求、发布、CI 和工作流。",
"cmd.root.short": "GitLink CLI - GitLink 命令行工具",
"cmd.search.repos.short": "搜索仓库",
"cmd.search.short": "搜索操作",
"cmd.search.users.short": "搜索用户",
"cmd.template.create.short": "创建项目模板",
"cmd.template.delete.short": "删除项目模板",
"cmd.template.get.short": "根据 ID 获取项目模板",
"cmd.template.list.short": "列出项目模板",
"cmd.template.short": "项目模板操作",
"cmd.template.update.short": "更新项目模板",
"cmd.user.info.short": "显示用户资料",
"cmd.user.me.short": "显示当前认证用户",
"cmd.user.short": "用户操作",
@ -132,12 +88,8 @@
"error.auth.login_failed": "登录失败:{message}",
"error.auth.store_token_failed": "保存 Token 失败:{message}",
"error.auth.token_empty": "Token 不能为空",
"error.branch.id_invalid": "无效的 --branch-id{value};必须是正整数",
"error.config.save_failed": "保存配置失败:{message}",
"error.dataset.delete_confirm": "删除数据集附件具有破坏性;请先 --dry-run 预览,再传 --yes 确认",
"error.missing_required_flag": "缺少必需参数 --{name}",
"error.profile.user_required": "无法确定目标用户;请通过 --user 指定,或先运行 gitlink-cli auth login 登录",
"error.template.invalid_type": "无效的 --type {type};应为以下之一:{types}",
"error.unsupported_language": "不支持的语言:{lang}",
"flag.api.batch_continue_on_error": "批处理请求失败后继续执行后续请求",
"flag.api.batch_dry_run": "预览批处理请求,不发送远端请求",
@ -150,25 +102,11 @@
"flag.api.query": "查询参数key=val&key2=val2",
"flag.auth.token": "通过粘贴已有 Token 登录",
"flag.branch.from": "源分支或 Commit",
"flag.branch.id": "已删除分支 ID",
"flag.branch.keyword": "按关键字筛选分支",
"flag.branch.name": "分支名称",
"flag.branch.state": "分支范围:默认可见分支,也可选 deleted 或 all",
"flag.ci.build": "构建编号",
"flag.ci.stage": "阶段编号",
"flag.ci.step": "步骤编号",
"flag.comment.body": "评论内容",
"flag.dataset.description": "数据集描述",
"flag.dataset.dry_run": "预览请求,不写入数据集",
"flag.dataset.dry_run_delete": "预览请求,不删除附件",
"flag.dataset.ids": "用于查询数据集的项目 ID逗号分隔",
"flag.dataset.license_id": "许可证 ID",
"flag.dataset.limit": "附件每页数量",
"flag.dataset.page": "附件页码",
"flag.dataset.paper_content": "研究论文内容",
"flag.dataset.title": "数据集标题",
"flag.dataset.uuid": "附件 UUID",
"flag.dataset.yes": "确认执行破坏性删除",
"flag.debug": "启用调试输出",
"flag.description": "描述",
"flag.doctor.skip_network": "跳过需要访问 GitLink 的认证连通性检查",
@ -186,10 +124,6 @@
"flag.issue.batch_list.limit": "最多返回的议题数,上限 100",
"flag.issue.batch_process.limit": "最多处理的议题数,上限 100",
"flag.issue.body": "议题描述",
"flag.issue.comment_id": "评论journalID可由 issue +comments 获取",
"flag.issue.comment_reply_to": "要回复的父评论journalID",
"flag.issue.comments.category": "筛选comment/operate默认全部",
"flag.issue.comments.keyword": "评论内容搜索关键词",
"flag.issue.label": "标签 ID",
"flag.issue.label_filter": "按已有标签筛选",
"flag.issue.milestone": "里程碑 ID",
@ -210,10 +144,12 @@
"flag.org.name": "组织名称",
"flag.owner": "仓库所有者(自动从 git remote 检测)",
"flag.page": "页码",
"flag.pm.project_id": "GitLink 项目 ID",
"flag.pr.assignee_id": "指派人用户 ID",
"flag.pr.base": "目标分支",
"flag.pr.body": "PR 描述",
"flag.pr.checkout_branch": "要创建的本地分支名默认pr-<id>",
"flag.pr.checkout_dry_run": "只打印 git 命令,不修改本地仓库",
"flag.pr.checkout_force": "本地分支已存在时重置该分支",
"flag.pr.file": "按文件路径筛选 diff",
"flag.pr.head": "源分支",
"flag.pr.id": "PR 编号",
@ -229,10 +165,6 @@
"flag.pr.tag_id": "议题标签 ID",
"flag.pr.title": "PR 标题",
"flag.pr.version_id": "补丁集版本 ID",
"flag.profile.end_time": "结束时间Unix 时间戳)",
"flag.profile.start_time": "开始时间Unix 时间戳)",
"flag.profile.user": "目标用户登录名(默认为当前认证用户)",
"flag.profile.year": "贡献热力图的年份(如 2025",
"flag.release.body": "发布说明",
"flag.release.id": "发布 ID",
"flag.release.id_or_tag": "发布 ID 或标签",
@ -243,14 +175,8 @@
"flag.repo": "仓库名称(自动从 git remote 检测)",
"flag.repo.category": "筛选manage/mirror/sync/fork/all默认manage",
"flag.repo.description": "仓库描述",
"flag.repo.file_exists.path": "要检查的文件路径",
"flag.repo.manifest.kind": "清单类型go、node、python、rust 或 java",
"flag.repo.name": "仓库名称",
"flag.repo.private": "设为私有仓库true/false",
"flag.repo.raw.path": "仓库中的文件路径",
"flag.repo.raw.ref": "分支、标签或提交引用",
"flag.repo.tree.path": "要列出的目录路径(默认:仓库根目录)",
"flag.repo.tree.ref": "分支、标签或提交引用",
"flag.search.keyword": "搜索关键词",
"flag.sort_by": "排序字段",
"flag.sort_direction": "排序方向asc、desc",
@ -265,10 +191,6 @@
"flag.webhook.secret": "Webhook 密钥",
"flag.webhook.secret_update": "Webhook 密钥。如果服务端不返回已有密钥,请再次传入。",
"flag.webhook.type": "Webhook 类型gitea/slack/discord/dingtalk/telegram/msteams/feishu/matrix/jianmu/softbot",
"flag.template.content": "模板内容",
"flag.template.id": "模板 ID",
"flag.template.name": "模板名称",
"flag.template.type": "模板类型例如ProjectTemplates::Issue",
"flag.webhook.url": "Webhook 目标 URL",
"output.auth.env_hint": " 或设置 {env} 环境变量",
"output.auth.login_hint": " 运行gitlink-cli auth login",

205
shortcuts/pr/checkout.go Normal file
View File

@ -0,0 +1,205 @@
package pr
import (
"fmt"
"net/url"
"os/exec"
"strings"
"github.com/gitlink-org/gitlink-cli/internal/output"
"github.com/gitlink-org/gitlink-cli/shortcuts/common"
)
type prCheckoutDetail struct {
ID string `json:"id"`
SourceOwner string `json:"source_owner"`
SourceRepo string `json:"source_repo"`
SourceBranch string `json:"source_branch"`
SourceURL string `json:"source_url"`
}
type prCheckoutPlan struct {
PullRequest string `json:"pull_request"`
SourceOwner string `json:"source_owner"`
SourceRepo string `json:"source_repo"`
SourceBranch string `json:"source_branch"`
SourceURL string `json:"source_url"`
LocalBranch string `json:"local_branch"`
Force bool `json:"force"`
DryRun bool `json:"dry_run"`
Commands [][]string `json:"commands"`
}
type gitCommandRunner func(args ...string) (string, error)
var runGitCommand gitCommandRunner = defaultRunGitCommand
func runCheckout(ctx *common.RuntimeContext) error {
if err := ctx.ResolveOwnerRepo(); err != nil {
return err
}
id, err := ctx.RequireArg("id")
if err != nil {
return err
}
env, err := ctx.CallAPI("GET", prV1Path(ctx, id), nil)
if err != nil {
return err
}
detail, err := extractPRCheckoutDetail(ctx, env, id)
if err != nil {
return err
}
localBranch := strings.TrimSpace(ctx.Arg("branch"))
if localBranch == "" {
localBranch = "pr-" + id
}
force := ctx.Arg("force") == "true"
dryRun := ctx.Arg("dry-run") == "true"
plan, err := buildPRCheckoutPlan(detail, localBranch, force, dryRun)
if err != nil {
return err
}
if dryRun {
return ctx.OutputData(plan)
}
for _, args := range plan.Commands {
if out, err := runGitCommand(args...); err != nil {
if strings.TrimSpace(out) != "" {
return fmt.Errorf("git %s failed: %w: %s", strings.Join(args, " "), err, strings.TrimSpace(out))
}
return fmt.Errorf("git %s failed: %w", strings.Join(args, " "), err)
}
}
return ctx.OutputData(plan)
}
func buildPRCheckoutPlan(detail prCheckoutDetail, localBranch string, force, dryRun bool) (prCheckoutPlan, error) {
if err := validateGitRef(localBranch, "local branch"); err != nil {
return prCheckoutPlan{}, err
}
if err := validateGitRef(detail.SourceBranch, "source branch"); err != nil {
return prCheckoutPlan{}, err
}
if detail.SourceURL == "" {
return prCheckoutPlan{}, fmt.Errorf("PR response missing source repository URL")
}
checkoutFlag := "-b"
if force {
checkoutFlag = "-B"
}
return prCheckoutPlan{
PullRequest: detail.ID,
SourceOwner: detail.SourceOwner,
SourceRepo: detail.SourceRepo,
SourceBranch: detail.SourceBranch,
SourceURL: detail.SourceURL,
LocalBranch: localBranch,
Force: force,
DryRun: dryRun,
Commands: [][]string{
{"fetch", "--no-tags", detail.SourceURL, detail.SourceBranch},
{"checkout", checkoutFlag, localBranch, "FETCH_HEAD"},
},
}, nil
}
func extractPRCheckoutDetail(ctx *common.RuntimeContext, env *output.Envelope, id string) (prCheckoutDetail, error) {
data, ok := env.Data.(map[string]interface{})
if !ok {
return prCheckoutDetail{}, fmt.Errorf("unexpected PR response format")
}
head := firstStringField(data, "head", "pull_request_head", "source_branch")
if head == "" {
if pr, ok := data["pull_request"].(map[string]interface{}); ok {
head = firstStringField(pr, "head", "pull_request_head", "source_branch")
}
}
sourceOwner, sourceRepo, sourceBranch := parsePRHeadForCheckout(head)
fork, _ := data["fork_project"].(map[string]interface{})
if login := stringField(fork, "login"); login != "" {
sourceOwner = login
}
if identifier := stringField(fork, "identifier"); identifier != "" {
sourceRepo = identifier
}
if sourceOwner == "" {
sourceOwner = ctx.Owner
}
if sourceRepo == "" {
sourceRepo = ctx.Repo
}
if sourceBranch == "" {
return prCheckoutDetail{}, fmt.Errorf("PR response missing head branch")
}
return prCheckoutDetail{
ID: id,
SourceOwner: sourceOwner,
SourceRepo: sourceRepo,
SourceBranch: sourceBranch,
SourceURL: gitlinkRepoURL(ctx.Client.BaseURL, sourceOwner, sourceRepo),
}, nil
}
func parsePRHeadForCheckout(head string) (owner, repo, branch string) {
head = strings.TrimSpace(head)
if head == "" {
return "", "", ""
}
left, right, hasOwner := strings.Cut(head, ":")
if !hasOwner {
return "", "", head
}
branch = strings.TrimSpace(right)
parts := strings.Split(strings.Trim(left, "/"), "/")
if len(parts) >= 2 {
return parts[0], parts[1], branch
}
if len(parts) == 1 {
return parts[0], "", branch
}
return "", "", branch
}
func gitlinkRepoURL(apiBaseURL, owner, repo string) string {
base := strings.TrimRight(apiBaseURL, "/")
for _, suffix := range []string{"/api/v1", "/api"} {
if strings.HasSuffix(base, suffix) {
base = strings.TrimSuffix(base, suffix)
break
}
}
if base == "" {
base = "https://www.gitlink.org.cn"
}
return fmt.Sprintf("%s/%s/%s.git", base, url.PathEscape(owner), url.PathEscape(repo))
}
func validateGitRef(ref, label string) error {
ref = strings.TrimSpace(ref)
if ref == "" {
return fmt.Errorf("%s cannot be empty", label)
}
if strings.HasPrefix(ref, "-") {
return fmt.Errorf("%s cannot start with '-'", label)
}
if strings.ContainsAny(ref, "\x00\r\n") {
return fmt.Errorf("%s contains unsupported control characters", label)
}
return nil
}
func firstStringField(m map[string]interface{}, keys ...string) string {
for _, key := range keys {
if v := stringField(m, key); v != "" {
return v
}
}
return ""
}
func defaultRunGitCommand(args ...string) (string, error) {
cmd := exec.Command("git", args...)
out, err := cmd.CombinedOutput()
return string(out), err
}

View File

@ -170,6 +170,18 @@ func Shortcuts(translators ...*i18n.Translator) []*common.Shortcut {
return ctx.Output(env)
},
},
{
Name: "checkout",
Description: tr.T("cmd.pr.checkout.short"),
Long: tr.T("cmd.pr.checkout.long"),
Flags: []common.Flag{
{Name: "id", Short: "i", Usage: tr.T("flag.pr.id"), Required: true},
{Name: "branch", Short: "b", Usage: tr.T("flag.pr.checkout_branch")},
{Name: "force", Short: "f", Usage: tr.T("flag.pr.checkout_force"), Bool: true, Default: "false"},
{Name: "dry-run", Usage: tr.T("flag.pr.checkout_dry_run"), Bool: true, Default: "false"},
},
Run: runCheckout,
},
{
Name: "merge",
Description: tr.T("cmd.pr.merge.short"),

View File

@ -249,6 +249,143 @@ func TestPRView(t *testing.T) {
}
}
// --- checkout ---
func TestPRCheckoutDryRunUsesForkSource(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != "GET" {
t.Fatalf("expected GET, got %s", r.Method)
}
if r.URL.Path != "/v1/owner/repo/pulls/42.json" {
t.Fatalf("unexpected path: %s", r.URL.Path)
}
writeJSON(t, w, map[string]interface{}{
"head": "feature/gitlink-pr",
"fork_project": map[string]interface{}{
"login": "contributor",
"identifier": "repo-fork",
},
})
}))
defer server.Close()
err := runPRShortcut(t, server, "checkout", map[string]string{
"id": "42",
"branch": "review/pr-42",
"dry-run": "true",
})
if err != nil {
t.Fatalf("checkout dry-run failed: %v", err)
}
}
func TestPRCheckoutRunsFetchAndCheckout(t *testing.T) {
var calls [][]string
oldRunner := runGitCommand
runGitCommand = func(args ...string) (string, error) {
calls = append(calls, append([]string(nil), args...))
return "", nil
}
defer func() { runGitCommand = oldRunner }()
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/v1/owner/repo/pulls/43.json" {
t.Fatalf("unexpected path: %s", r.URL.Path)
}
writeJSON(t, w, map[string]interface{}{
"head": "contributor/repo:feature/pr-checkout",
})
}))
defer server.Close()
err := runPRShortcut(t, server, "checkout", map[string]string{
"id": "43",
"branch": "review/pr-43",
})
if err != nil {
t.Fatalf("checkout failed: %v", err)
}
if len(calls) != 2 {
t.Fatalf("expected 2 git calls, got %d: %#v", len(calls), calls)
}
assertStringSlice(t, calls[0], []string{"fetch", "--no-tags", server.URL + "/contributor/repo.git", "feature/pr-checkout"})
assertStringSlice(t, calls[1], []string{"checkout", "-b", "review/pr-43", "FETCH_HEAD"})
}
func TestPRCheckoutForceUsesResetBranch(t *testing.T) {
plan, err := buildPRCheckoutPlan(prCheckoutDetail{
ID: "44",
SourceOwner: "owner",
SourceRepo: "repo",
SourceBranch: "feature/x",
SourceURL: "https://www.gitlink.org.cn/owner/repo.git",
}, "review/pr-44", true, false)
if err != nil {
t.Fatalf("build plan failed: %v", err)
}
assertStringSlice(t, plan.Commands[1], []string{"checkout", "-B", "review/pr-44", "FETCH_HEAD"})
}
func TestPRCheckoutDefaultBranch(t *testing.T) {
plan, err := buildPRCheckoutPlan(prCheckoutDetail{
ID: "45",
SourceOwner: "owner",
SourceRepo: "repo",
SourceBranch: "feature/default",
SourceURL: "https://www.gitlink.org.cn/owner/repo.git",
}, "pr-45", false, false)
if err != nil {
t.Fatalf("build plan failed: %v", err)
}
if plan.LocalBranch != "pr-45" {
t.Fatalf("local branch = %q, want pr-45", plan.LocalBranch)
}
}
func TestPRCheckoutRejectsUnsafeBranch(t *testing.T) {
_, err := buildPRCheckoutPlan(prCheckoutDetail{
ID: "46",
SourceOwner: "owner",
SourceRepo: "repo",
SourceBranch: "feature/x",
SourceURL: "https://www.gitlink.org.cn/owner/repo.git",
}, "-bad", false, false)
if err == nil {
t.Fatal("expected unsafe local branch error")
}
}
func TestParsePRHeadForCheckout(t *testing.T) {
cases := []struct {
head string
wantOwner string
wantRepo string
wantBranch string
}{
{head: "feature/x", wantBranch: "feature/x"},
{head: "alice:feature/x", wantOwner: "alice", wantBranch: "feature/x"},
{head: "alice/repo:feature/x", wantOwner: "alice", wantRepo: "repo", wantBranch: "feature/x"},
}
for _, tc := range cases {
owner, repo, branch := parsePRHeadForCheckout(tc.head)
if owner != tc.wantOwner || repo != tc.wantRepo || branch != tc.wantBranch {
t.Fatalf("parse %q = (%q, %q, %q), want (%q, %q, %q)",
tc.head, owner, repo, branch, tc.wantOwner, tc.wantRepo, tc.wantBranch)
}
}
}
func TestGitlinkRepoURLUsesConfiguredAPIBase(t *testing.T) {
got := gitlinkRepoURL("https://gitlink.example.com/api", "owner", "repo")
if got != "https://gitlink.example.com/owner/repo.git" {
t.Fatalf("url = %q", got)
}
got = gitlinkRepoURL("https://gitlink.example.com/api/v1", "owner", "repo")
if got != "https://gitlink.example.com/owner/repo.git" {
t.Fatalf("url with api/v1 = %q", got)
}
}
// --- merge ---
func TestPRMerge(t *testing.T) {
@ -532,177 +669,14 @@ func assertEqual(t *testing.T, got interface{}, want interface{}) {
}
}
func TestPRReviewCommentsListBuildsQuery(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != "GET" || r.URL.Path != "/v1/owner/repo/pulls/13/journals.json" {
t.Fatalf("unexpected request: %s %s", r.Method, r.URL.Path)
func assertStringSlice(t *testing.T, got, want []string) {
t.Helper()
if len(got) != len(want) {
t.Fatalf("got %v, want %v", got, want)
}
for i := range got {
if got[i] != want[i] {
t.Fatalf("got %v, want %v", got, want)
}
query := r.URL.Query()
assertEqual(t, query.Get("keyword"), "todo")
assertEqual(t, query.Get("review_id"), "10")
assertEqual(t, query.Get("need_respond"), "true")
assertEqual(t, query.Get("state"), "opened")
assertEqual(t, query.Get("parent_id"), "200")
assertEqual(t, query.Get("path"), "README.md")
assertEqual(t, query.Get("is_full"), "true")
assertEqual(t, query.Get("sort_by"), "created_on")
assertEqual(t, query.Get("sort_direction"), "desc")
writeJSON(t, w, map[string]interface{}{"total_count": 0, "journals": []interface{}{}})
}))
defer server.Close()
err := runPRShortcut(t, server, "review-comments", map[string]string{
"id": "13",
"keyword": "todo",
"review-id": "10",
"need-respond": "true",
"state": "opened",
"parent-id": "200",
"path": "README.md",
"is-full": "true",
"sort-by": "created_on",
"sort-direction": "desc",
})
if err != nil {
t.Fatalf("review-comments shortcut failed: %v", err)
}
}
func TestPRReviewCommentPostsPayload(t *testing.T) {
var payload map[string]interface{}
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != "POST" || r.URL.Path != "/v1/owner/repo/pulls/13/journals.json" {
t.Fatalf("unexpected request: %s %s", r.Method, r.URL.Path)
}
payload = decodeJSON(t, r)
writeJSON(t, w, map[string]interface{}{"id": 200, "note": "please fix"})
}))
defer server.Close()
err := runPRShortcut(t, server, "review-comment", map[string]string{
"id": "13",
"note": "please fix",
"review-id": "10",
"type": "problem",
"commit": "abc123",
"line-code": "abc123_0_10",
"path": "README.md",
"parent-id": "199",
"diff-json": `{"name":"README.md","addition":1}`,
})
if err != nil {
t.Fatalf("review-comment shortcut failed: %v", err)
}
assertEqual(t, payload["type"], "problem")
assertEqual(t, payload["note"], "please fix")
assertEqual(t, payload["review_id"], "10")
assertEqual(t, payload["commit_id"], "abc123")
assertEqual(t, payload["line_code"], "abc123_0_10")
assertEqual(t, payload["path"], "README.md")
assertEqual(t, payload["parent_id"], float64(199))
diff, ok := payload["diff"].(map[string]interface{})
if !ok {
t.Fatalf("diff type = %T, want map", payload["diff"])
}
assertEqual(t, diff["name"], "README.md")
assertEqual(t, diff["addition"], float64(1))
}
func TestPRReviewCommentDryRunDoesNotCallAPI(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
t.Fatalf("server should not be called during dry-run: %s %s", r.Method, r.URL.Path)
}))
defer server.Close()
err := runPRShortcut(t, server, "review-comment", map[string]string{
"id": "13",
"note": "please fix",
"review-id": "10",
"commit": "abc123",
"line-code": "abc123_0_10",
"path": "README.md",
"dry-run": "true",
})
if err != nil {
t.Fatalf("review-comment dry-run failed: %v", err)
}
}
func TestPRReviewCommentUpdatePayload(t *testing.T) {
var payload map[string]interface{}
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != "PUT" || r.URL.Path != "/v1/owner/repo/pulls/13/journals/200.json" {
t.Fatalf("unexpected request: %s %s", r.Method, r.URL.Path)
}
payload = decodeJSON(t, r)
writeJSON(t, w, map[string]interface{}{"id": 200, "note": "fixed", "state": "resolved"})
}))
defer server.Close()
err := runPRShortcut(t, server, "review-comment-update", map[string]string{
"id": "13",
"comment-id": "200",
"note": "fixed",
"commit": "def456",
"state": "resolved",
})
if err != nil {
t.Fatalf("review-comment-update shortcut failed: %v", err)
}
assertEqual(t, payload["note"], "fixed")
assertEqual(t, payload["commit_id"], "def456")
assertEqual(t, payload["state"], "resolved")
}
func TestPRReviewCommentDeleteUsesV1Endpoint(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != "DELETE" || r.URL.Path != "/v1/owner/repo/pulls/13/journals/200.json" {
t.Fatalf("unexpected request: %s %s", r.Method, r.URL.Path)
}
writeJSON(t, w, map[string]interface{}{"status": 0, "message": "success"})
}))
defer server.Close()
err := runPRShortcut(t, server, "review-comment-delete", map[string]string{
"id": "13",
"comment-id": "200",
})
if err != nil {
t.Fatalf("review-comment-delete shortcut failed: %v", err)
}
}
func TestPRReviewCommentRejectsInvalidInputs(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
t.Fatalf("server should not be called for invalid input: %s %s", r.Method, r.URL.Path)
}))
defer server.Close()
if err := runPRShortcut(t, server, "review-comment", map[string]string{
"id": "13",
"note": "please fix",
"review-id": "10",
"type": "todo",
"commit": "abc123",
"line-code": "abc123_0_10",
"path": "README.md",
}); err == nil {
t.Fatal("expected invalid review comment type to fail")
}
if err := runPRShortcut(t, server, "review-comment-update", map[string]string{
"id": "13",
"comment-id": "200",
"state": "done",
}); err == nil {
t.Fatal("expected invalid review comment state to fail")
}
if err := runPRShortcut(t, server, "review-comments", map[string]string{
"id": "13",
"need-respond": "maybe",
}); err == nil {
t.Fatal("expected invalid need-respond to fail")
}
}

View File

@ -32,7 +32,7 @@ gitlink-cli auth status
gitlink-cli user +me
```
详见: [gitlink-shared/SKILL.md](gitlink-shared/SKILL.md)
详见: [gitlink-shared/examples/auth-workflow.md](gitlink-shared/examples/auth-workflow.md)
### 2. 查看可用命令
@ -64,19 +64,25 @@ skills/
├── README.md # 本文件
├── gitlink-shared/ # 共享基础规则
│ ├── SKILL.md # 认证、全局参数、安全规则、分支约定
└── references/
├── api-reference.md # API 详细参考、错误处理
├── raw-api-batch.md # 批量 Raw API 调用参考
│ └── troubleshooting.md # 常见问题排查
├── REFERENCE.md # API 详细参考、错误处理
├── TROUBLESHOOTING.md # 常见问题排查
└── examples/
│ └── auth-workflow.md # 认证工作流示例
├── gitlink-repo/ # 仓库管理
│ ├── SKILL.md # 仓库操作指南
│ └── references/ # 仓库命令参考文档
│ ├── REFERENCE.md # 仓库 API 参考
│ └── examples/
│ └── repo-workflow.md # 仓库管理工作流
├── gitlink-issue/ # Issue 管理
│ ├── SKILL.md # Issue 操作指南
│ └── references/ # Issue 命令参考文档
│ ├── REFERENCE.md # Issue API 参考
│ └── examples/
│ └── issue-workflow.md # Issue 全流程工作流
├── gitlink-pr/ # Pull Request
│ ├── SKILL.md # PR 操作指南
│ └── references/ # PR 命令参考文档
│ ├── REFERENCE.md # PR API 参考
│ └── examples/
│ └── pr-workflow.md # PR 工作流
├── gitlink-member/ # 仓库成员管理
│ └── SKILL.md # 成员与邀请链接操作指南
├── gitlink-branch/ # 分支管理
@ -85,24 +91,25 @@ skills/
│ └── branch-workflow.md # 分支工作流
├── gitlink-release/ # 版本发布
│ ├── SKILL.md # Release 操作指南
└── references/ # Release 命令参考文档
├── gitlink-release-auto/ # 自动化 Release 管理
└── SKILL.md # 自动发版、版本号推荐、Release Notes 生成
├── REFERENCE.md # Release API 参考
│ └── examples/
└── release-workflow.md # Release 工作流
├── gitlink-search/ # 搜索功能
│ ├── SKILL.md # 搜索操作指南
│ └── references/ # 搜索命令参考文档
│ └── examples/
│ └── search-workflow.md # 搜索工作流
├── gitlink-user/ # 用户管理
│ ├── SKILL.md # 用户操作指南
│ └── references/ # 用户命令参考文档
│ └── SKILL.md # 用户操作指南
├── gitlink-org/ # 组织管理
│ ├── SKILL.md # 组织操作指南
│ └── references/ # 组织命令参考文档
│ └── examples/
│ └── org-workflow.md # 组织工作流
├── gitlink-ci/ # CI/CD
│ └── SKILL.md # CI 操作指南
│ ├── SKILL.md # CI 操作指南
│ └── examples/
│ └── ci-workflow.md # CI 工作流
├── gitlink-pipeline/ # 流水线工作流
│ └── SKILL.md # Pipeline 操作指南
├── gitlink-wiki/ # Wiki 页面管理
│ └── SKILL.md # Wiki 操作指南
├── gitlink-pm/ # 项目管理
│ └── SKILL.md # PM 操作指南
├── gitlink-health/ # 项目健康度分析
@ -128,11 +135,11 @@ skills/
|-------|------|----------|
| **gitlink-shared** | 认证、全局参数、API 参考、安全规则、分支约定 | `auth login`, `auth status` |
| **gitlink-repo** | 仓库管理与洞察 | `repo +list`, `repo +info`, `repo +languages`, `repo +contributors`, `repo +code-stats`, `repo +follow`, `repo +like` |
| **gitlink-issue** | Issue 管理 | `issue +create`, `issue +list`, `issue +view`, `issue +close`, `issue +batch-close`, `issue +batch-update`, `issue +batch-delete` |
| **gitlink-pr** | Pull Request | `pr +list`, `pr +create`, `pr +view`, `pr +merge`, `pr +versions`, `pr +version-diff`, `pr +reviews`, `pr +review` |
| **gitlink-issue** | Issue 管理 | `issue +create`, `issue +list`, `issue +view`, `issue +close`, `issue +batch-close` |
| **gitlink-pr** | Pull Request | `pr +list`, `pr +create`, `pr +view`, `pr +checkout`, `pr +merge`, `pr +versions`, `pr +version-diff`, `pr +reviews`, `pr +review` |
| **gitlink-member** | 仓库成员管理 | `member +list`, `member +add`, `member +batch-add`, `member +role`, `member +invite-link` |
| **gitlink-branch** | 分支管理 | `branch +list`, `branch +create`, `branch +delete`, `branch +protect` |
| **gitlink-release** | 版本发布 | `release +list`, `release +create`, `release +edit`, `release +update`, `release +view` |
| **gitlink-release** | 版本发布 | `release +list`, `release +create`, `release +view` |
### 辅助 Skills
@ -143,8 +150,7 @@ skills/
| **gitlink-org** | 组织管理 | `org +list`, `org +info`, `org +members` |
| **gitlink-ci** | CI/CD | `ci +builds`, `ci +logs` |
| **gitlink-pipeline** | 流水线工作流 | `pipeline +runs`, `pipeline +run`, `pipeline +logs` |
| **gitlink-wiki** | Wiki 页面管理 | `wiki +list`, `wiki +view`, `wiki +create`, `wiki +update`, `wiki +delete` |
| **gitlink-pm** | 项目管理 | `pm +dashboards`, `pm +sprint-issues`, `pm +weekly-issues`, `pm +action-runs` |
| **gitlink-pm** | 项目管理 | 通过 Raw API 访问 |
| **gitlink-workflow** | AI 工作流 | Issue 分类、PR Review、Release Notes |
| **gitlink-health** | 开源项目健康度 | 详情见SKILL.md |
@ -163,7 +169,7 @@ gitlink-cli repo +info
gitlink-cli repo +info --owner wbtiger --repo gitlink-cli
```
详见: [gitlink-repo/SKILL.md](gitlink-repo/SKILL.md)
详见: [gitlink-repo/examples/repo-workflow.md](gitlink-repo/examples/repo-workflow.md)
### 场景 2创建和管理 Issue
@ -184,7 +190,7 @@ gitlink-cli issue +close -i 123
gitlink-cli issue +batch-close --numbers 123,124 --dry-run
```
详见: [gitlink-issue/SKILL.md](gitlink-issue/SKILL.md)
详见: [gitlink-issue/examples/issue-workflow.md](gitlink-issue/examples/issue-workflow.md)
### 场景 3管理分支和发布
@ -202,7 +208,7 @@ gitlink-cli release +create -t v1.0.0 -n "v1.0.0 正式版" -b "更新内容..."
gitlink-cli release +view -i <version_id>
```
详见: [gitlink-release/SKILL.md](gitlink-release/SKILL.md)
详见: [gitlink-release/examples/release-workflow.md](gitlink-release/examples/release-workflow.md)
### 场景 4搜索和发现
@ -218,7 +224,7 @@ gitlink-cli org +list
gitlink-cli org +info -i Gitlink
```
详见: [gitlink-search/SKILL.md](gitlink-search/SKILL.md)
详见: [gitlink-search/examples/search-workflow.md](gitlink-search/examples/search-workflow.md)
---
@ -227,8 +233,8 @@ gitlink-cli org +info -i Gitlink
### 快速查找
- **我想了解认证**: [gitlink-shared/SKILL.md](gitlink-shared/SKILL.md)
- **我想查看 API 细节**: [gitlink-shared/references/api-reference.md](gitlink-shared/references/api-reference.md)
- **我遇到了错误**: [gitlink-shared/references/troubleshooting.md](gitlink-shared/references/troubleshooting.md)
- **我想查看 API 细节**: [gitlink-shared/REFERENCE.md](gitlink-shared/REFERENCE.md)
- **我遇到了错误**: [gitlink-shared/TROUBLESHOOTING.md](gitlink-shared/TROUBLESHOOTING.md)
- **我想看工作流示例**: 查看各 Skill 下的 `examples/` 目录
### 按功能分类
@ -236,12 +242,12 @@ gitlink-cli org +info -i Gitlink
**仓库操作**:
- [gitlink-repo/SKILL.md](gitlink-repo/SKILL.md) - 仓库命令
- [gitlink-branch/SKILL.md](gitlink-branch/SKILL.md) - 分支命令
- [gitlink-repo/SKILL.md](gitlink-repo/SKILL.md) - 完整工作流
- [gitlink-repo/examples/repo-workflow.md](gitlink-repo/examples/repo-workflow.md) - 完整工作流
**Issue 和 PR**:
- [gitlink-issue/SKILL.md](gitlink-issue/SKILL.md) - Issue 命令
- [gitlink-pr/SKILL.md](gitlink-pr/SKILL.md) - PR 命令
- [gitlink-issue/SKILL.md](gitlink-issue/SKILL.md) - Issue 工作流
- [gitlink-issue/examples/issue-workflow.md](gitlink-issue/examples/issue-workflow.md) - Issue 工作流
**发布和搜索**:
- [gitlink-release/SKILL.md](gitlink-release/SKILL.md) - Release 命令
@ -283,11 +289,11 @@ gitlink-cli auth login
### Q: 如何查看完整的 API 参考?
A: 查看 [gitlink-shared/references/api-reference.md](gitlink-shared/references/api-reference.md)
A: 查看 [gitlink-shared/REFERENCE.md](gitlink-shared/REFERENCE.md)
### Q: 遇到错误怎么办?
A: 查看 [gitlink-shared/references/troubleshooting.md](gitlink-shared/references/troubleshooting.md)
A: 查看 [gitlink-shared/TROUBLESHOOTING.md](gitlink-shared/TROUBLESHOOTING.md)
---
@ -323,7 +329,7 @@ AI 代理可以:
- 所有边界情况处理正确
- 完整的文档和示例
详见: [../doc/design.md](../doc/design.md)
详见: [../doc/SKILLS_TEST_REPORT_2026-04-02.md](../doc/SKILLS_TEST_REPORT_2026-04-02.md)
---
@ -331,7 +337,8 @@ AI 代理可以:
- [主项目 README](../README.md) - gitlink-cli 项目说明
- [设计文档](../doc/design.md) - 架构设计和开发计划
- [API 参考文档](../doc/gitlink_api_reference.md) - GitLink API 参考文档
- [测试报告](../doc/SKILLS_TEST_REPORT_2026-04-02.md) - 功能测试报告
- [代码同步方案](../doc/CODE_SYNC_STRATEGY_FINAL.md) - GitHub ↔ GitLink 同步设计
- [gitlink-bisync](https://www.gitlink.org.cn/wbtiger/gitlink-bisync) - 代码双向同步系统
---
@ -339,8 +346,8 @@ AI 代理可以:
## 📞 获取帮助
- **命令帮助**: `gitlink-cli <command> --help`
- **故障排查**: [gitlink-shared/references/troubleshooting.md](gitlink-shared/references/troubleshooting.md)
- **API 参考**: [gitlink-shared/references/api-reference.md](gitlink-shared/references/api-reference.md)
- **故障排查**: [gitlink-shared/TROUBLESHOOTING.md](gitlink-shared/TROUBLESHOOTING.md)
- **API 参考**: [gitlink-shared/REFERENCE.md](gitlink-shared/REFERENCE.md)
- **工作流示例**: 查看各 Skill 下的 `examples/` 目录
---
@ -348,7 +355,7 @@ AI 代理可以:
## 🎓 下一步
1. 阅读 [gitlink-shared/SKILL.md](gitlink-shared/SKILL.md) 了解基础
2. 查看 [gitlink-shared/SKILL.md](gitlink-shared/SKILL.md) 完成认证
2. 查看 [gitlink-shared/examples/auth-workflow.md](gitlink-shared/examples/auth-workflow.md) 完成认证
3. 根据需求选择相应的 Skill 文档
4. 参考 `examples/` 目录中的工作流示例
5. 使用 AI 代理自动化你的工作流

View File

@ -22,6 +22,7 @@ metadata:
| `pr +list` | PR 列表 | 否(公开项目) |
| `pr +create` | 创建 PR | 是 |
| `pr +view` | PR 详情 | 否(公开项目) |
| `pr +checkout` | 拉取并切换到 PR 分支 | 否(公开项目,本地需要 git |
| `pr +merge` | 合并 PR | 是 |
| `pr +refuse` | 拒绝并关闭 PR | 是 |
| `pr +reopen` | 重开已关闭的 PR | 是 |
@ -52,6 +53,10 @@ gitlink-cli pr +create --title "feat: 新增搜索功能" --head feature/search
# 查看 PR 详情(使用 pull_request_number即网页 URL 中的序号)
gitlink-cli pr +view --id 3
# 拉取 PR 源分支到本地 review 分支,适合本地跑测试或复现问题
gitlink-cli pr +checkout --id 3 --branch review/pr-3
gitlink-cli pr +checkout --id 3 --force
# 合并 PR支持 merge/rebase/squash
gitlink-cli pr +merge --id 3
gitlink-cli pr +merge --id 3 --method squash

View File

@ -0,0 +1,36 @@
# pr +checkout
将指定 PR 的源分支拉取到本地,并切换到一个本地 review 分支。
```bash
gitlink-cli pr +checkout --id 3
gitlink-cli pr +checkout -i 3 --branch review/pr-3
gitlink-cli pr +checkout -i 3 --force
gitlink-cli pr +checkout -i 3 --dry-run
```
## 参数
| 参数 | 必需 | 说明 |
|------|------|------|
| `--id` / `-i` | 是 | PR 序号,即网页 URL `/pulls/N` 中的数字 |
| `--branch` / `-b` | 否 | 本地分支名,默认 `pr-<id>` |
| `--force` / `-f` | 否 | 本地分支已存在时使用 `git checkout -B` 重置 |
| `--dry-run` | 否 | 只输出将执行的 git 命令,不修改本地仓库 |
## 行为
命令会先读取 PR 详情,解析源 fork、源仓库和源分支然后执行
```bash
git fetch --no-tags <source-url> <source-branch>
git checkout -b <local-branch> FETCH_HEAD
```
如果传入 `--force`,第二步会改为 `git checkout -B <local-branch> FETCH_HEAD`
## 使用建议
- 审查 PR 前可以先执行 `pr +view``pr +files` 获取上下文,再用 `pr +checkout` 拉到本地跑测试。
- fork PR 不需要手动添加 contributor remote命令会从 PR 详情里解析源仓库地址。
- 不确定会执行哪些本地 git 操作时,先加 `--dry-run`