feat(repo,pr): add code history file shortcuts

This commit is contained in:
NeeNe 2026-07-02 12:25:38 +08:00
parent 71ca2bb683
commit e20f80e26c
21 changed files with 1129 additions and 15 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

@ -54,6 +54,7 @@
"cmd.org.short": "Organization operations",
"cmd.pr.close.short": "Close a pull request",
"cmd.pr.comment.short": "Add a comment to a pull request",
"cmd.pr.commits.short": "List commits in a pull request",
"cmd.pr.create.short": "Create a pull request",
"cmd.pr.diff.short": "Show diff for a pull request",
"cmd.pr.files.short": "List changed files in a pull request",
@ -82,11 +83,19 @@
"cmd.release.short": "Release operations",
"cmd.release.view.short": "View release details",
"cmd.repo.create.short": "Create a new repository",
"cmd.repo.batch_commit.short": "Create, update, or delete multiple files in one commit",
"cmd.repo.commit_diff.short": "Show diff for one commit",
"cmd.repo.commit_files.short": "List files changed by one commit",
"cmd.repo.commits.short": "List repository commits",
"cmd.repo.delete_tag.short": "Delete a repository tag",
"cmd.repo.delete.short": "Delete a repository",
"cmd.repo.files.short": "Search repository files",
"cmd.repo.fork.short": "Fork a repository",
"cmd.repo.info.short": "Show repository details",
"cmd.repo.list.short": "List repositories for a user or organization",
"cmd.repo.short": "Repository operations",
"cmd.repo.tag.short": "View repository tag details",
"cmd.repo.tags.short": "List repository tags",
"cmd.repo.tree.short": "List repository files and directories",
"cmd.root.long": "Manage repositories, issues, pull requests, releases, CI and workflows on GitLink.",
"cmd.root.short": "GitLink CLI - command-line tool for GitLink",
@ -209,9 +218,29 @@
"flag.release.target": "Target branch",
"flag.repo": "Repository name (auto-detected from git remote)",
"flag.repo.category": "Filter: manage/mirror/sync/fork/all (default: manage)",
"flag.repo.author_email": "Commit author email",
"flag.repo.author_name": "Commit author name",
"flag.repo.batch_branch": "Target branch",
"flag.repo.batch_dry_run": "Preview the batch commit request without changing files",
"flag.repo.batch_encoding": "Content encoding for inline file content: text or base64",
"flag.repo.batch_files": "File operations: action:path:content; repeat with semicolons",
"flag.repo.batch_message": "Commit message",
"flag.repo.batch_new_branch": "Create and commit to this new branch",
"flag.repo.batch_yes": "Confirm remote file changes",
"flag.repo.commit_sha": "Commit SHA",
"flag.repo.committer_email": "Commit committer email",
"flag.repo.committer_name": "Commit committer name",
"flag.repo.delete_tag_dry_run": "Preview the tag deletion without changing repository state",
"flag.repo.delete_tag_yes": "Confirm the destructive tag deletion",
"flag.repo.description": "Repository description",
"flag.repo.file": "Filter by file path",
"flag.repo.files.search": "File name/path keyword",
"flag.repo.name": "Repository name",
"flag.repo.only_name": "Return only names (true/false)",
"flag.repo.private": "Make repository private (true/false)",
"flag.repo.ref": "Branch, tag, or commit SHA",
"flag.repo.tag_name": "Tag name",
"flag.repo.tag_name_filter": "Tag search keyword",
"flag.repo.tree.path": "Directory path to list (default: repository root)",
"flag.repo.tree.ref": "Branch, tag, or commit ref",
"flag.search.keyword": "Search keyword",

View File

