From 4a263c1ec0238840efd86c3a6f4c2f723f6f851a Mon Sep 17 00:00:00 2001 From: wangyue789 Date: Thu, 11 Jun 2026 10:33:06 +0800 Subject: [PATCH] feat(repo): add mirror sync shortcut --- doc/changes/repo-mirror-sync-shortcut.md | 13 ++++ shortcuts/register.go | 91 ++++++++++++------------ shortcuts/register_test.go | 2 +- shortcuts/repomirror/repomirror.go | 43 +++++++++++ shortcuts/repomirror/repomirror_test.go | 45 ++++++++++++ skills/gitlink-repo-mirror/SKILL.md | 20 ++++++ 6 files changed, 169 insertions(+), 45 deletions(-) create mode 100644 doc/changes/repo-mirror-sync-shortcut.md create mode 100644 shortcuts/repomirror/repomirror.go create mode 100644 shortcuts/repomirror/repomirror_test.go create mode 100644 skills/gitlink-repo-mirror/SKILL.md diff --git a/doc/changes/repo-mirror-sync-shortcut.md b/doc/changes/repo-mirror-sync-shortcut.md new file mode 100644 index 0000000..37e9142 --- /dev/null +++ b/doc/changes/repo-mirror-sync-shortcut.md @@ -0,0 +1,13 @@ +# Repo Mirror Sync Shortcut + +Added `gitlink-cli repo-mirror +sync` for `POST /repositories/{id}/sync_mirror`. + +Safety: supports `--dry-run`; validates `--id` as integer. + +Validation: + +```bash +GOPROXY=https://goproxy.cn,direct go test ./shortcuts/repomirror ./shortcuts +go vet ./shortcuts/repomirror ./shortcuts +go run . repo-mirror --help +``` diff --git a/shortcuts/register.go b/shortcuts/register.go index 1fedc7e..0aa8204 100644 --- a/shortcuts/register.go +++ b/shortcuts/register.go @@ -22,6 +22,7 @@ import ( "github.com/gitlink-org/gitlink-cli/shortcuts/profile" "github.com/gitlink-org/gitlink-cli/shortcuts/release" "github.com/gitlink-org/gitlink-cli/shortcuts/repo" + "github.com/gitlink-org/gitlink-cli/shortcuts/repomirror" "github.com/gitlink-org/gitlink-cli/shortcuts/search" "github.com/gitlink-org/gitlink-cli/shortcuts/user" "github.com/gitlink-org/gitlink-cli/shortcuts/webhook" @@ -36,53 +37,55 @@ func RegisterAll(root *cobra.Command, translators ...*i18n.Translator) { tr = translators[0] } groups := map[string][]*common.Shortcut{ - "repo": repo.Shortcuts(tr), - "issue": issue.Shortcuts(tr), - "label": label.Shortcuts(), - "license": license.Shortcuts(), - "member": member.Shortcuts(), - "milestone": milestone.Shortcuts(), - "pipeline": pipeline.Shortcuts(), - "pr": pr.Shortcuts(tr), - "profile": profile.Shortcuts(tr), - "release": release.Shortcuts(tr), - "branch": branch.Shortcuts(tr), - "org": org.Shortcuts(tr), - "user": user.Shortcuts(tr), - "search": search.Shortcuts(tr), - "ci": ci.Shortcuts(tr), - "compare": compare.Shortcuts(), - "dataset": dataset.Shortcuts(tr), - "webhook": webhook.Shortcuts(tr), - "wiki": wiki.Shortcuts(), - "health": health.Shortcuts(tr), - "ignore": ignore.Shortcuts(), - "workflow": workflow.Shortcuts(), + "repo": repo.Shortcuts(tr), + "issue": issue.Shortcuts(tr), + "label": label.Shortcuts(), + "license": license.Shortcuts(), + "member": member.Shortcuts(), + "milestone": milestone.Shortcuts(), + "pipeline": pipeline.Shortcuts(), + "pr": pr.Shortcuts(tr), + "profile": profile.Shortcuts(tr), + "release": release.Shortcuts(tr), + "branch": branch.Shortcuts(tr), + "org": org.Shortcuts(tr), + "user": user.Shortcuts(tr), + "search": search.Shortcuts(tr), + "ci": ci.Shortcuts(tr), + "compare": compare.Shortcuts(), + "dataset": dataset.Shortcuts(tr), + "webhook": webhook.Shortcuts(tr), + "wiki": wiki.Shortcuts(), + "health": health.Shortcuts(tr), + "ignore": ignore.Shortcuts(), + "repo-mirror": repomirror.Shortcuts(), + "workflow": workflow.Shortcuts(), } descriptions := map[string]string{ - "repo": tr.T("cmd.repo.short"), - "issue": tr.T("cmd.issue.short"), - "label": "Issue label operations", - "license": "License operations", - "member": "Repository member operations", - "milestone": "Milestone operations", - "pipeline": "Pipeline operations", - "pr": tr.T("cmd.pr.short"), - "profile": tr.T("cmd.profile.short"), - "release": tr.T("cmd.release.short"), - "branch": tr.T("cmd.branch.short"), - "org": tr.T("cmd.org.short"), - "user": tr.T("cmd.user.short"), - "search": tr.T("cmd.search.short"), - "ci": tr.T("cmd.ci.short"), - "compare": "Compare branches, tags, or commits", - "dataset": tr.T("cmd.dataset.short"), - "webhook": tr.T("cmd.webhook.short"), - "wiki": "Wiki page management", - "health": "Project health data collection", - "ignore": tr.T("cmd.ignore.short"), - "workflow": "AI agent workflow analysis", + "repo": tr.T("cmd.repo.short"), + "issue": tr.T("cmd.issue.short"), + "label": "Issue label operations", + "license": "License operations", + "member": "Repository member operations", + "milestone": "Milestone operations", + "pipeline": "Pipeline operations", + "pr": tr.T("cmd.pr.short"), + "profile": tr.T("cmd.profile.short"), + "release": tr.T("cmd.release.short"), + "branch": tr.T("cmd.branch.short"), + "org": tr.T("cmd.org.short"), + "user": tr.T("cmd.user.short"), + "search": tr.T("cmd.search.short"), + "ci": tr.T("cmd.ci.short"), + "compare": "Compare branches, tags, or commits", + "dataset": tr.T("cmd.dataset.short"), + "webhook": tr.T("cmd.webhook.short"), + "wiki": "Wiki page management", + "health": "Project health data collection", + "ignore": tr.T("cmd.ignore.short"), + "repo-mirror": "Repository mirror operations", + "workflow": "AI agent workflow analysis", } for name, shortcuts := range groups { diff --git a/shortcuts/register_test.go b/shortcuts/register_test.go index 00f4c57..7d68aee 100644 --- a/shortcuts/register_test.go +++ b/shortcuts/register_test.go @@ -14,7 +14,7 @@ func TestRegisterAll(t *testing.T) { "repo", "issue", "label", "license", "pr", "profile", "release", "branch", "org", "user", "search", "ci", "workflow", "compare", "member", "milestone", "pipeline", "webhook", - "dataset", "health", "ignore", "wiki", + "dataset", "health", "ignore", "wiki", "repo-mirror", } groupSet := map[string]bool{} diff --git a/shortcuts/repomirror/repomirror.go b/shortcuts/repomirror/repomirror.go new file mode 100644 index 0000000..9c233fb --- /dev/null +++ b/shortcuts/repomirror/repomirror.go @@ -0,0 +1,43 @@ +package repomirror + +import ( + "fmt" + "net/url" + "strconv" + + "github.com/gitlink-org/gitlink-cli/shortcuts/common" +) + +// Shortcuts returns mirror repository operations. +func Shortcuts() []*common.Shortcut { + return []*common.Shortcut{ + { + Name: "sync", + Description: "Synchronize a mirror repository", + Flags: []common.Flag{ + {Name: "id", Short: "i", Usage: "Repository/project ID", Required: true}, + {Name: "dry-run", Usage: "Preview the request without syncing the mirror", Bool: true, Default: "false"}, + }, + Run: runSync, + }, + } +} + +func runSync(ctx *common.RuntimeContext) error { + id, err := ctx.RequireArg("id") + if err != nil { + return err + } + if _, err := strconv.Atoi(id); err != nil { + return fmt.Errorf("invalid --id value %q: %w", id, err) + } + path := fmt.Sprintf("/repositories/%s/sync_mirror", url.PathEscape(id)) + if ctx.Arg("dry-run") == "true" { + return ctx.OutputData(map[string]interface{}{"dry_run": true, "action": "sync_mirror", "method": "POST", "path": path}) + } + env, err := ctx.CallAPI("POST", path, nil) + if err != nil { + return err + } + return ctx.Output(env) +} diff --git a/shortcuts/repomirror/repomirror_test.go b/shortcuts/repomirror/repomirror_test.go new file mode 100644 index 0000000..00570db --- /dev/null +++ b/shortcuts/repomirror/repomirror_test.go @@ -0,0 +1,45 @@ +package repomirror + +import ( + "encoding/json" + "net/http" + "net/http/httptest" + "testing" + + "github.com/gitlink-org/gitlink-cli/internal/client" + "github.com/gitlink-org/gitlink-cli/shortcuts/common" +) + +func TestMirrorSync(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Method != "POST" || r.URL.Path != "/repositories/42/sync_mirror.json" { + t.Fatalf("got %s %s", r.Method, r.URL.Path) + } + w.Header().Set("Content-Type", "application/json") + _ = json.NewEncoder(w).Encode(map[string]interface{}{"status": 0}) + })) + defer server.Close() + if err := runShortcut(server, map[string]string{"id": "42"}); err != nil { + t.Fatalf("sync failed: %v", err) + } +} + +func TestMirrorSyncDryRun(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + t.Fatalf("dry-run should not call API") + })) + defer server.Close() + if err := runShortcut(server, map[string]string{"id": "42", "dry-run": "true"}); err != nil { + t.Fatalf("dry-run failed: %v", err) + } +} + +func TestMirrorSyncRejectsInvalidID(t *testing.T) { + if err := runShortcut(httptest.NewServer(http.NewServeMux()), map[string]string{"id": "abc"}); err == nil { + t.Fatal("expected invalid id error") + } +} + +func runShortcut(server *httptest.Server, args map[string]string) error { + return Shortcuts()[0].Run(&common.RuntimeContext{Client: &client.Client{HTTP: server.Client(), BaseURL: server.URL}, Format: "json", Args: args}) +} diff --git a/skills/gitlink-repo-mirror/SKILL.md b/skills/gitlink-repo-mirror/SKILL.md new file mode 100644 index 0000000..4b141b5 --- /dev/null +++ b/skills/gitlink-repo-mirror/SKILL.md @@ -0,0 +1,20 @@ +--- +name: gitlink-repo-mirror +version: 1.0.0 +description: "GitLink 镜像仓库同步:触发镜像仓库 sync_mirror,支持 dry-run。" +metadata: + requires: + bins: ["gitlink-cli"] + cliHelp: "gitlink-cli repo-mirror --help" +--- + +# gitlink-repo-mirror + +镜像同步是写操作,先 dry-run 并确认。 + +```bash +gitlink-cli repo-mirror +sync --id 42 --dry-run +gitlink-cli repo-mirror +sync --id 42 +``` + +对应 OpenAPI:`POST /repositories/{id}/sync_mirror`。