diff --git a/README.md b/README.md index b13f1ef..2e933b4 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 04499a3..744c722 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 aee9560..aa1efc9 100644 --- a/internal/i18n/locales/en-US.json +++ b/internal/i18n/locales/en-US.json @@ -59,6 +59,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", @@ -87,11 +88,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", @@ -224,9 +233,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 6fb482c..e8c39b7 100644 --- a/internal/i18n/locales/zh-CN.json +++ b/internal/i18n/locales/zh-CN.json @@ -59,6 +59,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": "列出拉取请求中的变更文件", @@ -87,11 +88,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 命令行工具", @@ -224,9 +233,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 3dfa44c..9ea091b 100644 --- a/shortcuts/pr/pr.go +++ b/shortcuts/pr/pr.go @@ -334,6 +334,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 b9a8ef6..8725f8f 100644 --- a/shortcuts/pr/pr_test.go +++ b/shortcuts/pr/pr_test.go @@ -488,6 +488,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 54eeca9..4292bbd 100644 --- a/shortcuts/repo/repo.go +++ b/shortcuts/repo/repo.go @@ -109,31 +109,89 @@ func Shortcuts(translators ...*i18n.Translator) []*common.Shortcut { }, }, { - Name: "raw", - Description: tr.T("cmd.repo.raw.short"), + Name: "files", + Description: tr.T("cmd.repo.files.short"), Flags: []common.Flag{ - {Name: "path", Short: "p", Usage: tr.T("flag.repo.raw.path"), Required: true}, - {Name: "ref", Short: "r", Usage: tr.T("flag.repo.raw.ref"), Default: "master"}, + {Name: "search", Short: "s", Usage: tr.T("flag.repo.files.search")}, + {Name: "ref", Short: "r", Usage: tr.T("flag.repo.ref")}, }, - Run: runRawFile, + Run: runFiles, }, { - Name: "file-exists", - Description: tr.T("cmd.repo.file_exists.short"), + Name: "commits", + Description: tr.T("cmd.repo.commits.short"), Flags: []common.Flag{ - {Name: "path", Short: "p", Usage: tr.T("flag.repo.file_exists.path"), Required: true}, - {Name: "ref", Short: "r", Usage: tr.T("flag.repo.raw.ref"), Default: "master"}, + {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: runFileExists, + Run: runCommits, }, { - Name: "manifest", - Description: tr.T("cmd.repo.manifest.short"), + Name: "commit-files", + Description: tr.T("cmd.repo.commit_files.short"), Flags: []common.Flag{ - {Name: "kind", Short: "k", Usage: tr.T("flag.repo.manifest.kind"), Required: true}, - {Name: "ref", Short: "r", Usage: tr.T("flag.repo.raw.ref"), Default: "master"}, + {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: runManifest, + 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", @@ -283,14 +341,6 @@ func Shortcuts(translators ...*i18n.Translator) []*common.Shortcut { } } -var manifestPathsByKind = map[string][]string{ - "go": {"go.mod"}, - "node": {"package.json"}, - "python": {"requirements.txt", "pyproject.toml", "Pipfile", "setup.py"}, - "rust": {"Cargo.toml"}, - "java": {"pom.xml", "build.gradle", "build.gradle.kts"}, -} - func shortcutTranslator(translators ...*i18n.Translator) *i18n.Translator { if len(translators) > 0 && translators[0] != nil { return translators[0] @@ -298,77 +348,171 @@ func shortcutTranslator(translators ...*i18n.Translator) *i18n.Translator { return i18n.Default() } -func runRawFile(ctx *common.RuntimeContext) error { +func runFiles(ctx *common.RuntimeContext) error { if err := ctx.ResolveOwnerRepo(); err != nil { return err } - path, err := requireRepoPathArg(ctx, "path") - if err != nil { - return err - } - ref := repoRef(ctx.Arg("ref")) - env, err := ctx.CallAPI("GET", rawFilePath(ctx, ref, path), nil) + 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 runFileExists(ctx *common.RuntimeContext) error { +func runCommits(ctx *common.RuntimeContext) error { if err := ctx.ResolveOwnerRepo(); err != nil { return err } - path, err := requireRepoPathArg(ctx, "path") + 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 } - ref := repoRef(ctx.Arg("ref")) - exists, entry, err := repoFileExists(ctx, path, ref) - if err != nil { - return err - } - return ctx.OutputData(map[string]interface{}{ - "repository": fmt.Sprintf("%s/%s", ctx.Owner, ctx.Repo), - "ref": ref, - "path": path, - "exists": exists, - "entry": entry, - }) + return ctx.Output(env) } -func runManifest(ctx *common.RuntimeContext) error { +func runCommitFiles(ctx *common.RuntimeContext) error { if err := ctx.ResolveOwnerRepo(); err != nil { return err } - kind := strings.ToLower(strings.TrimSpace(ctx.Arg("kind"))) - paths, ok := manifestPathsByKind[kind] - if !ok { - return fmt.Errorf("invalid --kind %q: use go, node, python, rust, or java", ctx.Arg("kind")) + sha, err := ctx.RequireArg("sha") + if err != nil { + return err } - ref := repoRef(ctx.Arg("ref")) - checked := make([]string, 0, len(paths)) - for _, path := range paths { - checked = append(checked, path) - exists, _, err := repoFileExists(ctx, path, ref) - if err != nil { - return err - } - if !exists { - continue - } - env, err := ctx.CallAPI("GET", rawFilePath(ctx, ref, path), nil) + 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) } - return ctx.OutputData(map[string]interface{}{ - "repository": fmt.Sprintf("%s/%s", ctx.Owner, ctx.Repo), - "ref": ref, - "kind": kind, - "found": false, - "checked": checked, - }) + 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 { @@ -590,114 +734,101 @@ func setRepoQueryIfPresent(q url.Values, key, value string) { } } -func repoRef(ref string) string { - if ref := strings.TrimSpace(ref); ref != "" { - return ref +func firstRepoValue(value, fallback string) string { + if value := strings.TrimSpace(value); value != "" { + return value } - return "master" + return fallback } -func requireRepoPathArg(ctx *common.RuntimeContext, name string) (string, error) { - path, err := ctx.RequireArg(name) +func batchCommitPayload(ctx *common.RuntimeContext) (map[string]interface{}, error) { + branch, err := ctx.RequireArg("branch") if err != nil { - return "", err + return nil, err } - path = cleanRepoFilePath(path) - if path == "" { - return "", fmt.Errorf("--%s must not be empty", name) + message, err := ctx.RequireArg("message") + if err != nil { + return nil, err } - if hasParentPathSegment(path) { - return "", fmt.Errorf("--%s must not contain '..'", name) + rawFiles, err := ctx.RequireArg("files") + if err != nil { + return nil, err } - return path, nil + 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 cleanRepoFilePath(path string) string { - path = strings.TrimSpace(path) - path = strings.TrimPrefix(path, "/") - return strings.Trim(path, "/") -} - -func rawFilePath(ctx *common.RuntimeContext, ref, path string) string { - parts := []string{ctx.RepoPath(), "raw", url.PathEscape(ref)} - for _, part := range strings.Split(cleanRepoFilePath(path), "/") { - if part == "" { +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 = append(parts, url.PathEscape(part)) - } - return strings.Join(parts, "/") -} - -func hasParentPathSegment(path string) bool { - for _, part := range strings.Split(cleanRepoFilePath(path), "/") { - if part == ".." { - return true + parts := strings.SplitN(spec, ":", 3) + if len(parts) < 2 { + return nil, fmt.Errorf("invalid --files item %q: use action:path[:content]", spec) } - } - return false -} - -func repoFileExists(ctx *common.RuntimeContext, path, ref string) (bool, map[string]interface{}, error) { - q := url.Values{} - q.Set("filepath", path) - q.Set("ref", ref) - env, err := ctx.CallAPIWithQuery("GET", ctx.RepoPath()+"/sub_entries", q) - if err != nil { - return false, nil, err - } - entry := findRepoSubEntry(env.Data, path) - return entry != nil, entry, nil -} - -func findRepoSubEntry(data interface{}, path string) map[string]interface{} { - target := lastRepoPathPart(path) - if entry, ok := data.(map[string]interface{}); ok { - if matchesRepoEntry(entry, target, path) { - return entry + 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) } - for _, key := range []string{"entries", "sub_entries", "files", "data"} { - if found := findRepoSubEntryList(entry[key], target, path); found != nil { - return found + 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) } - return findRepoSubEntryList(data, target, path) + if len(files) == 0 { + return nil, fmt.Errorf("--files must include at least one file operation") + } + return files, nil } -func findRepoSubEntryList(data interface{}, target, path string) map[string]interface{} { - entries, ok := data.([]interface{}) - if !ok { - return nil +func isBatchFileAction(action string) bool { + switch action { + case "create", "update", "delete": + return true + default: + return false } - for _, raw := range entries { - entry, ok := raw.(map[string]interface{}) - if !ok { - continue - } - if matchesRepoEntry(entry, target, path) { - return entry - } - } - return nil } -func matchesRepoEntry(entry map[string]interface{}, target, path string) bool { - for _, key := range []string{"name", "filename", "path", "filepath"} { - value, _ := entry[key].(string) - if value == target || cleanRepoFilePath(value) == path { - return true - } +func setRepoPayloadIfPresent(payload map[string]interface{}, key, value string) { + if value := strings.TrimSpace(value); value != "" { + payload[key] = value } - return false -} - -func lastRepoPathPart(path string) string { - parts := strings.Split(cleanRepoFilePath(path), "/") - if len(parts) == 0 { - return "" - } - return parts[len(parts)-1] } func parseOptionalRepoNonNegativeInt(value, name string) (int, bool, error) { diff --git a/shortcuts/repo/repo_test.go b/shortcuts/repo/repo_test.go index 5fd5c1a..f4de653 100644 --- a/shortcuts/repo/repo_test.go +++ b/shortcuts/repo/repo_test.go @@ -58,6 +58,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) { @@ -473,6 +482,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 b36e665..19a0d28 100644 --- a/skills/gitlink-gatekeeper/REFERENCE.md +++ b/skills/gitlink-gatekeeper/REFERENCE.md @@ -234,7 +234,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 f59a3ba..1a3de6d 100644 --- a/skills/gitlink-gatekeeper/SKILL.md +++ b/skills/gitlink-gatekeeper/SKILL.md @@ -37,7 +37,7 @@ gitlink-gatekeeper 是一个**可复现的 PR 合并门禁**:团队把合并 | 阶段 | 操作 | AI Agent 角色 | |------|------|--------------| | ① 加载策略 | 读 `gatekeeper.yaml`,找不到则回退内置默认策略 | 解析 / 校验 / 回退 | -| ② 采集上下文 | 拉 PR 元信息、分支可合并性、变更文件、diff、commits、CI 状态 | 执行 CLI 命令采集数据 | +| ② 采集上下文 | 拉 PR 元信息、变更文件、diff、commits、CI 状态 | 执行 CLI 命令采集数据 | | ③ 产出发现 | 逐文件审查,按 severity 分级标记问题 | AI 分析,输出发现列表 | | ④ 逐维评分 | 五维各算 `0..weight` 得分,相加得 `total` | 确定性计算(非主观) | | ⑤ 硬门禁 | 逐项判定 `hard_gates`,命中即拦截 | 布尔判定 | @@ -96,22 +96,20 @@ behavior: | 步骤 | 数据 | 命令 | |------|------|------| -| 聚合上下文 | 仓库、PR、文件、Review、Issue、标签 | `gitlink-cli workflow +review-context --number --format json` | | PR 元信息 | 标题/描述/作者/关联 issue | `gitlink-cli pr +view -i --format json` | -| 合并准备状态 | 源/目标分支是否可合并 | `gitlink-cli pr +check-can-merge --head --base --dry-run`,确认后 `--yes` | | 变更文件 | 文件路径列表 | `gitlink-cli pr +files -i --format json` | | Diff | 变更内容供 AI 审查 | `gitlink-cli pr +diff -i --format json` | +| commits | commit 列表(消息供 commit_quality) | `gitlink-cli pr +commits -i --format json` | | CI 状态 | 构建结果 | `gitlink-cli ci +builds --format json` | -> 实测注意:优先使用 `workflow +review-context` 获取聚合上下文;如需更细的 diff 再补充 `pr +diff`。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 workflow +review-context --number "$PR" --format json gitlink-cli pr +view -i "$PR" --format json # title / body / 关联 issue -gitlink-cli pr +check-can-merge --head feature/pr --base master --dry-run gitlink-cli pr +files -i "$PR" --format json # changed files gitlink-cli pr +diff -i "$PR" --format json # diff(供 AI 审查) +gitlink-cli pr +commits -i "$PR" --format json gitlink-cli ci +builds --format json ``` @@ -295,10 +293,9 @@ gitlink-cli api POST /:owner/:repo/issues/$ISSUE_ID --body '{ # 在目标仓库目录下,对 PR #42 跑门禁,仅预览评分卡 PR=42 gitlink-cli pr +view -i "$PR" --format json -gitlink-cli pr +check-can-merge --head feature/pr --base master --dry-run gitlink-cli pr +files -i "$PR" --format json gitlink-cli pr +diff -i "$PR" --format json -gitlink-cli workflow +review-context --number "$PR" --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 b415515..b33744d 100644 --- a/skills/gitlink-pr/SKILL.md +++ b/skills/gitlink-pr/SKILL.md @@ -1,7 +1,7 @@ --- name: gitlink-pr version: 1.0.0 -description: "Pull Request 管理:创建、查看、合并、关闭 PR,查看变更文件、Diff、patchset/version、审查和 Review 评论。当用户需要操作 GitLink PR 时触发。" +description: "Pull Request 管理:创建、查看、合并、关闭 PR,查看变更文件、Diff 和 patchset/version。当用户需要操作 GitLink PR 时触发。" metadata: requires: bins: ["gitlink-cli"] @@ -22,21 +22,17 @@ metadata: | `pr +list` | PR 列表 | 否(公开项目) | | `pr +create` | 创建 PR | 是 | | `pr +view` | PR 详情 | 否(公开项目) | -| `pr +checkout` | 拉取并切换到 PR 分支 | 否(公开项目,本地需要 git) | | `pr +merge` | 合并 PR | 是 | | `pr +refuse` | 拒绝并关闭 PR | 是 | | `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 审查记录,支持状态过滤 | 否 | | `pr +review` | 创建 PR 审查(支持 dry-run 预览) | 是 | -| `pr +review-comments` | 查看 PR Review 行评论 | 否 | -| `pr +review-comment` | 创建 PR Review 行评论(支持 dry-run 预览) | 是 | -| `pr +review-comment-update` | 更新 PR Review 行评论(支持 dry-run 预览) | 是 | -| `pr +review-comment-delete` | 删除 PR Review 行评论(支持 dry-run 预览) | 是 | -| `pr +comment` | 给 PR 添加普通评论 | 是 | +| `pr +comment` | 给 PR 添加评论 | 是 | ## 使用示例 @@ -53,10 +49,6 @@ 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 @@ -70,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 @@ -85,17 +80,7 @@ gitlink-cli pr +reviews --id 3 --status approved gitlink-cli pr +review --id 3 --status approved --content "LGTM, ready to merge" gitlink-cli pr +review --id 3 --status approved --content "LGTM" --dry-run -# 查看 PR Review 行评论 -gitlink-cli pr +review-comments --id 3 --state opened --path README.md - -# 创建 PR Review 行评论(需要 review-id、commit、line-code 和 path,先 dry-run) -gitlink-cli pr +review-comment --id 3 --review-id 10 --commit abc123 --line-code abc123_0_10 --path README.md --note "这里需要修改" --type problem --dry-run - -# 更新/解决/删除 PR Review 行评论(先 dry-run) -gitlink-cli pr +review-comment-update --id 3 --comment-id 200 --state resolved --dry-run -gitlink-cli pr +review-comment-delete --id 3 --comment-id 200 --dry-run - -# 给 PR 添加普通评论 +# 给 PR 添加评论 gitlink-cli pr +comment --id 3 --body "LGTM, ready to merge" ``` @@ -167,8 +152,6 @@ gitlink-cli api POST /v1/:owner/:repo/pulls/:id/reviews --body '{"content":"LGTM gitlink-cli api GET /v1/:owner/:repo/pulls/:id/reviews gitlink-cli api GET /v1/:owner/:repo/pulls/:id/reviews?status=approved -# PR Review 评论接口已由 pr +review-comments / +review-comment / +review-comment-update / +review-comment-delete 覆盖。 - # 获取可用分支 gitlink-cli api GET /:owner/:repo/pulls/get_branches @@ -194,13 +177,9 @@ gitlink-cli api GET /v1/:owner/:repo/pulls/:id/versions/:version_id/diff - PR 状态值:`pull_request_status` 0=open, 1=merged, 2=closed - 关联已有 Issue 时,把 Issue 编号或 URL 写入 PR `--body`,或使用 `issue +comment` 留痕;不要用 Raw API 对 Issue 做不完整更新,否则可能清空 Issue 描述 +## 参考文档 -## PR Review 评论注意事项 - -- `pr +review-comment` 面向代码行级 Review 评论,不等同于 `pr +comment` 普通评论。 -- `--id` 是网页 URL `/pulls/N` 中的 PR 序号。 -- `--review-id` 可通过 `pr +reviews --id N` 获取。 -- `--line-code`、`--commit`、`--path` 应来自 GitLink diff / review 上下文;不确定时先用 `pr +files`、`pr +versions`、`pr +version-diff` 获取上下文。 -- `--type comment` 表示普通行评论,`--type problem` 表示需要回应的问题评论。 -- `--state opened|resolved|disabled` 用于筛选或更新 Review 评论状态。 -- 创建、更新、删除 Review 评论前必须先使用 `--dry-run` 向用户展示请求体。 +- [`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 df2572e..fd4ab26 100644 --- a/skills/gitlink-repo/SKILL.md +++ b/skills/gitlink-repo/SKILL.md @@ -1,7 +1,7 @@ --- name: gitlink-repo -version: 2.0.0 -description: "仓库管理:创建、查看、Fork、删除仓库,查看 README、文件树、原始文件、依赖清单、语言统计、贡献者、关注者,并执行关注/点赞等互动操作。当用户需要操作或分析 GitLink 仓库时触发。" +version: 1.0.0 +description: "仓库管理:创建、查看、Fork、删除仓库,查看 README、语言统计、贡献者、关注者,并执行关注/点赞等互动操作。当用户需要操作或分析 GitLink 仓库时触发。" metadata: requires: bins: ["gitlink-cli"] @@ -24,9 +24,12 @@ metadata: | `repo +info` | 仓库详情 | 否(公开项目) | | `repo +readme` | README 内容 | 否(公开项目) | | `repo +tree` | 仓库文件树 | 否(公开项目) | -| `repo +raw` | 读取原始文件内容 | 否(公开项目) | -| `repo +file-exists` | 检查文件是否存在 | 否(公开项目) | -| `repo +manifest` | 读取常见依赖清单 | 否(公开项目) | +| `repo +files` | 搜索仓库文件 | 否(公开项目) | +| `repo +commits` | 提交历史列表 | 否(公开项目) | +| `repo +commit-files` | 单个提交的变更文件 | 否(公开项目) | +| `repo +commit-diff` | 单个提交的 diff | 否(公开项目) | +| `repo +tags` | 仓库标签列表 | 否(公开项目) | +| `repo +tag` | 仓库标签详情 | 否(公开项目) | | `repo +languages` | 仓库语言统计 | 否(公开项目) | | `repo +contributors` | 仓库贡献者列表 | 否(公开项目) | | `repo +contributor-stats` | 贡献者代码行统计 | 否(公开项目) | @@ -37,11 +40,10 @@ metadata: | `repo +unfollow` | 取消关注仓库 | 是 | | `repo +like` | 点赞仓库 | 是 | | `repo +unlike` | 取消点赞仓库 | 是 | +| `repo +delete-tag` | 删除仓库标签,默认建议先 dry-run | 是 | +| `repo +batch-commit` | 多文件批量提交,默认建议先 dry-run | 是 | | `repo +create` | 创建仓库 | 是 | | `repo +fork` | Fork 仓库 | 是 | -| `repo +transfer-orgs` | 查看可接收仓库转移的组织 | 是 | -| `repo +transfer` | 发起仓库转移请求 | 是 | -| `repo +transfer-cancel` | 取消待处理的仓库转移 | 是 | | `repo +delete` | 删除仓库 | 是 | ## 使用示例 @@ -60,12 +62,21 @@ gitlink-cli repo +list --user zhangsan # 查看文件树、语言占比和贡献者 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 +raw --owner Gitlink --repo forgeplus --path LICENSE --ref master -gitlink-cli repo +file-exists --owner Gitlink --repo forgeplus --path package.json --ref master -gitlink-cli repo +manifest --owner Gitlink --repo forgeplus --kind go --ref master 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 @@ -81,58 +92,49 @@ 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 "项目描述" # Fork 仓库 gitlink-cli repo +fork --owner Gitlink --repo forgeplus -# 查看仓库可转移到哪些组织 -gitlink-cli repo +transfer-orgs --owner Gitlink --repo forgeplus - -# 预览仓库转移请求,不修改线上数据 -gitlink-cli repo +transfer --owner Gitlink --repo forgeplus --target-owner my-org --dry-run - -# 确认后发起仓库转移 -gitlink-cli repo +transfer --owner Gitlink --repo forgeplus --target-owner my-org --yes - -# 取消待处理的仓库转移 -gitlink-cli repo +transfer-cancel --owner Gitlink --repo forgeplus --dry-run -gitlink-cli repo +transfer-cancel --owner Gitlink --repo forgeplus --yes - # 删除仓库(⚠️ 危险操作) gitlink-cli repo +delete --owner myuser --repo old-project ``` -## Repository Units - -Use these shortcuts to inspect or update the repository navigation modules shown in GitLink: - -```bash -gitlink-cli repo +units --owner Gitlink --repo forgeplus -gitlink-cli repo +set-units --owner Gitlink --repo forgeplus --units code,issues,pulls,wiki -``` - -Allowed unit values: `code`, `issues`, `pulls`, `devops`, `versions`, `wiki`, `services`, `resources`. - ## Raw API 补充 Shortcuts 未覆盖的仓库操作可用 Raw API: ```bash -# 获取提交列表 -gitlink-cli api GET /:owner/:repo/commits --query 'page=1&limit=20' - # 获取标签列表 gitlink-cli api GET /:owner/:repo/tags +# 获取文件内容 +gitlink-cli api GET /:owner/:repo/raw/main/README.md ``` ## 注意事项 - `repo +delete` 是不可逆操作,执行前必须确认用户意图 -- `repo +transfer` 会改变仓库所有者,只有显式传入 `--yes` 才会真正发起;执行前先使用 `repo +transfer-orgs` 确认可转移目标,并用 `--dry-run` 预览请求 -- `repo +transfer-cancel` 只用于取消已发起且未处理的转移申请,只有显式传入 `--yes` 才会真正执行,建议先用 `--dry-run` 预览 - 创建仓库默认为公开,使用 `--private true` 创建私有仓库 -- `repo +raw` 会读取指定 `--ref` 下的文件内容,默认 `master` -- `repo +manifest --kind` 支持 `go`、`node`、`python`、`rust`、`java` +- `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)