@ -54,6 +54,7 @@
"cmd.org.short": "组织操作",
"cmd.pr.close.short": "关闭拉取请求",
"cmd.pr.comment.short": "给拉取请求添加评论",
"cmd.pr.commits.short": "列出拉取请求中的提交",
"cmd.pr.create.short": "创建拉取请求",
"cmd.pr.diff.short": "显示拉取请求 diff",
"cmd.pr.files.short": "列出拉取请求中的变更文件",
@ -82,11 +83,19 @@
"cmd.release.short": "发布操作",
"cmd.release.view.short": "查看发布详情",
"cmd.repo.create.short": "创建新仓库",
"cmd.repo.batch_commit.short": "在一次提交中创建、更新或删除多个文件",
"cmd.repo.commit_diff.short": "显示单个提交的 diff",
"cmd.repo.commit_files.short": "列出单个提交变更的文件",
"cmd.repo.commits.short": "列出仓库提交",
"cmd.repo.delete_tag.short": "删除仓库标签",
"cmd.repo.delete.short": "删除仓库",
"cmd.repo.files.short": "搜索仓库文件",
"cmd.repo.fork.short": "Fork 仓库",
"cmd.repo.info.short": "显示仓库详情",
"cmd.repo.list.short": "列出用户或组织的仓库",
"cmd.repo.short": "仓库操作",
"cmd.repo.tag.short": "查看仓库标签详情",
"cmd.repo.tags.short": "列出仓库标签",
"cmd.repo.tree.short": "列出仓库文件和目录",
"cmd.root.long": "用于管理 GitLink 上的仓库、议题、拉取请求、发布、CI 和工作流。",
"cmd.root.short": "GitLink CLI - GitLink 命令行工具",
@ -209,9 +218,29 @@
"flag.release.target": "目标分支",
"flag.repo": "仓库名称(自动从 git remote 检测)",
"flag.repo.category": "筛选manage/mirror/sync/fork/all默认manage",
"flag.repo.author_email": "提交作者邮箱",
"flag.repo.author_name": "提交作者名称",
"flag.repo.batch_branch": "目标分支",
"flag.repo.batch_dry_run": "预览批量提交请求,不修改文件",
"flag.repo.batch_encoding": "内联文件内容编码text 或 base64",
"flag.repo.batch_files": "文件操作action:path:content多个操作用英文分号分隔",
"flag.repo.batch_message": "提交信息",
"flag.repo.batch_new_branch": "创建并提交到这个新分支",
"flag.repo.batch_yes": "确认修改远端文件",
"flag.repo.commit_sha": "Commit SHA",
"flag.repo.committer_email": "提交者邮箱",
"flag.repo.committer_name": "提交者名称",
"flag.repo.delete_tag_dry_run": "预览标签删除操作,不修改仓库状态",
"flag.repo.delete_tag_yes": "确认执行破坏性标签删除",
"flag.repo.description": "仓库描述",
"flag.repo.file": "按文件路径筛选",
"flag.repo.files.search": "文件名或路径关键词",
"flag.repo.name": "仓库名称",
"flag.repo.only_name": "只返回名称true/false",
"flag.repo.private": "设为私有仓库true/false",
"flag.repo.ref": "分支、标签或 Commit SHA",
"flag.repo.tag_name": "标签名称",
"flag.repo.tag_name_filter": "标签搜索关键词",
"flag.repo.tree.path": "要列出的目录路径(默认:仓库根目录)",
"flag.repo.tree.ref": "分支、标签或提交引用",
"flag.search.keyword": "搜索关键词",

View File

