Merge PR #314: feat(repo,pr): add code history and batch file shortcuts

# Conflicts:
#	shortcuts/repo/repo.go
#	skills/gitlink-gatekeeper/SKILL.md
#	skills/gitlink-pr/SKILL.md
#	skills/gitlink-repo/SKILL.md
This commit is contained in:
wbtiger 2026-07-14 22:50:09 +08:00
commit a6b2841748
21 changed files with 1071 additions and 245 deletions

View File

@ -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 <commit_sha>
gitlink-cli repo +commit-diff --owner Gitlink --repo forgeplus --sha <commit_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

View File

@ -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 <commit_sha>
gitlink-cli repo +commit-diff --owner Gitlink --repo forgeplus --sha <commit_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

View File

@ -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
```

View File

@ -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",

View File

@ -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": "搜索关键词",

View File

@ -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"),

View File

@ -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) {

View File

@ -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) {

View File

@ -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")

View File

@ -59,10 +59,7 @@ metadata:
```bash
# 获取 PR 关联的提交列表
gitlink-cli pr +diff --id <pr_id> --owner <owner> --repo <repo> --format json
# 或通过 Raw API 获取提交详情
gitlink-cli api GET /:owner/:repo/pulls/:pr_id/commits --format json
gitlink-cli pr +commits --id <pr_id> --owner <owner> --repo <repo> --format json
```
### Commit Message 质量检查清单

View File

@ -234,7 +234,7 @@ def decide(total, hard_gate_failed, thresholds):
| PR 元信息 | 标题/描述/作者/关联 | `gitlink-cli pr +view -i <id> --format json` | `+view``-i`/`--id` |
| 变更文件 | 文件路径列表 | `gitlink-cli pr +files -i <id> --format json` | `+files` |
| Diff | 变更内容供 AI 审查 | `gitlink-cli pr +diff -i <id> --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 <id> --format json` | `+commits` |
| CI 状态 | 构建结果 | `gitlink-cli ci +builds --format json` | `+builds``-p`/`-l` 分页) |
| 回写评论 | 评分卡 | `gitlink-cli pr +comment -i <id> -b "<scorecard>"` | `+comment` 底层走 issue journals评论流评审记录形式用 `pr +review -i <id> -s common -c "..."`(走 reviews 端点payload 字段是 `content`/`status`status 取 `common`/`approved`/`rejected`)。评分卡作为建议性回写,二者均用 `common` |
| 创建标签 | 裁决标签 | `gitlink-cli label +create -n "<name>" -c "#RRGGBB"` | `+create`(本作品子题一新增;`label +list/+update/+delete` 同组) |

View File

@ -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 <id> --format json` |
| PR 元信息 | 标题/描述/作者/关联 issue | `gitlink-cli pr +view -i <id> --format json` |
| 合并准备状态 | 源/目标分支是否可合并 | `gitlink-cli pr +check-can-merge --head <head> --base <base> --dry-run`,确认后 `--yes` |
| 变更文件 | 文件路径列表 | `gitlink-cli pr +files -i <id> --format json` |
| Diff | 变更内容供 AI 审查 | `gitlink-cli pr +diff -i <id> --format json` |
| commits | commit 列表(消息供 commit_quality | `gitlink-cli pr +commits -i <id> --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」

View File

@ -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
```

View File

@ -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

View File

@ -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
```

View File

@ -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)

View File

@ -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)

View File

@ -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 <commit_sha>
gitlink-cli repo +commit-files --owner Gitlink --repo forgeplus --sha <commit_sha> --file src/main.go
gitlink-cli repo +commit-diff --owner Gitlink --repo forgeplus --sha <commit_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)

View File

@ -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)

View File

@ -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 <commit_sha>
# 按文件路径筛选单个提交变更
gitlink-cli repo +commit-files --owner someone --repo myrepo --sha <commit_sha> --file src/main.go
# 查看单个提交 diff
gitlink-cli repo +commit-diff --owner someone --repo myrepo --sha <commit_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)

View File

@ -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)