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 f3dce12..0aa8204 100644 --- a/shortcuts/register.go +++ b/shortcuts/register.go @@ -6,7 +6,6 @@ import ( "github.com/gitlink-org/gitlink-cli/internal/i18n" "github.com/gitlink-org/gitlink-cli/shortcuts/branch" "github.com/gitlink-org/gitlink-cli/shortcuts/ci" - "github.com/gitlink-org/gitlink-cli/shortcuts/commit" "github.com/gitlink-org/gitlink-cli/shortcuts/common" "github.com/gitlink-org/gitlink-cli/shortcuts/compare" "github.com/gitlink-org/gitlink-cli/shortcuts/dataset" @@ -23,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" @@ -37,55 +37,55 @@ func RegisterAll(root *cobra.Command, translators ...*i18n.Translator) { tr = translators[0] } groups := map[string][]*common.Shortcut{ - "repo": repo.Shortcuts(tr), - "commit": commit.Shortcuts(), - "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"), - "commit": "Commit inspection operations", - "issue": tr.T("cmd.issue.short"), - "label": tr.T("cmd.label.short"), - "license": tr.T("cmd.license.short"), - "member": tr.T("cmd.member.short"), - "milestone": tr.T("cmd.milestone.short"), - "pipeline": tr.T("cmd.pipeline.short"), - "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": tr.T("cmd.compare.short"), - "dataset": tr.T("cmd.dataset.short"), - "webhook": tr.T("cmd.webhook.short"), - "wiki": tr.T("cmd.wiki.short"), - "health": tr.T("cmd.health.short"), - "ignore": tr.T("cmd.ignore.short"), - "workflow": tr.T("cmd.workflow.short"), + "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 1a2c5fe..7d68aee 100644 --- a/shortcuts/register_test.go +++ b/shortcuts/register_test.go @@ -1,7 +1,6 @@ package shortcuts import ( - "strings" "testing" "github.com/spf13/cobra" @@ -15,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", "commit", + "dataset", "health", "ignore", "wiki", "repo-mirror", } groupSet := map[string]bool{} @@ -61,9 +60,6 @@ func TestRegisterAllGroupDescriptions(t *testing.T) { if cmd.Short == "" { t.Fatal("Short description is empty") } - if strings.HasPrefix(cmd.Short, "cmd.") && strings.HasSuffix(cmd.Short, ".short") { - t.Fatalf("Short description is an unresolved i18n key: %q", cmd.Short) - } }) } } 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`。