@ -245,6 +245,27 @@ func Shortcuts(translators ...*i18n.Translator) []*common.Shortcut {
return ctx.Output(env)
},
},
{
Name: "commits",
Description: tr.T("cmd.pr.commits.short"),
Flags: []common.Flag{
{Name: "id", Short: "i", Usage: tr.T("flag.pr.id"), Required: true},
},
Run: func(ctx *common.RuntimeContext) error {
if err := ctx.ResolveOwnerRepo(); err != nil {
return err
}
id, err := ctx.RequireArg("id")
if err != nil {
return err
}
env, err := ctx.CallAPI("GET", fmt.Sprintf("%s/pulls/%s/commits", ctx.RepoPath(), url.PathEscape(id)), nil)
if err != nil {
return err
}
return ctx.Output(env)
},
},
{
Name: "versions",
Description: tr.T("cmd.pr.versions.short"),

View File

@ -351,6 +351,31 @@ func TestPRDiff(t *testing.T) {
}
}
// --- commits ---
func TestPRCommits(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != "GET" {
t.Fatalf("expected GET, got %s", r.Method)
}
if r.URL.Path != "/owner/repo/pulls/42/commits.json" {
t.Fatalf("unexpected path: %s", r.URL.Path)
}
writeJSON(t, w, map[string]interface{}{
"commits_count": float64(1),
"commits": []interface{}{
map[string]interface{}{"sha": "abc123", "message": "feat: add shortcut"},
},
})
}))
defer server.Close()
err := runPRShortcut(t, server, "commits", map[string]string{"id": "42"})
if err != nil {
t.Fatalf("commits failed: %v", err)
}
}
// --- extractIssueID ---
func TestExtractIssueID(t *testing.T) {

View File

@ -108,6 +108,91 @@ func Shortcuts(translators ...*i18n.Translator) []*common.Shortcut {
return ctx.Output(env)
},
},
{
Name: "files",
Description: tr.T("cmd.repo.files.short"),
Flags: []common.Flag{
{Name: "search", Short: "s", Usage: tr.T("flag.repo.files.search")},
{Name: "ref", Short: "r", Usage: tr.T("flag.repo.ref")},
},
Run: runFiles,
},
{
Name: "commits",
Description: tr.T("cmd.repo.commits.short"),
Flags: []common.Flag{
{Name: "ref", Short: "r", Usage: tr.T("flag.repo.ref")},
{Name: "page", Short: "p", Usage: tr.T("flag.page"), Default: "1"},
{Name: "limit", Short: "l", Usage: tr.T("flag.limit"), Default: "20"},
},
Run: runCommits,
},
{
Name: "commit-files",
Description: tr.T("cmd.repo.commit_files.short"),
Flags: []common.Flag{
{Name: "sha", Short: "s", Usage: tr.T("flag.repo.commit_sha"), Required: true},
{Name: "file", Short: "f", Usage: tr.T("flag.repo.file")},
{Name: "page", Short: "p", Usage: tr.T("flag.page"), Default: "1"},
{Name: "limit", Short: "l", Usage: tr.T("flag.limit"), Default: "20"},
},
Run: runCommitFiles,
},
{
Name: "commit-diff",
Description: tr.T("cmd.repo.commit_diff.short"),
Flags: []common.Flag{
{Name: "sha", Short: "s", Usage: tr.T("flag.repo.commit_sha"), Required: true},
},
Run: runCommitDiff,
},
{
Name: "tags",
Description: tr.T("cmd.repo.tags.short"),
Flags: []common.Flag{
{Name: "name", Short: "n", Usage: tr.T("flag.repo.tag_name_filter")},
{Name: "only-name", Usage: tr.T("flag.repo.only_name"), Default: "false"},
{Name: "page", Short: "p", Usage: tr.T("flag.page"), Default: "1"},
{Name: "limit", Short: "l", Usage: tr.T("flag.limit"), Default: "20"},
},
Run: runTags,
},
{
Name: "tag",
Description: tr.T("cmd.repo.tag.short"),
Flags: []common.Flag{
{Name: "name", Short: "n", Usage: tr.T("flag.repo.tag_name"), Required: true},
},
Run: runTag,
},
{
Name: "delete-tag",
Description: tr.T("cmd.repo.delete_tag.short"),
Flags: []common.Flag{
{Name: "name", Short: "n", Usage: tr.T("flag.repo.tag_name"), Required: true},
{Name: "dry-run", Usage: tr.T("flag.repo.delete_tag_dry_run"), Bool: true, Default: "false"},
{Name: "yes", Usage: tr.T("flag.repo.delete_tag_yes"), Bool: true, Default: "false"},
},
Run: runDeleteTag,
},
{
Name: "batch-commit",
Description: tr.T("cmd.repo.batch_commit.short"),
Flags: []common.Flag{
{Name: "branch", Short: "b", Usage: tr.T("flag.repo.batch_branch"), Required: true},
{Name: "message", Short: "m", Usage: tr.T("flag.repo.batch_message"), Required: true},
{Name: "files", Short: "f", Usage: tr.T("flag.repo.batch_files"), Required: true},
{Name: "new-branch", Usage: tr.T("flag.repo.batch_new_branch")},
{Name: "encoding", Usage: tr.T("flag.repo.batch_encoding"), Default: "text"},
{Name: "author-name", Usage: tr.T("flag.repo.author_name")},
{Name: "author-email", Usage: tr.T("flag.repo.author_email")},
{Name: "committer-name", Usage: tr.T("flag.repo.committer_name")},
{Name: "committer-email", Usage: tr.T("flag.repo.committer_email")},
{Name: "dry-run", Usage: tr.T("flag.repo.batch_dry_run"), Bool: true, Default: "false"},
{Name: "yes", Usage: tr.T("flag.repo.batch_yes"), Bool: true, Default: "false"},
},
Run: runBatchCommit,
},
{
Name: "languages",
Description: "Show repository language statistics",
@ -263,6 +348,173 @@ func shortcutTranslator(translators ...*i18n.Translator) *i18n.Translator {
return i18n.Default()
}
func runFiles(ctx *common.RuntimeContext) error {
if err := ctx.ResolveOwnerRepo(); err != nil {
return err
}
q := url.Values{}
setRepoQueryIfPresent(q, "search", ctx.Arg("search"))
setRepoQueryIfPresent(q, "ref", ctx.Arg("ref"))
env, err := ctx.CallAPIWithQuery("GET", ctx.RepoPath()+"/files", q)
if err != nil {
return err
}
return ctx.Output(env)
}
func runCommits(ctx *common.RuntimeContext) error {
if err := ctx.ResolveOwnerRepo(); err != nil {
return err
}
q := url.Values{}
q.Set("page", firstRepoValue(ctx.Arg("page"), "1"))
q.Set("limit", firstRepoValue(ctx.Arg("limit"), "20"))
setRepoQueryIfPresent(q, "sha", ctx.Arg("ref"))
env, err := ctx.CallAPIWithQuery("GET", "/v1"+ctx.RepoPath()+"/commits", q)
if err != nil {
return err
}
return ctx.Output(env)
}
func runCommitFiles(ctx *common.RuntimeContext) error {
if err := ctx.ResolveOwnerRepo(); err != nil {
return err
}
sha, err := ctx.RequireArg("sha")
if err != nil {
return err
}
q := url.Values{}
if file := strings.TrimSpace(ctx.Arg("file")); file != "" {
q.Set("filepath", file)
} else {
q.Set("page", firstRepoValue(ctx.Arg("page"), "1"))
q.Set("limit", firstRepoValue(ctx.Arg("limit"), "20"))
}
env, err := ctx.CallAPIWithQuery("GET", fmt.Sprintf("/v1%s/commits/%s/files", ctx.RepoPath(), url.PathEscape(sha)), q)
if err != nil {
return err
}
return ctx.Output(env)
}
func runCommitDiff(ctx *common.RuntimeContext) error {
if err := ctx.ResolveOwnerRepo(); err != nil {
return err
}
sha, err := ctx.RequireArg("sha")
if err != nil {
return err
}
env, err := ctx.CallAPI("GET", fmt.Sprintf("/v1%s/commits/%s/diff", ctx.RepoPath(), url.PathEscape(sha)), nil)
if err != nil {
return err
}
return ctx.Output(env)
}
func runTags(ctx *common.RuntimeContext) error {
if err := ctx.ResolveOwnerRepo(); err != nil {
return err
}
q := url.Values{}
if name := strings.TrimSpace(ctx.Arg("name")); name != "" {
q.Set("name", name)
}
onlyName := strings.TrimSpace(ctx.Arg("only-name"))
if onlyName != "" && onlyName != "false" {
q.Set("only_name", onlyName)
}
if len(q) > 0 {
env, err := ctx.CallAPIWithQuery("GET", ctx.RepoPath()+"/tags", q)
if err != nil {
return err
}
return ctx.Output(env)
}
q.Set("page", firstRepoValue(ctx.Arg("page"), "1"))
q.Set("limit", firstRepoValue(ctx.Arg("limit"), "20"))
env, err := ctx.CallAPIWithQuery("GET", "/v1"+ctx.RepoPath()+"/tags", q)
if err != nil {
return err
}
return ctx.Output(env)
}
func runTag(ctx *common.RuntimeContext) error {
if err := ctx.ResolveOwnerRepo(); err != nil {
return err
}
name, err := ctx.RequireArg("name")
if err != nil {
return err
}
env, err := ctx.CallAPI("GET", fmt.Sprintf("/v1%s/tags/%s", ctx.RepoPath(), url.PathEscape(name)), nil)
if err != nil {
return err
}
return ctx.Output(env)
}
func runDeleteTag(ctx *common.RuntimeContext) error {
if err := ctx.ResolveOwnerRepo(); err != nil {
return err
}
name, err := ctx.RequireArg("name")
if err != nil {
return err
}
path := fmt.Sprintf("/v1%s/tags/%s", ctx.RepoPath(), url.PathEscape(name))
if ctx.Arg("dry-run") == "true" {
return ctx.OutputData(map[string]interface{}{
"dry_run": true,
"action": "delete_tag",
"method": "DELETE",
"path": path,
"repository": fmt.Sprintf("%s/%s", ctx.Owner, ctx.Repo),
"tag": name,
})
}
if ctx.Arg("yes") != "true" {
return fmt.Errorf("tag deletion is destructive; run --dry-run first, then pass --yes to execute")
}
env, err := ctx.CallAPI("DELETE", path, nil)
if err != nil {
return err
}
return ctx.Output(env)
}
func runBatchCommit(ctx *common.RuntimeContext) error {
if err := ctx.ResolveOwnerRepo(); err != nil {
return err
}
payload, err := batchCommitPayload(ctx)
if err != nil {
return err
}
path := "/v1" + ctx.RepoPath() + "/contents/batch"
if ctx.Arg("dry-run") == "true" {
return ctx.OutputData(map[string]interface{}{
"dry_run": true,
"action": "batch_commit",
"method": "POST",
"path": path,
"repository": fmt.Sprintf("%s/%s", ctx.Owner, ctx.Repo),
"payload": payload,
})
}
if ctx.Arg("yes") != "true" {
return fmt.Errorf("batch file commit changes repository content; run --dry-run first, then pass --yes to execute")
}
env, err := ctx.CallAPI("POST", path, payload)
if err != nil {
return err
}
return ctx.Output(env)
}
func runLanguages(ctx *common.RuntimeContext) error {
if err := ctx.ResolveOwnerRepo(); err != nil {
return err
@ -482,6 +734,103 @@ func setRepoQueryIfPresent(q url.Values, key, value string) {
}
}
func firstRepoValue(value, fallback string) string {
if value := strings.TrimSpace(value); value != "" {
return value
}
return fallback
}
func batchCommitPayload(ctx *common.RuntimeContext) (map[string]interface{}, error) {
branch, err := ctx.RequireArg("branch")
if err != nil {
return nil, err
}
message, err := ctx.RequireArg("message")
if err != nil {
return nil, err
}
rawFiles, err := ctx.RequireArg("files")
if err != nil {
return nil, err
}
encoding := firstRepoValue(ctx.Arg("encoding"), "text")
if encoding != "text" && encoding != "base64" {
return nil, fmt.Errorf("invalid --encoding %q: use text or base64", encoding)
}
files, err := parseBatchFileSpecs(rawFiles, encoding)
if err != nil {
return nil, err
}
payload := map[string]interface{}{
"branch": strings.TrimSpace(branch),
"message": message,
"files": files,
}
if newBranch := strings.TrimSpace(ctx.Arg("new-branch")); newBranch != "" {
payload["new_branch"] = newBranch
}
setRepoPayloadIfPresent(payload, "author_name", ctx.Arg("author-name"))
setRepoPayloadIfPresent(payload, "author_email", ctx.Arg("author-email"))
setRepoPayloadIfPresent(payload, "committer_name", ctx.Arg("committer-name"))
setRepoPayloadIfPresent(payload, "committer_email", ctx.Arg("committer-email"))
return payload, nil
}
func parseBatchFileSpecs(raw, encoding string) ([]map[string]interface{}, error) {
specs := strings.Split(raw, ";")
files := make([]map[string]interface{}, 0, len(specs))
for _, spec := range specs {
spec = strings.TrimSpace(spec)
if spec == "" {
continue
}
parts := strings.SplitN(spec, ":", 3)
if len(parts) < 2 {
return nil, fmt.Errorf("invalid --files item %q: use action:path[:content]", spec)
}
action := strings.TrimSpace(parts[0])
path := strings.TrimSpace(parts[1])
if !isBatchFileAction(action) {
return nil, fmt.Errorf("invalid file action %q: use create, update, or delete", action)
}
if path == "" {
return nil, fmt.Errorf("invalid --files item %q: file path is required", spec)
}
item := map[string]interface{}{
"action_type": action,
"file_path": path,
}
if action != "delete" {
if len(parts) != 3 {
return nil, fmt.Errorf("file action %q for %q requires content", action, path)
}
item["content"] = parts[2]
item["encoding"] = encoding
}
files = append(files, item)
}
if len(files) == 0 {
return nil, fmt.Errorf("--files must include at least one file operation")
}
return files, nil
}
func isBatchFileAction(action string) bool {
switch action {
case "create", "update", "delete":
return true
default:
return false
}
}
func setRepoPayloadIfPresent(payload map[string]interface{}, key, value string) {
if value := strings.TrimSpace(value); value != "" {
payload[key] = value
}
}
func parseOptionalRepoNonNegativeInt(value, name string) (int, bool, error) {
if strings.TrimSpace(value) == "" {
return 0, false, nil

View File

@ -57,6 +57,15 @@ func writeText(t *testing.T, w http.ResponseWriter, code int, text string) {
}
}
func decodeRepoJSON(t *testing.T, r *http.Request) map[string]interface{} {
t.Helper()
var payload map[string]interface{}
if err := json.NewDecoder(r.Body).Decode(&payload); err != nil {
t.Fatalf("decode request body: %v", err)
}
return payload
}
// --- list ---
func TestRepoListDefault(t *testing.T) {
@ -273,6 +282,250 @@ func TestRepoCodeStatsUsesRefQuery(t *testing.T) {
}
}
func TestRepoFilesBuildsSearchAndRefQuery(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
assertRequest(t, r, "GET", "/owner/repo/files.json")
assertEqual(t, r.URL.Query().Get("search"), "README")
assertEqual(t, r.URL.Query().Get("ref"), "main")
writeJSON(t, w, []interface{}{map[string]interface{}{"path": "README.md"}})
}))
defer server.Close()
if err := runShortcut(t, server, "files", map[string]string{"search": "README", "ref": "main"}); err != nil {
t.Fatalf("files shortcut failed: %v", err)
}
}
func TestRepoCommitsBuildsRefPaginationQuery(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
assertRequest(t, r, "GET", "/v1/owner/repo/commits.json")
assertEqual(t, r.URL.Query().Get("sha"), "main")
assertEqual(t, r.URL.Query().Get("page"), "2")
assertEqual(t, r.URL.Query().Get("limit"), "50")
writeJSON(t, w, map[string]interface{}{"total_count": 1, "commits": []interface{}{}})
}))
defer server.Close()
err := runShortcut(t, server, "commits", map[string]string{"ref": "main", "page": "2", "limit": "50"})
if err != nil {
t.Fatalf("commits shortcut failed: %v", err)
}
}
func TestRepoCommitFilesUsesPaginationWhenNoFileFilter(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
assertRequest(t, r, "GET", "/v1/owner/repo/commits/abc123/files.json")
assertEqual(t, r.URL.Query().Get("page"), "3")
assertEqual(t, r.URL.Query().Get("limit"), "10")
if got := r.URL.Query().Get("filepath"); got != "" {
t.Fatalf("did not expect filepath query, got %q", got)
}
writeJSON(t, w, map[string]interface{}{"files": []interface{}{}})
}))
defer server.Close()
err := runShortcut(t, server, "commit-files", map[string]string{"sha": "abc123", "page": "3", "limit": "10"})
if err != nil {
t.Fatalf("commit-files shortcut failed: %v", err)
}
}
func TestRepoCommitFilesUsesFileFilterWithoutPagination(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
assertRequest(t, r, "GET", "/v1/owner/repo/commits/abc123/files.json")
assertEqual(t, r.URL.Query().Get("filepath"), "src/main.go")
if got := r.URL.Query().Get("page"); got != "" {
t.Fatalf("did not expect page query with filepath filter, got %q", got)
}
writeJSON(t, w, map[string]interface{}{"files": []interface{}{}})
}))
defer server.Close()
err := runShortcut(t, server, "commit-files", map[string]string{"sha": "abc123", "file": "src/main.go"})
if err != nil {
t.Fatalf("commit-files shortcut failed: %v", err)
}
}
func TestRepoCommitDiffUsesCommitDiffEndpoint(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
assertRequest(t, r, "GET", "/v1/owner/repo/commits/abc123/diff.json")
writeJSON(t, w, map[string]interface{}{"diff": "@@ -1 +1 @@"})
}))
defer server.Close()
if err := runShortcut(t, server, "commit-diff", map[string]string{"sha": "abc123"}); err != nil {
t.Fatalf("commit-diff shortcut failed: %v", err)
}
}
func TestRepoTagsBuildsPaginationQuery(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
assertRequest(t, r, "GET", "/v1/owner/repo/tags.json")
assertEqual(t, r.URL.Query().Get("page"), "2")
assertEqual(t, r.URL.Query().Get("limit"), "30")
writeJSON(t, w, map[string]interface{}{"total_count": 1, "tags": []interface{}{}})
}))
defer server.Close()
err := runShortcut(t, server, "tags", map[string]string{"page": "2", "limit": "30"})
if err != nil {
t.Fatalf("tags shortcut failed: %v", err)
}
}
func TestRepoTagsUsesSearchEndpointForNameFilter(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
assertRequest(t, r, "GET", "/owner/repo/tags.json")
assertEqual(t, r.URL.Query().Get("name"), "v1")
assertEqual(t, r.URL.Query().Get("only_name"), "true")
writeJSON(t, w, map[string]interface{}{"total_count": 1, "tags": []interface{}{map[string]interface{}{"name": "v1.0.0"}}})
}))
defer server.Close()
err := runShortcut(t, server, "tags", map[string]string{"name": "v1", "only-name": "true"})
if err != nil {
t.Fatalf("tags shortcut with name filter failed: %v", err)
}
}
func TestRepoTagUsesTagDetailEndpoint(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
assertRequest(t, r, "GET", "/v1/owner/repo/tags/v1.0.0.json")
writeJSON(t, w, map[string]interface{}{"name": "v1.0.0"})
}))
defer server.Close()
if err := runShortcut(t, server, "tag", map[string]string{"name": "v1.0.0"}); err != nil {
t.Fatalf("tag shortcut failed: %v", err)
}
}
func TestRepoDeleteTagDryRunDoesNotCallAPI(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
t.Fatalf("delete-tag dry-run should not call remote API: %s %s", r.Method, r.URL.Path)
}))
defer server.Close()
err := runShortcut(t, server, "delete-tag", map[string]string{"name": "v1.0.0", "dry-run": "true"})
if err != nil {
t.Fatalf("delete-tag dry-run failed: %v", err)
}
}
func TestRepoDeleteTagRequiresYesForRemoteDelete(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
t.Fatalf("delete-tag without --yes should not call remote API: %s %s", r.Method, r.URL.Path)
}))
defer server.Close()
err := runShortcut(t, server, "delete-tag", map[string]string{"name": "v1.0.0"})
if err == nil {
t.Fatal("expected error when delete-tag is missing --yes")
}
}
func TestRepoDeleteTagWithYesCallsDeleteEndpoint(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
assertRequest(t, r, "DELETE", "/v1/owner/repo/tags/v1.0.0.json")
writeJSON(t, w, map[string]interface{}{"status": 0, "message": "success"})
}))
defer server.Close()
err := runShortcut(t, server, "delete-tag", map[string]string{"name": "v1.0.0", "yes": "true"})
if err != nil {
t.Fatalf("delete-tag failed: %v", err)
}
}
func TestRepoBatchCommitDryRunDoesNotCallAPI(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
t.Fatalf("dry-run should not call remote API: %s %s", r.Method, r.URL.Path)
}))
defer server.Close()
err := runShortcut(t, server, "batch-commit", map[string]string{
"branch": "master",
"message": "docs: update",
"files": "update:README.md:# hello;delete:old.txt",
"new-branch": "docs/update-readme",
"dry-run": "true",
})
if err != nil {
t.Fatalf("batch-commit dry-run failed: %v", err)
}
}
func TestRepoBatchCommitRequiresYesForRemoteWrite(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
t.Fatalf("batch-commit without --yes should not call remote API: %s %s", r.Method, r.URL.Path)
}))
defer server.Close()
err := runShortcut(t, server, "batch-commit", map[string]string{
"branch": "master",
"message": "docs: update",
"files": "update:README.md:# hello",
})
if err == nil {
t.Fatal("expected error when batch-commit is missing --yes")
}
}
func TestRepoBatchCommitPostsPayloadWithYes(t *testing.T) {
var payload map[string]interface{}
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
assertRequest(t, r, "POST", "/v1/owner/repo/contents/batch.json")
payload = decodeRepoJSON(t, r)
writeJSON(t, w, map[string]interface{}{"commit": map[string]interface{}{"sha": "abc123"}})
}))
defer server.Close()
err := runShortcut(t, server, "batch-commit", map[string]string{
"branch": "master",
"message": "docs: update",
"files": "create:docs/new.md:# new;update:README.md:# hello;delete:old.txt",
"encoding": "text",
"author-name": "Alice",
"author-email": "alice@example.com",
"committer-name": "Bot",
"committer-email": "bot@example.com",
"yes": "true",
})
if err != nil {
t.Fatalf("batch-commit failed: %v", err)
}
assertEqual(t, payload["branch"], "master")
assertEqual(t, payload["message"], "docs: update")
assertEqual(t, payload["author_name"], "Alice")
files, ok := payload["files"].([]interface{})
if !ok || len(files) != 3 {
t.Fatalf("expected three file operations, got %#v", payload["files"])
}
first, _ := files[0].(map[string]interface{})
assertEqual(t, first["action_type"], "create")
assertEqual(t, first["file_path"], "docs/new.md")
assertEqual(t, first["content"], "# new")
assertEqual(t, first["encoding"], "text")
}
func TestRepoBatchCommitRejectsInvalidFileSpec(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(http.ResponseWriter, *http.Request) {
t.Fatal("invalid file spec should fail before calling remote API")
}))
defer server.Close()
err := runShortcut(t, server, "batch-commit", map[string]string{
"branch": "master",
"message": "docs: update",
"files": "move:README.md:# hello",
"dry-run": "true",
})
if err == nil {
t.Fatal("expected invalid file action error")
}
}
func TestRepoWatchersBuildsTimeRangeQuery(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
assertRequest(t, r, "GET", "/owner/repo/watchers.json")

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

@ -223,7 +223,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

@ -99,17 +99,17 @@ behavior:
| PR 元信息 | 标题/描述/作者/关联 issue | `gitlink-cli pr +view -i <id> --format json` |
| 变更文件 | 文件路径列表 | `gitlink-cli pr +files -i <id> --format json` |
| Diff | 变更内容供 AI 审查 | `gitlink-cli pr +diff -i <id> --format json` |
| commits | commit 列表(消息供 commit_quality | `gitlink-cli api GET /:owner/:repo/pulls/:id/commits --format json` |
| commits | commit 列表(消息供 commit_quality | `gitlink-cli pr +commits -i <id> --format json` |
| CI 状态 | 构建结果 | `gitlink-cli ci +builds --format json` |
> 实测注意:`pr +files` 与 `pr +diff` 底层都打 `/pulls/:id/files`——`+files` 取路径列表,`+diff` 取含 patch 的同一份数据,按需取用即可。`pr +commits` 快捷命令commit 列表只能走 Raw API。CI 通过/失败需从 builds 返回的 `status` 字段判断;**无 build 记录时按「CI 未知」处理**(见 §3.5)。
> 实测注意:`pr +files` 与 `pr +diff` 底层都打 `/pulls/:id/files`——`+files` 取路径列表,`+diff` 取含 patch 的同一份数据,按需取用即可。commit 列表使用 `pr +commits`。CI 通过/失败需从 builds 返回的 `status` 字段判断;**无 build 记录时按「CI 未知」处理**(见 §3.5)。
```bash
PR=42
gitlink-cli pr +view -i "$PR" --format json # title / body / 关联 issue
gitlink-cli pr +files -i "$PR" --format json # changed files
gitlink-cli pr +diff -i "$PR" --format json # diff供 AI 审查)
gitlink-cli api GET /:owner/:repo/pulls/$PR/commits --format json
gitlink-cli pr +commits -i "$PR" --format json
gitlink-cli ci +builds --format json
```
@ -295,7 +295,7 @@ PR=42
gitlink-cli pr +view -i "$PR" --format json
gitlink-cli pr +files -i "$PR" --format json
gitlink-cli pr +diff -i "$PR" --format json
gitlink-cli api GET /:owner/:repo/pulls/$PR/commits --format json
gitlink-cli pr +commits -i "$PR" --format json
gitlink-cli ci +builds --format json
# → AI 产出发现 → 按 §4 评分 → §5 硬门禁 → §6 裁决 → §7 渲染评分卡
# → dry-run仅把评分卡打印给用户结尾提示「如需回写到 PR请加 --apply」

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

@ -27,6 +27,7 @@ metadata:
| `pr +reopen` | 重开已关闭的 PR | 是 |
| `pr +files` | 变更文件列表 | 否 |
| `pr +diff` | 查看变更文件和 diff 内容 | 否 |
| `pr +commits` | 查看 PR 包含的提交列表 | 否 |
| `pr +versions` | 查看 PR patchset/version 列表 | 否 |
| `pr +version-diff` | 查看指定 patchset/version diff | 否 |
| `pr +reviews` | 查看 PR 审查记录,支持状态过滤 | 否 |
@ -61,6 +62,9 @@ gitlink-cli pr +reopen --id 3
# 查看变更文件(含 diff 内容)
gitlink-cli pr +files --id 3
# 查看 PR 包含的提交列表
gitlink-cli pr +commits --id 3
# 查看 PR patchset/version 列表
gitlink-cli pr +versions --id 3
@ -172,3 +176,10 @@ gitlink-cli api GET /v1/:owner/:repo/pulls/:id/versions/:version_id/diff
- `pr +list``--state` 参数会映射为 GitLink API 的 `status` 筛选;需要查看所有 PR 时传 `--state all`
- PR 状态值:`pull_request_status` 0=open, 1=merged, 2=closed
- 关联已有 Issue 时,把 Issue 编号或 URL 写入 PR `--body`,或使用 `issue +comment` 留痕;不要用 Raw API 对 Issue 做不完整更新,否则可能清空 Issue 描述
## 参考文档
- [`references/gitlink-pr-commits.md`](references/gitlink-pr-commits.md)
- [`references/gitlink-pr-files.md`](references/gitlink-pr-files.md)
- [`references/gitlink-pr-review.md`](references/gitlink-pr-review.md)
- [`references/gitlink-pr-version-diff.md`](references/gitlink-pr-version-diff.md)

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

@ -24,6 +24,12 @@ metadata:
| `repo +info` | 仓库详情 | 否(公开项目) |
| `repo +readme` | README 内容 | 否(公开项目) |
| `repo +tree` | 仓库文件树 | 否(公开项目) |
| `repo +files` | 搜索仓库文件 | 否(公开项目) |
| `repo +commits` | 提交历史列表 | 否(公开项目) |
| `repo +commit-files` | 单个提交的变更文件 | 否(公开项目) |
| `repo +commit-diff` | 单个提交的 diff | 否(公开项目) |
| `repo +tags` | 仓库标签列表 | 否(公开项目) |
| `repo +tag` | 仓库标签详情 | 否(公开项目) |
| `repo +languages` | 仓库语言统计 | 否(公开项目) |
| `repo +contributors` | 仓库贡献者列表 | 否(公开项目) |
| `repo +contributor-stats` | 贡献者代码行统计 | 否(公开项目) |
@ -34,6 +40,8 @@ metadata:
| `repo +unfollow` | 取消关注仓库 | 是 |
| `repo +like` | 点赞仓库 | 是 |
| `repo +unlike` | 取消点赞仓库 | 是 |
| `repo +delete-tag` | 删除仓库标签,默认建议先 dry-run | 是 |
| `repo +batch-commit` | 多文件批量提交,默认建议先 dry-run | 是 |
| `repo +create` | 创建仓库 | 是 |
| `repo +fork` | Fork 仓库 | 是 |
| `repo +delete` | 删除仓库 | 是 |
@ -57,6 +65,18 @@ gitlink-cli repo +tree --owner Gitlink --repo forgeplus --path src --ref main
gitlink-cli repo +languages --owner Gitlink --repo forgeplus
gitlink-cli repo +contributors --owner Gitlink --repo forgeplus
# 搜索文件和分析提交历史
gitlink-cli repo +files --owner Gitlink --repo forgeplus --search README --ref master
gitlink-cli repo +commits --owner Gitlink --repo forgeplus --ref master --limit 20
gitlink-cli repo +commit-files --owner Gitlink --repo forgeplus --sha <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
@ -72,6 +92,16 @@ gitlink-cli repo +unfollow --owner Gitlink --repo forgeplus --project-id 123
gitlink-cli repo +like --owner Gitlink --repo forgeplus
gitlink-cli repo +unlike --owner Gitlink --repo forgeplus --project-id 123
# 预览多文件提交;真实写入前必须确认用户意图
gitlink-cli repo +batch-commit --owner me --repo proj \
--branch master --message "docs: update guide" \
--files 'update:README.md:# Updated;create:docs/demo.md:# Demo' \
--dry-run
gitlink-cli repo +batch-commit --owner me --repo proj \
--branch master --message "docs: update guide" \
--files 'update:README.md:# Updated;delete:old.md' \
--yes
# 创建仓库
gitlink-cli repo +create --name my-project --description "项目描述"
@ -87,9 +117,6 @@ gitlink-cli repo +delete --owner myuser --repo old-project
Shortcuts 未覆盖的仓库操作可用 Raw API
```bash
# 获取提交列表
gitlink-cli api GET /:owner/:repo/commits --query 'page=1&limit=20'
# 获取标签列表
gitlink-cli api GET /:owner/:repo/tags
@ -101,3 +128,13 @@ gitlink-cli api GET /:owner/:repo/raw/main/README.md
- `repo +delete` 是不可逆操作,执行前必须确认用户意图
- 创建仓库默认为公开,使用 `--private true` 创建私有仓库
- `repo +delete-tag` 会删除远端标签Agent 必须先运行 `--dry-run` 并获得用户明确确认,再加 `--yes`
- `repo +batch-commit` 会修改仓库内容Agent 必须先运行 `--dry-run` 并向用户展示 payload再在用户明确确认后加 `--yes`
- `repo +batch-commit --files` 使用 `action:path[:content]`,多个操作用英文分号分隔;`delete` 不需要 content`create/update` 需要 content
## 参考文档
- [`references/gitlink-repo-tree.md`](references/gitlink-repo-tree.md)
- [`references/gitlink-repo-code-history.md`](references/gitlink-repo-code-history.md)
- [`references/gitlink-repo-tags.md`](references/gitlink-repo-tags.md)
- [`references/gitlink-repo-batch-commit.md`](references/gitlink-repo-batch-commit.md)

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)