diff --git a/README.md b/README.md index f9553b1..0d18422 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/README.zh-CN.md b/README.zh-CN.md index 9a29684..28ef7fa 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -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 diff --git a/doc/changes/pr-checkout-shortcut.md b/doc/changes/pr-checkout-shortcut.md new file mode 100644 index 0000000..83b9331 --- /dev/null +++ b/doc/changes/pr-checkout-shortcut.md @@ -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-` 本地分支,也支持 `--branch` 指定分支名、`--force` 重置已存在分支、`--dry-run` 预览将执行的 git 命令。 + +本次变更补充了单元测试、README 示例、PR Skill 说明和独立参考文档,覆盖 fork PR 解析、同仓库 head 解析、dry-run、force checkout 以及非法分支名校验。 diff --git a/internal/i18n/locales/en-US.json b/internal/i18n/locales/en-US.json index 6a0feb7..95605c8 100644 --- a/internal/i18n/locales/en-US.json +++ b/internal/i18n/locales/en-US.json @@ -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-)", + "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", diff --git a/internal/i18n/locales/zh-CN.json b/internal/i18n/locales/zh-CN.json index 2a65eb1..929db0b 100644 --- a/internal/i18n/locales/zh-CN.json +++ b/internal/i18n/locales/zh-CN.json @@ -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": "评论(journal)ID,可由 issue +comments 获取", - "flag.issue.comment_reply_to": "要回复的父评论(journal)ID", - "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-)", + "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", diff --git a/shortcuts/pr/checkout.go b/shortcuts/pr/checkout.go new file mode 100644 index 0000000..7933f2e --- /dev/null +++ b/shortcuts/pr/checkout.go @@ -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 +} diff --git a/shortcuts/pr/pr.go b/shortcuts/pr/pr.go index 4025a13..3dfa44c 100644 --- a/shortcuts/pr/pr.go +++ b/shortcuts/pr/pr.go @@ -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"), diff --git a/shortcuts/pr/pr_test.go b/shortcuts/pr/pr_test.go index cdec8e8..b9a8ef6 100644 --- a/shortcuts/pr/pr_test.go +++ b/shortcuts/pr/pr_test.go @@ -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") } } diff --git a/skills/README.md b/skills/README.md index 9be10f1..a36e9ff 100644 --- a/skills/README.md +++ b/skills/README.md @@ -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 ``` -详见: [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 --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 代理自动化你的工作流 diff --git a/skills/gitlink-pr/SKILL.md b/skills/gitlink-pr/SKILL.md index a1644bb..b415515 100644 --- a/skills/gitlink-pr/SKILL.md +++ b/skills/gitlink-pr/SKILL.md @@ -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 diff --git a/skills/gitlink-pr/references/gitlink-pr-checkout.md b/skills/gitlink-pr/references/gitlink-pr-checkout.md new file mode 100644 index 0000000..3b3920f --- /dev/null +++ b/skills/gitlink-pr/references/gitlink-pr-checkout.md @@ -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-` | +| `--force` / `-f` | 否 | 本地分支已存在时使用 `git checkout -B` 重置 | +| `--dry-run` | 否 | 只输出将执行的 git 命令,不修改本地仓库 | + +## 行为 + +命令会先读取 PR 详情,解析源 fork、源仓库和源分支,然后执行: + +```bash +git fetch --no-tags +git checkout -b FETCH_HEAD +``` + +如果传入 `--force`,第二步会改为 `git checkout -B FETCH_HEAD`。 + +## 使用建议 + +- 审查 PR 前可以先执行 `pr +view` 和 `pr +files` 获取上下文,再用 `pr +checkout` 拉到本地跑测试。 +- fork PR 不需要手动添加 contributor remote,命令会从 PR 详情里解析源仓库地址。 +- 不确定会执行哪些本地 git 操作时,先加 `--dry-run`。