From e20f80e26c6eade5d6fe76de32d054dcb1848114 Mon Sep 17 00:00:00 2001 From: NeeNe <26158277@qq.com> Date: Thu, 2 Jul 2026 12:25:38 +0800 Subject: [PATCH] feat(repo,pr): add code history file shortcuts --- README.md | 24 ++ README.zh-CN.md | 24 ++ doc/changes/code-history-file-shortcuts.md | 86 +++++ internal/i18n/locales/en-US.json | 29 ++ internal/i18n/locales/zh-CN.json | 29 ++ shortcuts/pr/pr.go | 21 ++ shortcuts/pr/pr_test.go | 25 ++ shortcuts/repo/repo.go | 349 ++++++++++++++++++ shortcuts/repo/repo_test.go | 253 +++++++++++++ skills/gitlink-commit-quality/SKILL.md | 5 +- skills/gitlink-gatekeeper/REFERENCE.md | 2 +- skills/gitlink-gatekeeper/SKILL.md | 8 +- .../examples/decision-comment.md | 2 +- .../examples/decision-pass.md | 2 +- .../examples/decision-request-changes.md | 2 +- skills/gitlink-pr/SKILL.md | 11 + .../references/gitlink-pr-commits.md | 40 ++ skills/gitlink-repo/SKILL.md | 43 ++- .../references/gitlink-repo-batch-commit.md | 76 ++++ .../references/gitlink-repo-code-history.md | 57 +++ .../references/gitlink-repo-tags.md | 56 +++ 21 files changed, 1129 insertions(+), 15 deletions(-) create mode 100644 doc/changes/code-history-file-shortcuts.md create mode 100644 skills/gitlink-pr/references/gitlink-pr-commits.md create mode 100644 skills/gitlink-repo/references/gitlink-repo-batch-commit.md create mode 100644 skills/gitlink-repo/references/gitlink-repo-code-history.md create mode 100644 skills/gitlink-repo/references/gitlink-repo-tags.md diff --git a/README.md b/README.md index e5e4318..c462f92 100644 --- a/README.md +++ b/README.md @@ -227,6 +227,17 @@ gitlink-cli repo +readme --owner Gitlink --repo forgeplus --ref master gitlink-cli repo +tree --owner Gitlink --repo forgeplus --ref master gitlink-cli repo +tree --owner Gitlink --repo forgeplus --path src --ref main +# Search repository files and inspect commit history +gitlink-cli repo +files --owner Gitlink --repo forgeplus --search README --ref master +gitlink-cli repo +commits --owner Gitlink --repo forgeplus --ref master --limit 20 +gitlink-cli repo +commit-files --owner Gitlink --repo forgeplus --sha +gitlink-cli repo +commit-diff --owner Gitlink --repo forgeplus --sha + +# Inspect repository tags +gitlink-cli repo +tags --owner Gitlink --repo forgeplus --name v1 --only-name true +gitlink-cli repo +tag --owner Gitlink --repo forgeplus --name v1.0.0 +gitlink-cli repo +delete-tag --owner Gitlink --repo forgeplus --name v1.0.0 --dry-run + # Show language breakdown gitlink-cli repo +languages --owner Gitlink --repo forgeplus @@ -250,6 +261,16 @@ gitlink-cli repo +unfollow --owner Gitlink --repo forgeplus --project-id 123 gitlink-cli repo +like --owner Gitlink --repo forgeplus gitlink-cli repo +unlike --owner Gitlink --repo forgeplus --project-id 123 +# Preview and apply a multi-file commit +gitlink-cli repo +batch-commit --owner me --repo proj \ + --branch master --message "docs: update guide" \ + --files 'update:README.md:# Updated;create:docs/demo.md:# Demo' \ + --dry-run +gitlink-cli repo +batch-commit --owner me --repo proj \ + --branch master --message "docs: update guide" \ + --files 'update:README.md:# Updated;delete:old.md' \ + --yes + # Create a repository gitlink-cli repo +create -n my-project -d "Project description" @@ -420,6 +441,9 @@ gitlink-cli pr +reopen --owner Gitlink --repo forgeplus -i 42 # View changed files gitlink-cli pr +files --owner Gitlink --repo forgeplus -i 42 +# View commits included in a PR +gitlink-cli pr +commits --owner Gitlink --repo forgeplus -i 42 + # List PR patchset versions gitlink-cli pr +versions --owner Gitlink --repo forgeplus -i 42 diff --git a/README.zh-CN.md b/README.zh-CN.md index 6a8879d..2bd0330 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -238,6 +238,17 @@ gitlink-cli repo +readme --owner Gitlink --repo forgeplus --ref master gitlink-cli repo +tree --owner Gitlink --repo forgeplus --ref master gitlink-cli repo +tree --owner Gitlink --repo forgeplus --path src --ref main +# 搜索仓库文件并查看提交历史 +gitlink-cli repo +files --owner Gitlink --repo forgeplus --search README --ref master +gitlink-cli repo +commits --owner Gitlink --repo forgeplus --ref master --limit 20 +gitlink-cli repo +commit-files --owner Gitlink --repo forgeplus --sha +gitlink-cli repo +commit-diff --owner Gitlink --repo forgeplus --sha + +# 查看仓库标签 +gitlink-cli repo +tags --owner Gitlink --repo forgeplus --name v1 --only-name true +gitlink-cli repo +tag --owner Gitlink --repo forgeplus --name v1.0.0 +gitlink-cli repo +delete-tag --owner Gitlink --repo forgeplus --name v1.0.0 --dry-run + # 查看语言占比 gitlink-cli repo +languages --owner Gitlink --repo forgeplus @@ -261,6 +272,16 @@ gitlink-cli repo +unfollow --owner Gitlink --repo forgeplus --project-id 123 gitlink-cli repo +like --owner Gitlink --repo forgeplus gitlink-cli repo +unlike --owner Gitlink --repo forgeplus --project-id 123 +# 预览并执行多文件提交 +gitlink-cli repo +batch-commit --owner me --repo proj \ + --branch master --message "docs: update guide" \ + --files 'update:README.md:# Updated;create:docs/demo.md:# Demo' \ + --dry-run +gitlink-cli repo +batch-commit --owner me --repo proj \ + --branch master --message "docs: update guide" \ + --files 'update:README.md:# Updated;delete:old.md' \ + --yes + # 创建仓库 gitlink-cli repo +create -n my-project -d "项目描述" @@ -430,6 +451,9 @@ gitlink-cli pr +reopen --owner Gitlink --repo forgeplus -i 42 # 查看 PR 变更文件 gitlink-cli pr +files --owner Gitlink --repo forgeplus -i 42 +# 查看 PR 包含的提交 +gitlink-cli pr +commits --owner Gitlink --repo forgeplus -i 42 + # 查看 PR patchset/version 列表 gitlink-cli pr +versions --owner Gitlink --repo forgeplus -i 42 diff --git a/doc/changes/code-history-file-shortcuts.md b/doc/changes/code-history-file-shortcuts.md new file mode 100644 index 0000000..ada2c9e --- /dev/null +++ b/doc/changes/code-history-file-shortcuts.md @@ -0,0 +1,86 @@ +# Code history and batch file shortcuts + +## Background + +Agent workflows such as PR review, commit quality checks, repository health +reports, and research reproducibility audits need commit timelines, changed +files, commit diffs, and sometimes controlled multi-file updates. Before this +change, several of these operations required Raw API calls. + +This change adds a larger Subtask 1 feature set around repository code history +and file operations. + +## New shortcuts + +Repository shortcuts: + +- `repo +files` searches repository files with optional `--search` and `--ref`. +- `repo +commits` lists commits for a branch, tag, or commit ref with pagination. +- `repo +commit-files` lists files changed by a commit, with optional file-path filtering. +- `repo +commit-diff` returns a commit diff. +- `repo +tags` lists repository tags with pagination and optional name filtering. +- `repo +tag` returns one tag's metadata and target commit. +- `repo +delete-tag` deletes a repository tag after explicit confirmation. +- `repo +batch-commit` creates, updates, or deletes multiple files in one commit. + +Pull request shortcuts: + +- `pr +commits` lists commits included in a pull request. + +## Safety model + +All history and file inspection commands are read-only. + +`repo +delete-tag` and `repo +batch-commit` can modify repository content, so +they require an explicit confirmation flag for remote writes: + +```bash +gitlink-cli repo +delete-tag --owner me --repo proj --name v0.1.0 --dry-run +gitlink-cli repo +delete-tag --owner me --repo proj --name v0.1.0 --yes +``` + +```bash +gitlink-cli repo +batch-commit --owner me --repo proj \ + --branch master --message "docs: update" \ + --files 'update:README.md:# Updated' \ + --dry-run + +gitlink-cli repo +batch-commit --owner me --repo proj \ + --branch master --message "docs: update" \ + --files 'update:README.md:# Updated' \ + --yes +``` + +The `--files` format is: + +```text +action:path[:content][;action:path[:content]...] +``` + +Supported actions are `create`, `update`, and `delete`. `create` and `update` +require content; `delete` does not. + +## Tests + +Unit tests cover: + +- repository file search query mapping; +- commit list pagination and ref mapping; +- commit changed-file and diff endpoints; +- repository tag list/detail/delete endpoint mapping; +- PR commit list endpoint; +- `repo +batch-commit` dry-run behavior; +- `repo +batch-commit` remote write protection without `--yes`; +- batch file operation payload construction and validation. + +Suggested verification: + +```bash +go test ./shortcuts/repo ./shortcuts/pr +``` + +Full project verification: + +```bash +make test +``` diff --git a/internal/i18n/locales/en-US.json b/internal/i18n/locales/en-US.json index 0739395..1491272 100644 --- a/internal/i18n/locales/en-US.json +++ b/internal/i18n/locales/en-US.json @@ -54,6 +54,7 @@ "cmd.org.short": "Organization operations", "cmd.pr.close.short": "Close a pull request", "cmd.pr.comment.short": "Add a comment to a pull request", + "cmd.pr.commits.short": "List commits in a pull request", "cmd.pr.create.short": "Create a pull request", "cmd.pr.diff.short": "Show diff for a pull request", "cmd.pr.files.short": "List changed files in a pull request", @@ -82,11 +83,19 @@ "cmd.release.short": "Release operations", "cmd.release.view.short": "View release details", "cmd.repo.create.short": "Create a new repository", + "cmd.repo.batch_commit.short": "Create, update, or delete multiple files in one commit", + "cmd.repo.commit_diff.short": "Show diff for one commit", + "cmd.repo.commit_files.short": "List files changed by one commit", + "cmd.repo.commits.short": "List repository commits", + "cmd.repo.delete_tag.short": "Delete a repository tag", "cmd.repo.delete.short": "Delete a repository", + "cmd.repo.files.short": "Search repository files", "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.short": "Repository operations", + "cmd.repo.tag.short": "View repository tag details", + "cmd.repo.tags.short": "List repository tags", "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", @@ -209,9 +218,29 @@ "flag.release.target": "Target branch", "flag.repo": "Repository name (auto-detected from git remote)", "flag.repo.category": "Filter: manage/mirror/sync/fork/all (default: manage)", + "flag.repo.author_email": "Commit author email", + "flag.repo.author_name": "Commit author name", + "flag.repo.batch_branch": "Target branch", + "flag.repo.batch_dry_run": "Preview the batch commit request without changing files", + "flag.repo.batch_encoding": "Content encoding for inline file content: text or base64", + "flag.repo.batch_files": "File operations: action:path:content; repeat with semicolons", + "flag.repo.batch_message": "Commit message", + "flag.repo.batch_new_branch": "Create and commit to this new branch", + "flag.repo.batch_yes": "Confirm remote file changes", + "flag.repo.commit_sha": "Commit SHA", + "flag.repo.committer_email": "Commit committer email", + "flag.repo.committer_name": "Commit committer name", + "flag.repo.delete_tag_dry_run": "Preview the tag deletion without changing repository state", + "flag.repo.delete_tag_yes": "Confirm the destructive tag deletion", "flag.repo.description": "Repository description", + "flag.repo.file": "Filter by file path", + "flag.repo.files.search": "File name/path keyword", "flag.repo.name": "Repository name", + "flag.repo.only_name": "Return only names (true/false)", "flag.repo.private": "Make repository private (true/false)", + "flag.repo.ref": "Branch, tag, or commit SHA", + "flag.repo.tag_name": "Tag name", + "flag.repo.tag_name_filter": "Tag search keyword", "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", diff --git a/internal/i18n/locales/zh-CN.json b/internal/i18n/locales/zh-CN.json index 2e6fc4d..cb6fd96 100644 --- a/internal/i18n/locales/zh-CN.json +++ b/internal/i18n/locales/zh-CN.json @@ -54,6 +54,7 @@ "cmd.org.short": "组织操作", "cmd.pr.close.short": "关闭拉取请求", "cmd.pr.comment.short": "给拉取请求添加评论", + "cmd.pr.commits.short": "列出拉取请求中的提交", "cmd.pr.create.short": "创建拉取请求", "cmd.pr.diff.short": "显示拉取请求 diff", "cmd.pr.files.short": "列出拉取请求中的变更文件", @@ -82,11 +83,19 @@ "cmd.release.short": "发布操作", "cmd.release.view.short": "查看发布详情", "cmd.repo.create.short": "创建新仓库", + "cmd.repo.batch_commit.short": "在一次提交中创建、更新或删除多个文件", + "cmd.repo.commit_diff.short": "显示单个提交的 diff", + "cmd.repo.commit_files.short": "列出单个提交变更的文件", + "cmd.repo.commits.short": "列出仓库提交", + "cmd.repo.delete_tag.short": "删除仓库标签", "cmd.repo.delete.short": "删除仓库", + "cmd.repo.files.short": "搜索仓库文件", "cmd.repo.fork.short": "Fork 仓库", "cmd.repo.info.short": "显示仓库详情", "cmd.repo.list.short": "列出用户或组织的仓库", "cmd.repo.short": "仓库操作", + "cmd.repo.tag.short": "查看仓库标签详情", + "cmd.repo.tags.short": "列出仓库标签", "cmd.repo.tree.short": "列出仓库文件和目录", "cmd.root.long": "用于管理 GitLink 上的仓库、议题、拉取请求、发布、CI 和工作流。", "cmd.root.short": "GitLink CLI - GitLink 命令行工具", @@ -209,9 +218,29 @@ "flag.release.target": "目标分支", "flag.repo": "仓库名称(自动从 git remote 检测)", "flag.repo.category": "筛选:manage/mirror/sync/fork/all(默认:manage)", + "flag.repo.author_email": "提交作者邮箱", + "flag.repo.author_name": "提交作者名称", + "flag.repo.batch_branch": "目标分支", + "flag.repo.batch_dry_run": "预览批量提交请求,不修改文件", + "flag.repo.batch_encoding": "内联文件内容编码:text 或 base64", + "flag.repo.batch_files": "文件操作:action:path:content;多个操作用英文分号分隔", + "flag.repo.batch_message": "提交信息", + "flag.repo.batch_new_branch": "创建并提交到这个新分支", + "flag.repo.batch_yes": "确认修改远端文件", + "flag.repo.commit_sha": "Commit SHA", + "flag.repo.committer_email": "提交者邮箱", + "flag.repo.committer_name": "提交者名称", + "flag.repo.delete_tag_dry_run": "预览标签删除操作,不修改仓库状态", + "flag.repo.delete_tag_yes": "确认执行破坏性标签删除", "flag.repo.description": "仓库描述", + "flag.repo.file": "按文件路径筛选", + "flag.repo.files.search": "文件名或路径关键词", "flag.repo.name": "仓库名称", + "flag.repo.only_name": "只返回名称(true/false)", "flag.repo.private": "设为私有仓库(true/false)", + "flag.repo.ref": "分支、标签或 Commit SHA", + "flag.repo.tag_name": "标签名称", + "flag.repo.tag_name_filter": "标签搜索关键词", "flag.repo.tree.path": "要列出的目录路径(默认:仓库根目录)", "flag.repo.tree.ref": "分支、标签或提交引用", "flag.search.keyword": "搜索关键词", diff --git a/shortcuts/pr/pr.go b/shortcuts/pr/pr.go index 03f537f..afa25b4 100644 --- a/shortcuts/pr/pr.go +++ b/shortcuts/pr/pr.go @@ -245,6 +245,27 @@ func Shortcuts(translators ...*i18n.Translator) []*common.Shortcut { return ctx.Output(env) }, }, + { + Name: "commits", + Description: tr.T("cmd.pr.commits.short"), + Flags: []common.Flag{ + {Name: "id", Short: "i", Usage: tr.T("flag.pr.id"), Required: true}, + }, + Run: func(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", fmt.Sprintf("%s/pulls/%s/commits", ctx.RepoPath(), url.PathEscape(id)), nil) + if err != nil { + return err + } + return ctx.Output(env) + }, + }, { Name: "versions", Description: tr.T("cmd.pr.versions.short"), diff --git a/shortcuts/pr/pr_test.go b/shortcuts/pr/pr_test.go index eece6d9..1908813 100644 --- a/shortcuts/pr/pr_test.go +++ b/shortcuts/pr/pr_test.go @@ -351,6 +351,31 @@ func TestPRDiff(t *testing.T) { } } +// --- commits --- + +func TestPRCommits(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 != "/owner/repo/pulls/42/commits.json" { + t.Fatalf("unexpected path: %s", r.URL.Path) + } + writeJSON(t, w, map[string]interface{}{ + "commits_count": float64(1), + "commits": []interface{}{ + map[string]interface{}{"sha": "abc123", "message": "feat: add shortcut"}, + }, + }) + })) + defer server.Close() + + err := runPRShortcut(t, server, "commits", map[string]string{"id": "42"}) + if err != nil { + t.Fatalf("commits failed: %v", err) + } +} + // --- extractIssueID --- func TestExtractIssueID(t *testing.T) { diff --git a/shortcuts/repo/repo.go b/shortcuts/repo/repo.go index 06774a6..4292bbd 100644 --- a/shortcuts/repo/repo.go +++ b/shortcuts/repo/repo.go @@ -108,6 +108,91 @@ func Shortcuts(translators ...*i18n.Translator) []*common.Shortcut { return ctx.Output(env) }, }, + { + Name: "files", + Description: tr.T("cmd.repo.files.short"), + Flags: []common.Flag{ + {Name: "search", Short: "s", Usage: tr.T("flag.repo.files.search")}, + {Name: "ref", Short: "r", Usage: tr.T("flag.repo.ref")}, + }, + Run: runFiles, + }, + { + Name: "commits", + Description: tr.T("cmd.repo.commits.short"), + Flags: []common.Flag{ + {Name: "ref", Short: "r", Usage: tr.T("flag.repo.ref")}, + {Name: "page", Short: "p", Usage: tr.T("flag.page"), Default: "1"}, + {Name: "limit", Short: "l", Usage: tr.T("flag.limit"), Default: "20"}, + }, + Run: runCommits, + }, + { + Name: "commit-files", + Description: tr.T("cmd.repo.commit_files.short"), + Flags: []common.Flag{ + {Name: "sha", Short: "s", Usage: tr.T("flag.repo.commit_sha"), Required: true}, + {Name: "file", Short: "f", Usage: tr.T("flag.repo.file")}, + {Name: "page", Short: "p", Usage: tr.T("flag.page"), Default: "1"}, + {Name: "limit", Short: "l", Usage: tr.T("flag.limit"), Default: "20"}, + }, + Run: runCommitFiles, + }, + { + Name: "commit-diff", + Description: tr.T("cmd.repo.commit_diff.short"), + Flags: []common.Flag{ + {Name: "sha", Short: "s", Usage: tr.T("flag.repo.commit_sha"), Required: true}, + }, + Run: runCommitDiff, + }, + { + Name: "tags", + Description: tr.T("cmd.repo.tags.short"), + Flags: []common.Flag{ + {Name: "name", Short: "n", Usage: tr.T("flag.repo.tag_name_filter")}, + {Name: "only-name", Usage: tr.T("flag.repo.only_name"), Default: "false"}, + {Name: "page", Short: "p", Usage: tr.T("flag.page"), Default: "1"}, + {Name: "limit", Short: "l", Usage: tr.T("flag.limit"), Default: "20"}, + }, + Run: runTags, + }, + { + Name: "tag", + Description: tr.T("cmd.repo.tag.short"), + Flags: []common.Flag{ + {Name: "name", Short: "n", Usage: tr.T("flag.repo.tag_name"), Required: true}, + }, + Run: runTag, + }, + { + Name: "delete-tag", + Description: tr.T("cmd.repo.delete_tag.short"), + Flags: []common.Flag{ + {Name: "name", Short: "n", Usage: tr.T("flag.repo.tag_name"), Required: true}, + {Name: "dry-run", Usage: tr.T("flag.repo.delete_tag_dry_run"), Bool: true, Default: "false"}, + {Name: "yes", Usage: tr.T("flag.repo.delete_tag_yes"), Bool: true, Default: "false"}, + }, + Run: runDeleteTag, + }, + { + Name: "batch-commit", + Description: tr.T("cmd.repo.batch_commit.short"), + Flags: []common.Flag{ + {Name: "branch", Short: "b", Usage: tr.T("flag.repo.batch_branch"), Required: true}, + {Name: "message", Short: "m", Usage: tr.T("flag.repo.batch_message"), Required: true}, + {Name: "files", Short: "f", Usage: tr.T("flag.repo.batch_files"), Required: true}, + {Name: "new-branch", Usage: tr.T("flag.repo.batch_new_branch")}, + {Name: "encoding", Usage: tr.T("flag.repo.batch_encoding"), Default: "text"}, + {Name: "author-name", Usage: tr.T("flag.repo.author_name")}, + {Name: "author-email", Usage: tr.T("flag.repo.author_email")}, + {Name: "committer-name", Usage: tr.T("flag.repo.committer_name")}, + {Name: "committer-email", Usage: tr.T("flag.repo.committer_email")}, + {Name: "dry-run", Usage: tr.T("flag.repo.batch_dry_run"), Bool: true, Default: "false"}, + {Name: "yes", Usage: tr.T("flag.repo.batch_yes"), Bool: true, Default: "false"}, + }, + Run: runBatchCommit, + }, { Name: "languages", Description: "Show repository language statistics", @@ -263,6 +348,173 @@ func shortcutTranslator(translators ...*i18n.Translator) *i18n.Translator { return i18n.Default() } +func runFiles(ctx *common.RuntimeContext) error { + if err := ctx.ResolveOwnerRepo(); err != nil { + return err + } + q := url.Values{} + setRepoQueryIfPresent(q, "search", ctx.Arg("search")) + setRepoQueryIfPresent(q, "ref", ctx.Arg("ref")) + env, err := ctx.CallAPIWithQuery("GET", ctx.RepoPath()+"/files", q) + if err != nil { + return err + } + return ctx.Output(env) +} + +func runCommits(ctx *common.RuntimeContext) error { + if err := ctx.ResolveOwnerRepo(); err != nil { + return err + } + q := url.Values{} + q.Set("page", firstRepoValue(ctx.Arg("page"), "1")) + q.Set("limit", firstRepoValue(ctx.Arg("limit"), "20")) + setRepoQueryIfPresent(q, "sha", ctx.Arg("ref")) + env, err := ctx.CallAPIWithQuery("GET", "/v1"+ctx.RepoPath()+"/commits", q) + if err != nil { + return err + } + return ctx.Output(env) +} + +func runCommitFiles(ctx *common.RuntimeContext) error { + if err := ctx.ResolveOwnerRepo(); err != nil { + return err + } + sha, err := ctx.RequireArg("sha") + if err != nil { + return err + } + q := url.Values{} + if file := strings.TrimSpace(ctx.Arg("file")); file != "" { + q.Set("filepath", file) + } else { + q.Set("page", firstRepoValue(ctx.Arg("page"), "1")) + q.Set("limit", firstRepoValue(ctx.Arg("limit"), "20")) + } + env, err := ctx.CallAPIWithQuery("GET", fmt.Sprintf("/v1%s/commits/%s/files", ctx.RepoPath(), url.PathEscape(sha)), q) + if err != nil { + return err + } + return ctx.Output(env) +} + +func runCommitDiff(ctx *common.RuntimeContext) error { + if err := ctx.ResolveOwnerRepo(); err != nil { + return err + } + sha, err := ctx.RequireArg("sha") + if err != nil { + return err + } + env, err := ctx.CallAPI("GET", fmt.Sprintf("/v1%s/commits/%s/diff", ctx.RepoPath(), url.PathEscape(sha)), nil) + if err != nil { + return err + } + return ctx.Output(env) +} + +func runTags(ctx *common.RuntimeContext) error { + if err := ctx.ResolveOwnerRepo(); err != nil { + return err + } + q := url.Values{} + if name := strings.TrimSpace(ctx.Arg("name")); name != "" { + q.Set("name", name) + } + onlyName := strings.TrimSpace(ctx.Arg("only-name")) + if onlyName != "" && onlyName != "false" { + q.Set("only_name", onlyName) + } + if len(q) > 0 { + env, err := ctx.CallAPIWithQuery("GET", ctx.RepoPath()+"/tags", q) + if err != nil { + return err + } + return ctx.Output(env) + } + q.Set("page", firstRepoValue(ctx.Arg("page"), "1")) + q.Set("limit", firstRepoValue(ctx.Arg("limit"), "20")) + env, err := ctx.CallAPIWithQuery("GET", "/v1"+ctx.RepoPath()+"/tags", q) + if err != nil { + return err + } + return ctx.Output(env) +} + +func runTag(ctx *common.RuntimeContext) error { + if err := ctx.ResolveOwnerRepo(); err != nil { + return err + } + name, err := ctx.RequireArg("name") + if err != nil { + return err + } + env, err := ctx.CallAPI("GET", fmt.Sprintf("/v1%s/tags/%s", ctx.RepoPath(), url.PathEscape(name)), nil) + if err != nil { + return err + } + return ctx.Output(env) +} + +func runDeleteTag(ctx *common.RuntimeContext) error { + if err := ctx.ResolveOwnerRepo(); err != nil { + return err + } + name, err := ctx.RequireArg("name") + if err != nil { + return err + } + path := fmt.Sprintf("/v1%s/tags/%s", ctx.RepoPath(), url.PathEscape(name)) + if ctx.Arg("dry-run") == "true" { + return ctx.OutputData(map[string]interface{}{ + "dry_run": true, + "action": "delete_tag", + "method": "DELETE", + "path": path, + "repository": fmt.Sprintf("%s/%s", ctx.Owner, ctx.Repo), + "tag": name, + }) + } + if ctx.Arg("yes") != "true" { + return fmt.Errorf("tag deletion is destructive; run --dry-run first, then pass --yes to execute") + } + env, err := ctx.CallAPI("DELETE", path, nil) + if err != nil { + return err + } + return ctx.Output(env) +} + +func runBatchCommit(ctx *common.RuntimeContext) error { + if err := ctx.ResolveOwnerRepo(); err != nil { + return err + } + payload, err := batchCommitPayload(ctx) + if err != nil { + return err + } + path := "/v1" + ctx.RepoPath() + "/contents/batch" + if ctx.Arg("dry-run") == "true" { + return ctx.OutputData(map[string]interface{}{ + "dry_run": true, + "action": "batch_commit", + "method": "POST", + "path": path, + "repository": fmt.Sprintf("%s/%s", ctx.Owner, ctx.Repo), + "payload": payload, + }) + } + if ctx.Arg("yes") != "true" { + return fmt.Errorf("batch file commit changes repository content; run --dry-run first, then pass --yes to execute") + } + env, err := ctx.CallAPI("POST", path, payload) + if err != nil { + return err + } + return ctx.Output(env) +} + func runLanguages(ctx *common.RuntimeContext) error { if err := ctx.ResolveOwnerRepo(); err != nil { return err @@ -482,6 +734,103 @@ func setRepoQueryIfPresent(q url.Values, key, value string) { } } +func firstRepoValue(value, fallback string) string { + if value := strings.TrimSpace(value); value != "" { + return value + } + return fallback +} + +func batchCommitPayload(ctx *common.RuntimeContext) (map[string]interface{}, error) { + branch, err := ctx.RequireArg("branch") + if err != nil { + return nil, err + } + message, err := ctx.RequireArg("message") + if err != nil { + return nil, err + } + rawFiles, err := ctx.RequireArg("files") + if err != nil { + return nil, err + } + encoding := firstRepoValue(ctx.Arg("encoding"), "text") + if encoding != "text" && encoding != "base64" { + return nil, fmt.Errorf("invalid --encoding %q: use text or base64", encoding) + } + files, err := parseBatchFileSpecs(rawFiles, encoding) + if err != nil { + return nil, err + } + payload := map[string]interface{}{ + "branch": strings.TrimSpace(branch), + "message": message, + "files": files, + } + if newBranch := strings.TrimSpace(ctx.Arg("new-branch")); newBranch != "" { + payload["new_branch"] = newBranch + } + setRepoPayloadIfPresent(payload, "author_name", ctx.Arg("author-name")) + setRepoPayloadIfPresent(payload, "author_email", ctx.Arg("author-email")) + setRepoPayloadIfPresent(payload, "committer_name", ctx.Arg("committer-name")) + setRepoPayloadIfPresent(payload, "committer_email", ctx.Arg("committer-email")) + return payload, nil +} + +func parseBatchFileSpecs(raw, encoding string) ([]map[string]interface{}, error) { + specs := strings.Split(raw, ";") + files := make([]map[string]interface{}, 0, len(specs)) + for _, spec := range specs { + spec = strings.TrimSpace(spec) + if spec == "" { + continue + } + parts := strings.SplitN(spec, ":", 3) + if len(parts) < 2 { + return nil, fmt.Errorf("invalid --files item %q: use action:path[:content]", spec) + } + action := strings.TrimSpace(parts[0]) + path := strings.TrimSpace(parts[1]) + if !isBatchFileAction(action) { + return nil, fmt.Errorf("invalid file action %q: use create, update, or delete", action) + } + if path == "" { + return nil, fmt.Errorf("invalid --files item %q: file path is required", spec) + } + item := map[string]interface{}{ + "action_type": action, + "file_path": path, + } + if action != "delete" { + if len(parts) != 3 { + return nil, fmt.Errorf("file action %q for %q requires content", action, path) + } + item["content"] = parts[2] + item["encoding"] = encoding + } + files = append(files, item) + } + if len(files) == 0 { + return nil, fmt.Errorf("--files must include at least one file operation") + } + return files, nil +} + +func isBatchFileAction(action string) bool { + switch action { + case "create", "update", "delete": + return true + default: + return false + } +} + +func setRepoPayloadIfPresent(payload map[string]interface{}, key, value string) { + if value := strings.TrimSpace(value); value != "" { + payload[key] = value + } +} + func parseOptionalRepoNonNegativeInt(value, name string) (int, bool, error) { if strings.TrimSpace(value) == "" { return 0, false, nil diff --git a/shortcuts/repo/repo_test.go b/shortcuts/repo/repo_test.go index 5c5f34b..3b0d6cb 100644 --- a/shortcuts/repo/repo_test.go +++ b/shortcuts/repo/repo_test.go @@ -57,6 +57,15 @@ func writeText(t *testing.T, w http.ResponseWriter, code int, text string) { } } +func decodeRepoJSON(t *testing.T, r *http.Request) map[string]interface{} { + t.Helper() + var payload map[string]interface{} + if err := json.NewDecoder(r.Body).Decode(&payload); err != nil { + t.Fatalf("decode request body: %v", err) + } + return payload +} + // --- list --- func TestRepoListDefault(t *testing.T) { @@ -273,6 +282,250 @@ func TestRepoCodeStatsUsesRefQuery(t *testing.T) { } } +func TestRepoFilesBuildsSearchAndRefQuery(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + assertRequest(t, r, "GET", "/owner/repo/files.json") + assertEqual(t, r.URL.Query().Get("search"), "README") + assertEqual(t, r.URL.Query().Get("ref"), "main") + writeJSON(t, w, []interface{}{map[string]interface{}{"path": "README.md"}}) + })) + defer server.Close() + + if err := runShortcut(t, server, "files", map[string]string{"search": "README", "ref": "main"}); err != nil { + t.Fatalf("files shortcut failed: %v", err) + } +} + +func TestRepoCommitsBuildsRefPaginationQuery(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + assertRequest(t, r, "GET", "/v1/owner/repo/commits.json") + assertEqual(t, r.URL.Query().Get("sha"), "main") + assertEqual(t, r.URL.Query().Get("page"), "2") + assertEqual(t, r.URL.Query().Get("limit"), "50") + writeJSON(t, w, map[string]interface{}{"total_count": 1, "commits": []interface{}{}}) + })) + defer server.Close() + + err := runShortcut(t, server, "commits", map[string]string{"ref": "main", "page": "2", "limit": "50"}) + if err != nil { + t.Fatalf("commits shortcut failed: %v", err) + } +} + +func TestRepoCommitFilesUsesPaginationWhenNoFileFilter(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + assertRequest(t, r, "GET", "/v1/owner/repo/commits/abc123/files.json") + assertEqual(t, r.URL.Query().Get("page"), "3") + assertEqual(t, r.URL.Query().Get("limit"), "10") + if got := r.URL.Query().Get("filepath"); got != "" { + t.Fatalf("did not expect filepath query, got %q", got) + } + writeJSON(t, w, map[string]interface{}{"files": []interface{}{}}) + })) + defer server.Close() + + err := runShortcut(t, server, "commit-files", map[string]string{"sha": "abc123", "page": "3", "limit": "10"}) + if err != nil { + t.Fatalf("commit-files shortcut failed: %v", err) + } +} + +func TestRepoCommitFilesUsesFileFilterWithoutPagination(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + assertRequest(t, r, "GET", "/v1/owner/repo/commits/abc123/files.json") + assertEqual(t, r.URL.Query().Get("filepath"), "src/main.go") + if got := r.URL.Query().Get("page"); got != "" { + t.Fatalf("did not expect page query with filepath filter, got %q", got) + } + writeJSON(t, w, map[string]interface{}{"files": []interface{}{}}) + })) + defer server.Close() + + err := runShortcut(t, server, "commit-files", map[string]string{"sha": "abc123", "file": "src/main.go"}) + if err != nil { + t.Fatalf("commit-files shortcut failed: %v", err) + } +} + +func TestRepoCommitDiffUsesCommitDiffEndpoint(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + assertRequest(t, r, "GET", "/v1/owner/repo/commits/abc123/diff.json") + writeJSON(t, w, map[string]interface{}{"diff": "@@ -1 +1 @@"}) + })) + defer server.Close() + + if err := runShortcut(t, server, "commit-diff", map[string]string{"sha": "abc123"}); err != nil { + t.Fatalf("commit-diff shortcut failed: %v", err) + } +} + +func TestRepoTagsBuildsPaginationQuery(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + assertRequest(t, r, "GET", "/v1/owner/repo/tags.json") + assertEqual(t, r.URL.Query().Get("page"), "2") + assertEqual(t, r.URL.Query().Get("limit"), "30") + writeJSON(t, w, map[string]interface{}{"total_count": 1, "tags": []interface{}{}}) + })) + defer server.Close() + + err := runShortcut(t, server, "tags", map[string]string{"page": "2", "limit": "30"}) + if err != nil { + t.Fatalf("tags shortcut failed: %v", err) + } +} + +func TestRepoTagsUsesSearchEndpointForNameFilter(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + assertRequest(t, r, "GET", "/owner/repo/tags.json") + assertEqual(t, r.URL.Query().Get("name"), "v1") + assertEqual(t, r.URL.Query().Get("only_name"), "true") + writeJSON(t, w, map[string]interface{}{"total_count": 1, "tags": []interface{}{map[string]interface{}{"name": "v1.0.0"}}}) + })) + defer server.Close() + + err := runShortcut(t, server, "tags", map[string]string{"name": "v1", "only-name": "true"}) + if err != nil { + t.Fatalf("tags shortcut with name filter failed: %v", err) + } +} + +func TestRepoTagUsesTagDetailEndpoint(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + assertRequest(t, r, "GET", "/v1/owner/repo/tags/v1.0.0.json") + writeJSON(t, w, map[string]interface{}{"name": "v1.0.0"}) + })) + defer server.Close() + + if err := runShortcut(t, server, "tag", map[string]string{"name": "v1.0.0"}); err != nil { + t.Fatalf("tag shortcut failed: %v", err) + } +} + +func TestRepoDeleteTagDryRunDoesNotCallAPI(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + t.Fatalf("delete-tag dry-run should not call remote API: %s %s", r.Method, r.URL.Path) + })) + defer server.Close() + + err := runShortcut(t, server, "delete-tag", map[string]string{"name": "v1.0.0", "dry-run": "true"}) + if err != nil { + t.Fatalf("delete-tag dry-run failed: %v", err) + } +} + +func TestRepoDeleteTagRequiresYesForRemoteDelete(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + t.Fatalf("delete-tag without --yes should not call remote API: %s %s", r.Method, r.URL.Path) + })) + defer server.Close() + + err := runShortcut(t, server, "delete-tag", map[string]string{"name": "v1.0.0"}) + if err == nil { + t.Fatal("expected error when delete-tag is missing --yes") + } +} + +func TestRepoDeleteTagWithYesCallsDeleteEndpoint(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + assertRequest(t, r, "DELETE", "/v1/owner/repo/tags/v1.0.0.json") + writeJSON(t, w, map[string]interface{}{"status": 0, "message": "success"}) + })) + defer server.Close() + + err := runShortcut(t, server, "delete-tag", map[string]string{"name": "v1.0.0", "yes": "true"}) + if err != nil { + t.Fatalf("delete-tag failed: %v", err) + } +} + +func TestRepoBatchCommitDryRunDoesNotCallAPI(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + t.Fatalf("dry-run should not call remote API: %s %s", r.Method, r.URL.Path) + })) + defer server.Close() + + err := runShortcut(t, server, "batch-commit", map[string]string{ + "branch": "master", + "message": "docs: update", + "files": "update:README.md:# hello;delete:old.txt", + "new-branch": "docs/update-readme", + "dry-run": "true", + }) + if err != nil { + t.Fatalf("batch-commit dry-run failed: %v", err) + } +} + +func TestRepoBatchCommitRequiresYesForRemoteWrite(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + t.Fatalf("batch-commit without --yes should not call remote API: %s %s", r.Method, r.URL.Path) + })) + defer server.Close() + + err := runShortcut(t, server, "batch-commit", map[string]string{ + "branch": "master", + "message": "docs: update", + "files": "update:README.md:# hello", + }) + if err == nil { + t.Fatal("expected error when batch-commit is missing --yes") + } +} + +func TestRepoBatchCommitPostsPayloadWithYes(t *testing.T) { + var payload map[string]interface{} + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + assertRequest(t, r, "POST", "/v1/owner/repo/contents/batch.json") + payload = decodeRepoJSON(t, r) + writeJSON(t, w, map[string]interface{}{"commit": map[string]interface{}{"sha": "abc123"}}) + })) + defer server.Close() + + err := runShortcut(t, server, "batch-commit", map[string]string{ + "branch": "master", + "message": "docs: update", + "files": "create:docs/new.md:# new;update:README.md:# hello;delete:old.txt", + "encoding": "text", + "author-name": "Alice", + "author-email": "alice@example.com", + "committer-name": "Bot", + "committer-email": "bot@example.com", + "yes": "true", + }) + if err != nil { + t.Fatalf("batch-commit failed: %v", err) + } + assertEqual(t, payload["branch"], "master") + assertEqual(t, payload["message"], "docs: update") + assertEqual(t, payload["author_name"], "Alice") + files, ok := payload["files"].([]interface{}) + if !ok || len(files) != 3 { + t.Fatalf("expected three file operations, got %#v", payload["files"]) + } + first, _ := files[0].(map[string]interface{}) + assertEqual(t, first["action_type"], "create") + assertEqual(t, first["file_path"], "docs/new.md") + assertEqual(t, first["content"], "# new") + assertEqual(t, first["encoding"], "text") +} + +func TestRepoBatchCommitRejectsInvalidFileSpec(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(http.ResponseWriter, *http.Request) { + t.Fatal("invalid file spec should fail before calling remote API") + })) + defer server.Close() + + err := runShortcut(t, server, "batch-commit", map[string]string{ + "branch": "master", + "message": "docs: update", + "files": "move:README.md:# hello", + "dry-run": "true", + }) + if err == nil { + t.Fatal("expected invalid file action error") + } +} + func TestRepoWatchersBuildsTimeRangeQuery(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { assertRequest(t, r, "GET", "/owner/repo/watchers.json") diff --git a/skills/gitlink-commit-quality/SKILL.md b/skills/gitlink-commit-quality/SKILL.md index 939261f..11a0c78 100644 --- a/skills/gitlink-commit-quality/SKILL.md +++ b/skills/gitlink-commit-quality/SKILL.md @@ -59,10 +59,7 @@ metadata: ```bash # 获取 PR 关联的提交列表 -gitlink-cli pr +diff --id --owner --repo --format json - -# 或通过 Raw API 获取提交详情 -gitlink-cli api GET /:owner/:repo/pulls/:pr_id/commits --format json +gitlink-cli pr +commits --id --owner --repo --format json ``` ### Commit Message 质量检查清单 diff --git a/skills/gitlink-gatekeeper/REFERENCE.md b/skills/gitlink-gatekeeper/REFERENCE.md index b19a54f..b31023a 100644 --- a/skills/gitlink-gatekeeper/REFERENCE.md +++ b/skills/gitlink-gatekeeper/REFERENCE.md @@ -223,7 +223,7 @@ def decide(total, hard_gate_failed, thresholds): | PR 元信息 | 标题/描述/作者/关联 | `gitlink-cli pr +view -i --format json` | `+view`,`-i`/`--id` | | 变更文件 | 文件路径列表 | `gitlink-cli pr +files -i --format json` | `+files` | | Diff | 变更内容供 AI 审查 | `gitlink-cli pr +diff -i --format json` | `+diff`;当前实现与 `+files` 命中同一 `/pulls/:id/files` 端点 | -| commits | commit 列表 | `gitlink-cli api GET /:owner/:repo/pulls/:id/commits --format json` | Raw API(无对应 shortcut) | +| commits | commit 列表 | `gitlink-cli pr +commits -i --format json` | `+commits` | | CI 状态 | 构建结果 | `gitlink-cli ci +builds --format json` | `+builds`(`-p`/`-l` 分页) | | 回写评论 | 评分卡 | `gitlink-cli pr +comment -i -b ""` | `+comment` 底层走 issue journals(评论流);评审记录形式用 `pr +review -i -s common -c "..."`(走 reviews 端点,payload 字段是 `content`/`status`,status 取 `common`/`approved`/`rejected`)。评分卡作为建议性回写,二者均用 `common` | | 创建标签 | 裁决标签 | `gitlink-cli label +create -n "" -c "#RRGGBB"` | `+create`(本作品子题一新增;`label +list/+update/+delete` 同组) | diff --git a/skills/gitlink-gatekeeper/SKILL.md b/skills/gitlink-gatekeeper/SKILL.md index ce31ddb..1a3de6d 100644 --- a/skills/gitlink-gatekeeper/SKILL.md +++ b/skills/gitlink-gatekeeper/SKILL.md @@ -99,17 +99,17 @@ behavior: | PR 元信息 | 标题/描述/作者/关联 issue | `gitlink-cli pr +view -i --format json` | | 变更文件 | 文件路径列表 | `gitlink-cli pr +files -i --format json` | | Diff | 变更内容供 AI 审查 | `gitlink-cli pr +diff -i --format json` | -| commits | commit 列表(消息供 commit_quality) | `gitlink-cli api GET /:owner/:repo/pulls/:id/commits --format json` | +| commits | commit 列表(消息供 commit_quality) | `gitlink-cli pr +commits -i --format json` | | CI 状态 | 构建结果 | `gitlink-cli ci +builds --format json` | -> 实测注意:`pr +files` 与 `pr +diff` 底层都打 `/pulls/:id/files`——`+files` 取路径列表,`+diff` 取含 patch 的同一份数据,按需取用即可。无 `pr +commits` 快捷命令,commit 列表只能走 Raw API。CI 通过/失败需从 builds 返回的 `status` 字段判断;**无 build 记录时按「CI 未知」处理**(见 §3.5)。 +> 实测注意:`pr +files` 与 `pr +diff` 底层都打 `/pulls/:id/files`——`+files` 取路径列表,`+diff` 取含 patch 的同一份数据,按需取用即可。commit 列表使用 `pr +commits`。CI 通过/失败需从 builds 返回的 `status` 字段判断;**无 build 记录时按「CI 未知」处理**(见 §3.5)。 ```bash PR=42 gitlink-cli pr +view -i "$PR" --format json # title / body / 关联 issue gitlink-cli pr +files -i "$PR" --format json # changed files gitlink-cli pr +diff -i "$PR" --format json # diff(供 AI 审查) -gitlink-cli api GET /:owner/:repo/pulls/$PR/commits --format json +gitlink-cli pr +commits -i "$PR" --format json gitlink-cli ci +builds --format json ``` @@ -295,7 +295,7 @@ PR=42 gitlink-cli pr +view -i "$PR" --format json gitlink-cli pr +files -i "$PR" --format json gitlink-cli pr +diff -i "$PR" --format json -gitlink-cli api GET /:owner/:repo/pulls/$PR/commits --format json +gitlink-cli pr +commits -i "$PR" --format json gitlink-cli ci +builds --format json # → AI 产出发现 → 按 §4 评分 → §5 硬门禁 → §6 裁决 → §7 渲染评分卡 # → dry-run:仅把评分卡打印给用户,结尾提示「如需回写到 PR,请加 --apply」 diff --git a/skills/gitlink-gatekeeper/examples/decision-comment.md b/skills/gitlink-gatekeeper/examples/decision-comment.md index 41ae723..c7b311e 100644 --- a/skills/gitlink-gatekeeper/examples/decision-comment.md +++ b/skills/gitlink-gatekeeper/examples/decision-comment.md @@ -14,7 +14,7 @@ gitlink-cli pr +view -i 277 --owner Gitlink --repo forgeplus --format json gitlink-cli pr +files -i 277 --owner Gitlink --repo forgeplus --format json gitlink-cli pr +diff -i 277 --owner Gitlink --repo forgeplus --format json -gitlink-cli api GET /Gitlink/forgeplus/pulls/277/commits --format json +gitlink-cli pr +commits -i 277 --owner Gitlink --repo forgeplus --format json gitlink-cli ci +builds --owner Gitlink --repo forgeplus --format json ``` diff --git a/skills/gitlink-gatekeeper/examples/decision-pass.md b/skills/gitlink-gatekeeper/examples/decision-pass.md index e47001c..f148892 100644 --- a/skills/gitlink-gatekeeper/examples/decision-pass.md +++ b/skills/gitlink-gatekeeper/examples/decision-pass.md @@ -21,7 +21,7 @@ gitlink-cli pr +files -i 214 --owner Gitlink --repo forgeplus --format json gitlink-cli pr +diff -i 214 --owner Gitlink --repo forgeplus --format json # commits -gitlink-cli api GET /Gitlink/forgeplus/pulls/214/commits --format json +gitlink-cli pr +commits -i 214 --owner Gitlink --repo forgeplus --format json # CI 状态 gitlink-cli ci +builds --owner Gitlink --repo forgeplus --format json diff --git a/skills/gitlink-gatekeeper/examples/decision-request-changes.md b/skills/gitlink-gatekeeper/examples/decision-request-changes.md index 34d78d0..caeb56d 100644 --- a/skills/gitlink-gatekeeper/examples/decision-request-changes.md +++ b/skills/gitlink-gatekeeper/examples/decision-request-changes.md @@ -14,7 +14,7 @@ gitlink-cli pr +view -i 305 --owner Gitlink --repo forgeplus --format json gitlink-cli pr +files -i 305 --owner Gitlink --repo forgeplus --format json gitlink-cli pr +diff -i 305 --owner Gitlink --repo forgeplus --format json -gitlink-cli api GET /Gitlink/forgeplus/pulls/305/commits --format json +gitlink-cli pr +commits -i 305 --owner Gitlink --repo forgeplus --format json gitlink-cli ci +builds --owner Gitlink --repo forgeplus --format json ``` diff --git a/skills/gitlink-pr/SKILL.md b/skills/gitlink-pr/SKILL.md index d00091d..b33744d 100644 --- a/skills/gitlink-pr/SKILL.md +++ b/skills/gitlink-pr/SKILL.md @@ -27,6 +27,7 @@ metadata: | `pr +reopen` | 重开已关闭的 PR | 是 | | `pr +files` | 变更文件列表 | 否 | | `pr +diff` | 查看变更文件和 diff 内容 | 否 | +| `pr +commits` | 查看 PR 包含的提交列表 | 否 | | `pr +versions` | 查看 PR patchset/version 列表 | 否 | | `pr +version-diff` | 查看指定 patchset/version diff | 否 | | `pr +reviews` | 查看 PR 审查记录,支持状态过滤 | 否 | @@ -61,6 +62,9 @@ gitlink-cli pr +reopen --id 3 # 查看变更文件(含 diff 内容) gitlink-cli pr +files --id 3 +# 查看 PR 包含的提交列表 +gitlink-cli pr +commits --id 3 + # 查看 PR patchset/version 列表 gitlink-cli pr +versions --id 3 @@ -172,3 +176,10 @@ gitlink-cli api GET /v1/:owner/:repo/pulls/:id/versions/:version_id/diff - `pr +list` 的 `--state` 参数会映射为 GitLink API 的 `status` 筛选;需要查看所有 PR 时传 `--state all` - PR 状态值:`pull_request_status` 0=open, 1=merged, 2=closed - 关联已有 Issue 时,把 Issue 编号或 URL 写入 PR `--body`,或使用 `issue +comment` 留痕;不要用 Raw API 对 Issue 做不完整更新,否则可能清空 Issue 描述 + +## 参考文档 + +- [`references/gitlink-pr-commits.md`](references/gitlink-pr-commits.md) +- [`references/gitlink-pr-files.md`](references/gitlink-pr-files.md) +- [`references/gitlink-pr-review.md`](references/gitlink-pr-review.md) +- [`references/gitlink-pr-version-diff.md`](references/gitlink-pr-version-diff.md) diff --git a/skills/gitlink-pr/references/gitlink-pr-commits.md b/skills/gitlink-pr/references/gitlink-pr-commits.md new file mode 100644 index 0000000..c88c5e5 --- /dev/null +++ b/skills/gitlink-pr/references/gitlink-pr-commits.md @@ -0,0 +1,40 @@ +# pr +commits + +> **前置条件:** 先阅读 [`../../gitlink-shared/SKILL.md`](../../gitlink-shared/SKILL.md) 了解认证、全局参数和安全规则。 + +查看 Pull Request 包含的提交列表。该命令适合代码审查、commit message 质量检查、PR 门禁和 Release Notes 生成。 + +## 命令 + +```bash +# 查看 PR 提交列表 +gitlink-cli pr +commits --id 3 + +# 简写 +gitlink-cli pr +commits -i 3 + +# JSON 格式 +gitlink-cli pr +commits -i 3 --format json +``` + +## 参数 + +| 参数 | 必填 | 说明 | +|------|------|------| +| `--id, -i` | 是 | PR 序号(`pull_request_number`) | + +## API + +```text +GET /{owner}/{repo}/pulls/{number}/commits +``` + +## 注意事项 + +- 返回提交 SHA、提交消息、作者、提交者和时间信息。 +- 在 `gitlink-gatekeeper`、`gitlink-commit-quality` 等 Skill 中优先使用此 Shortcut,不再需要 Raw API。 + +## 参考 + +- [gitlink-pr](../SKILL.md) +- [gitlink-shared](../../gitlink-shared/SKILL.md) diff --git a/skills/gitlink-repo/SKILL.md b/skills/gitlink-repo/SKILL.md index 5d59705..fd4ab26 100644 --- a/skills/gitlink-repo/SKILL.md +++ b/skills/gitlink-repo/SKILL.md @@ -24,6 +24,12 @@ metadata: | `repo +info` | 仓库详情 | 否(公开项目) | | `repo +readme` | README 内容 | 否(公开项目) | | `repo +tree` | 仓库文件树 | 否(公开项目) | +| `repo +files` | 搜索仓库文件 | 否(公开项目) | +| `repo +commits` | 提交历史列表 | 否(公开项目) | +| `repo +commit-files` | 单个提交的变更文件 | 否(公开项目) | +| `repo +commit-diff` | 单个提交的 diff | 否(公开项目) | +| `repo +tags` | 仓库标签列表 | 否(公开项目) | +| `repo +tag` | 仓库标签详情 | 否(公开项目) | | `repo +languages` | 仓库语言统计 | 否(公开项目) | | `repo +contributors` | 仓库贡献者列表 | 否(公开项目) | | `repo +contributor-stats` | 贡献者代码行统计 | 否(公开项目) | @@ -34,6 +40,8 @@ metadata: | `repo +unfollow` | 取消关注仓库 | 是 | | `repo +like` | 点赞仓库 | 是 | | `repo +unlike` | 取消点赞仓库 | 是 | +| `repo +delete-tag` | 删除仓库标签,默认建议先 dry-run | 是 | +| `repo +batch-commit` | 多文件批量提交,默认建议先 dry-run | 是 | | `repo +create` | 创建仓库 | 是 | | `repo +fork` | Fork 仓库 | 是 | | `repo +delete` | 删除仓库 | 是 | @@ -57,6 +65,18 @@ gitlink-cli repo +tree --owner Gitlink --repo forgeplus --path src --ref main gitlink-cli repo +languages --owner Gitlink --repo forgeplus gitlink-cli repo +contributors --owner Gitlink --repo forgeplus +# 搜索文件和分析提交历史 +gitlink-cli repo +files --owner Gitlink --repo forgeplus --search README --ref master +gitlink-cli repo +commits --owner Gitlink --repo forgeplus --ref master --limit 20 +gitlink-cli repo +commit-files --owner Gitlink --repo forgeplus --sha +gitlink-cli repo +commit-files --owner Gitlink --repo forgeplus --sha --file src/main.go +gitlink-cli repo +commit-diff --owner Gitlink --repo forgeplus --sha + +# 查看标签和标签详情 +gitlink-cli repo +tags --owner Gitlink --repo forgeplus --name v1 --only-name true +gitlink-cli repo +tag --owner Gitlink --repo forgeplus --name v1.0.0 +gitlink-cli repo +delete-tag --owner Gitlink --repo forgeplus --name v1.0.0 --dry-run + # 查看代码统计 gitlink-cli repo +contributor-stats --owner Gitlink --repo forgeplus --ref master --pass-year 1 gitlink-cli repo +code-stats --owner Gitlink --repo forgeplus --ref master @@ -72,6 +92,16 @@ gitlink-cli repo +unfollow --owner Gitlink --repo forgeplus --project-id 123 gitlink-cli repo +like --owner Gitlink --repo forgeplus gitlink-cli repo +unlike --owner Gitlink --repo forgeplus --project-id 123 +# 预览多文件提交;真实写入前必须确认用户意图 +gitlink-cli repo +batch-commit --owner me --repo proj \ + --branch master --message "docs: update guide" \ + --files 'update:README.md:# Updated;create:docs/demo.md:# Demo' \ + --dry-run +gitlink-cli repo +batch-commit --owner me --repo proj \ + --branch master --message "docs: update guide" \ + --files 'update:README.md:# Updated;delete:old.md' \ + --yes + # 创建仓库 gitlink-cli repo +create --name my-project --description "项目描述" @@ -87,9 +117,6 @@ gitlink-cli repo +delete --owner myuser --repo old-project Shortcuts 未覆盖的仓库操作可用 Raw API: ```bash -# 获取提交列表 -gitlink-cli api GET /:owner/:repo/commits --query 'page=1&limit=20' - # 获取标签列表 gitlink-cli api GET /:owner/:repo/tags @@ -101,3 +128,13 @@ gitlink-cli api GET /:owner/:repo/raw/main/README.md - `repo +delete` 是不可逆操作,执行前必须确认用户意图 - 创建仓库默认为公开,使用 `--private true` 创建私有仓库 +- `repo +delete-tag` 会删除远端标签,Agent 必须先运行 `--dry-run` 并获得用户明确确认,再加 `--yes` +- `repo +batch-commit` 会修改仓库内容,Agent 必须先运行 `--dry-run` 并向用户展示 payload,再在用户明确确认后加 `--yes` +- `repo +batch-commit --files` 使用 `action:path[:content]`,多个操作用英文分号分隔;`delete` 不需要 content,`create/update` 需要 content + +## 参考文档 + +- [`references/gitlink-repo-tree.md`](references/gitlink-repo-tree.md) +- [`references/gitlink-repo-code-history.md`](references/gitlink-repo-code-history.md) +- [`references/gitlink-repo-tags.md`](references/gitlink-repo-tags.md) +- [`references/gitlink-repo-batch-commit.md`](references/gitlink-repo-batch-commit.md) diff --git a/skills/gitlink-repo/references/gitlink-repo-batch-commit.md b/skills/gitlink-repo/references/gitlink-repo-batch-commit.md new file mode 100644 index 0000000..bef9bb3 --- /dev/null +++ b/skills/gitlink-repo/references/gitlink-repo-batch-commit.md @@ -0,0 +1,76 @@ +# repo +batch-commit + +> **前置条件:** 先阅读 [`../../gitlink-shared/SKILL.md`](../../gitlink-shared/SKILL.md) 了解认证、全局参数和安全规则。 + +在一次提交中创建、更新或删除多个文件。该命令封装 GitLink 的 batch contents API,适合 Agent 生成文档、配置或示例文件后集中提交到分支。 + +## 命令 + +```bash +# 预览多文件提交 +gitlink-cli repo +batch-commit --owner someone --repo myrepo \ + --branch master \ + --message "docs: update guide" \ + --files 'update:README.md:# Updated;create:docs/demo.md:# Demo' \ + --dry-run + +# 确认执行多文件提交 +gitlink-cli repo +batch-commit --owner someone --repo myrepo \ + --branch master \ + --message "docs: update guide" \ + --files 'update:README.md:# Updated;delete:old.md' \ + --yes + +# 创建新分支并提交 +gitlink-cli repo +batch-commit --owner someone --repo myrepo \ + --branch master \ + --new-branch docs/update-guide \ + --message "docs: update guide" \ + --files 'create:docs/guide.md:# Guide' \ + --dry-run +``` + +## 参数 + +| 参数 | 必填 | 说明 | +|------|------|------| +| `--branch, -b` | 是 | 目标分支 | +| `--message, -m` | 是 | 提交信息 | +| `--files, -f` | 是 | 文件操作列表,格式见下文 | +| `--new-branch` | 否 | 创建并提交到新分支 | +| `--encoding` | 否 | 内容编码,`text` 或 `base64`,默认 `text` | +| `--author-name` | 否 | 作者名称 | +| `--author-email` | 否 | 作者邮箱 | +| `--committer-name` | 否 | 提交者名称 | +| `--committer-email` | 否 | 提交者邮箱 | +| `--dry-run` | 否 | 预览请求,不修改远端 | +| `--yes` | 否 | 确认执行远端写入 | + +## `--files` 格式 + +```text +action:path[:content][;action:path[:content]...] +``` + +支持的 `action`: + +- `create`:创建文件,必须提供 content。 +- `update`:更新文件,必须提供 content。 +- `delete`:删除文件,不需要 content。 + +## API + +```text +POST /v1/{owner}/{repo}/contents/batch +``` + +## 安全规则 + +- 真实写入前必须先运行 `--dry-run`。 +- 未传 `--yes` 时命令不会调用远端写入 API。 +- Agent 必须向用户展示 dry-run 中的 `payload`,确认文件路径、操作类型、分支和提交信息无误后再执行。 + +## 参考 + +- [gitlink-repo](../SKILL.md) +- [gitlink-shared](../../gitlink-shared/SKILL.md) diff --git a/skills/gitlink-repo/references/gitlink-repo-code-history.md b/skills/gitlink-repo/references/gitlink-repo-code-history.md new file mode 100644 index 0000000..0d5397a --- /dev/null +++ b/skills/gitlink-repo/references/gitlink-repo-code-history.md @@ -0,0 +1,57 @@ +# repo code history shortcuts + +> **前置条件:** 先阅读 [`../../gitlink-shared/SKILL.md`](../../gitlink-shared/SKILL.md) 了解认证、全局参数和安全规则。 + +用于搜索仓库文件、查看提交历史、查看单个提交的变更文件和 diff。适合代码审查、科研复现性分析、Release Notes 生成和项目健康度报告。 + +## 命令 + +```bash +# 搜索仓库文件 +gitlink-cli repo +files --owner someone --repo myrepo --search README --ref master + +# 查看提交历史 +gitlink-cli repo +commits --owner someone --repo myrepo --ref master --page 1 --limit 20 + +# 查看单个提交变更文件 +gitlink-cli repo +commit-files --owner someone --repo myrepo --sha + +# 按文件路径筛选单个提交变更 +gitlink-cli repo +commit-files --owner someone --repo myrepo --sha --file src/main.go + +# 查看单个提交 diff +gitlink-cli repo +commit-diff --owner someone --repo myrepo --sha +``` + +## 参数 + +| 命令 | 参数 | 必填 | 说明 | +|------|------|------|------| +| `repo +files` | `--search, -s` | 否 | 文件名或路径关键词 | +| `repo +files` | `--ref, -r` | 否 | 分支、标签或 Commit SHA | +| `repo +commits` | `--ref, -r` | 否 | 分支、标签或 Commit SHA,映射到 API `sha` | +| `repo +commits` | `--page, -p` | 否 | 页码,默认 `1` | +| `repo +commits` | `--limit, -l` | 否 | 每页数量,默认 `20` | +| `repo +commit-files` | `--sha, -s` | 是 | Commit SHA | +| `repo +commit-files` | `--file, -f` | 否 | 只查看指定文件路径的变更 | +| `repo +commit-diff` | `--sha, -s` | 是 | Commit SHA | + +## API + +```text +GET /{owner}/{repo}/files +GET /v1/{owner}/{repo}/commits +GET /v1/{owner}/{repo}/commits/{sha}/files +GET /v1/{owner}/{repo}/commits/{sha}/diff +``` + +## 注意事项 + +- AI Agent 场景建议始终使用 `--format json`。 +- `repo +commit-files --file` 会使用 `filepath` 查询参数;不传 `--file` 时使用分页参数。 +- `repo +commits --ref` 支持分支、标签或 Commit SHA。 + +## 参考 + +- [gitlink-repo](../SKILL.md) +- [gitlink-shared](../../gitlink-shared/SKILL.md) diff --git a/skills/gitlink-repo/references/gitlink-repo-tags.md b/skills/gitlink-repo/references/gitlink-repo-tags.md new file mode 100644 index 0000000..1c7f030 --- /dev/null +++ b/skills/gitlink-repo/references/gitlink-repo-tags.md @@ -0,0 +1,56 @@ +# repo tag shortcuts + +> **前置条件:** 先阅读 [`../../gitlink-shared/SKILL.md`](../../gitlink-shared/SKILL.md) 了解认证、全局参数和安全规则。 + +用于查看仓库标签列表、标签详情,以及在明确确认后删除远端标签。 + +## 命令 + +```bash +# 分页列出标签 +gitlink-cli repo +tags --owner someone --repo myrepo --page 1 --limit 20 + +# 按名称搜索标签,仅返回名称 +gitlink-cli repo +tags --owner someone --repo myrepo --name v1 --only-name true + +# 查看标签详情 +gitlink-cli repo +tag --owner someone --repo myrepo --name v1.0.0 + +# 预览删除标签 +gitlink-cli repo +delete-tag --owner someone --repo myrepo --name v1.0.0 --dry-run + +# 确认删除标签 +gitlink-cli repo +delete-tag --owner someone --repo myrepo --name v1.0.0 --yes +``` + +## 参数 + +| 命令 | 参数 | 必填 | 说明 | +|------|------|------|------| +| `repo +tags` | `--page, -p` | 否 | 页码,默认 `1` | +| `repo +tags` | `--limit, -l` | 否 | 每页数量,默认 `20` | +| `repo +tags` | `--name, -n` | 否 | 标签搜索关键词 | +| `repo +tags` | `--only-name` | 否 | 只返回标签名称,通常与 `--name` 搭配 | +| `repo +tag` | `--name, -n` | 是 | 标签名称 | +| `repo +delete-tag` | `--name, -n` | 是 | 标签名称 | +| `repo +delete-tag` | `--dry-run` | 否 | 预览删除请求,不修改远端 | +| `repo +delete-tag` | `--yes` | 否 | 确认执行删除 | + +## API + +```text +GET /v1/{owner}/{repo}/tags +GET /{owner}/{repo}/tags +GET /v1/{owner}/{repo}/tags/{name} +DELETE /v1/{owner}/{repo}/tags/{tag} +``` + +## 注意事项 + +- `repo +delete-tag` 是破坏性操作,Agent 必须先执行 `--dry-run`,展示将删除的标签,再等待用户明确确认。 +- 不传 `--name`/`--only-name` 时,`repo +tags` 使用 v1 分页标签接口;传搜索条件时使用支持名称筛选的标签接口。 + +## 参考 + +- [gitlink-repo](../SKILL.md) +- [gitlink-shared](../../gitlink-shared/SKILL.md)