diff --git a/README.md b/README.md index 31c4c00..d1997a5 100644 --- a/README.md +++ b/README.md @@ -121,7 +121,6 @@ The official [GitLink](https://www.gitlink.org.cn) CLI tool — built for humans | 📊 Profile | User ability, role, major, activity, and contribution statistics | | 📋 PM | Sprint management, kanban boards, weekly reports | | 🤖 Workflow | AI-powered issue triage, PR review, release notes | -| 🩺 Doctor | Diagnose configuration, authentication, repo context, and API connectivity | ## Installation & Quick Start @@ -228,10 +227,8 @@ 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 topics, then attach/detach one on a repository -gitlink-cli repo +topics -k golang -gitlink-cli repo +topic-add --owner Gitlink --repo forgeplus -n golang -gitlink-cli repo +topic-remove --owner Gitlink --repo forgeplus -t 627 +# Line-by-line blame for a file +gitlink-cli repo +blame --owner Gitlink --repo forgeplus -p README.md --ref master # Show language breakdown gitlink-cli repo +languages --owner Gitlink --repo forgeplus @@ -283,13 +280,10 @@ gitlink-cli webhook +tasks --owner Gitlink --repo forgeplus --id 68 ### Wiki Management ```bash -# List wiki pages (table of contents); --project-id is auto-resolved from the repository when omitted -gitlink-cli wiki +list --owner Gitlink --repo forgeplus +# List wiki pages (table of contents) +gitlink-cli wiki +list --owner Gitlink --repo forgeplus --project-id 12345 # View a wiki page by page name -gitlink-cli wiki +view --owner Gitlink --repo forgeplus -n home - -# Pass --project-id explicitly to skip the extra lookup request gitlink-cli wiki +view --owner Gitlink --repo forgeplus --project-id 12345 -n home # Create a wiki page @@ -325,13 +319,6 @@ gitlink-cli member +role --owner Gitlink --repo forgeplus --user-id 101 --role D # Create an invite link gitlink-cli member +invite-link --owner Gitlink --repo forgeplus --role developer --apply true - -# Apply to join a project by invite code -gitlink-cli member +apply --code MPzQgH --role developer --dry-run - -# Quit a repository membership -gitlink-cli member +quit --owner Gitlink --repo forgeplus --dry-run -gitlink-cli member +quit --owner Gitlink --repo forgeplus --yes ``` ### Issue Management @@ -355,9 +342,6 @@ gitlink-cli issue +update --owner Gitlink --repo forgeplus --number 123 --priori # Close an issue gitlink-cli issue +close --owner Gitlink --repo forgeplus -i 123 -# Delete an issue (destructive; requires --yes) -gitlink-cli issue +delete --owner Gitlink --repo forgeplus --number 123 --yes - # Preview batch close without changing data gitlink-cli issue +batch-close --owner Gitlink --repo forgeplus --numbers 123,124 --dry-run @@ -430,10 +414,6 @@ gitlink-cli pr +create --owner Gitlink --repo forgeplus -t "feat: New feature" - # View a PR gitlink-cli pr +view --owner Gitlink --repo forgeplus -i 42 -# Check out a pull request branch locally (mirrors `gh pr checkout`; run inside a git clone) -gitlink-cli pr +checkout --owner Gitlink --repo forgeplus -i 42 -gitlink-cli pr +checkout --owner Gitlink --repo forgeplus -i 42 -b review-42 - # Merge a PR gitlink-cli pr +merge --owner Gitlink --repo forgeplus -i 42 @@ -463,12 +443,6 @@ gitlink-cli pr +review --owner Gitlink --repo forgeplus -i 42 --status approved # List branches gitlink-cli branch +list --owner Gitlink --repo forgeplus -# Filter branches by name keyword -gitlink-cli branch +list --owner Gitlink --repo forgeplus -k feature - -# List all branch names without pagination -gitlink-cli branch +all --owner Gitlink --repo forgeplus - # Create a branch gitlink-cli branch +create --name feature/new-feature @@ -705,21 +679,6 @@ gitlink-cli dataset +delete-attachment --owner me --repo proj --uuid --ye > published OpenAPI contract but are not yet deployed on production (they return > 404 there); they will work once the platform enables them. -### Doctor (Self-Diagnostics) - -```bash -# Run all checks: config file, config values, auth, repo context, API connectivity -gitlink-cli doctor - -# Structured output for scripts / AI agents -gitlink-cli doctor --format json - -# Offline mode: skip authenticated API connectivity checks -gitlink-cli doctor --skip-network -``` - -Each check reports `ok` / `warning` / `error` with a fix `suggestion`. Warnings do not block usage. - ### Raw API For endpoints not covered by shortcuts, use the Raw API directly: @@ -741,26 +700,6 @@ Get-Content issue.json | gitlink-cli api POST /Gitlink/forgeplus/issues --body-s gitlink-cli api GET /Gitlink/forgeplus/commits --query 'page=1&limit=5' ``` -### Shell Completion - -`gitlink-cli` ships with built-in shell completion (bash / zsh / fish / PowerShell): - -```bash -# Bash (add to ~/.bashrc) -source <(gitlink-cli completion bash) - -# Zsh (add to ~/.zshrc) -source <(gitlink-cli completion zsh) - -# Fish -gitlink-cli completion fish | source - -# PowerShell -gitlink-cli completion powershell | Out-String | Invoke-Expression -``` - -Run `gitlink-cli completion --help` for install-once instructions per shell. - ## Global Parameters | Parameter | Description | Example | @@ -936,7 +875,3 @@ See [skills/gitlink-shared/references/api-reference.md](./skills/gitlink-shared/ ## License [MulanPSL-2.0](https://license.coscl.org.cn/MulanPSL2) - -# Low-level git objects: tree entries and blob content by SHA -gitlink-cli repo +git-tree --owner myname --repo myrepo -s master --recursive -gitlink-cli repo +blob --owner myname --repo myrepo -s --decode diff --git a/internal/i18n/locales/en-US.json b/internal/i18n/locales/en-US.json index 49ee36d..528268a 100644 --- a/internal/i18n/locales/en-US.json +++ b/internal/i18n/locales/en-US.json @@ -82,6 +82,7 @@ "cmd.release.list.short": "List releases", "cmd.release.short": "Release operations", "cmd.release.view.short": "View release details", + "cmd.repo.blame.short": "Show line-by-line blame for a file", "cmd.repo.create.short": "Create a new repository", "cmd.repo.delete.short": "Delete a repository", "cmd.repo.fork.short": "Fork a repository", @@ -212,6 +213,7 @@ "flag.release.tag": "Tag name", "flag.release.target": "Target branch", "flag.repo": "Repository name (auto-detected from git remote)", + "flag.repo.blame.path": "File path to blame", "flag.repo.category": "Filter: manage/mirror/sync/fork/all (default: manage)", "flag.repo.description": "Repository description", "flag.repo.name": "Repository name", diff --git a/internal/i18n/locales/zh-CN.json b/internal/i18n/locales/zh-CN.json index 9e43372..098da22 100644 --- a/internal/i18n/locales/zh-CN.json +++ b/internal/i18n/locales/zh-CN.json @@ -82,6 +82,7 @@ "cmd.release.list.short": "列出发布", "cmd.release.short": "发布操作", "cmd.release.view.short": "查看发布详情", + "cmd.repo.blame.short": "逐行显示文件的 blame 信息", "cmd.repo.create.short": "创建新仓库", "cmd.repo.delete.short": "删除仓库", "cmd.repo.fork.short": "Fork 仓库", @@ -212,6 +213,7 @@ "flag.release.tag": "标签名称", "flag.release.target": "目标分支", "flag.repo": "仓库名称(自动从 git remote 检测)", + "flag.repo.blame.path": "要 blame 的文件路径", "flag.repo.category": "筛选:manage/mirror/sync/fork/all(默认:manage)", "flag.repo.description": "仓库描述", "flag.repo.name": "仓库名称", diff --git a/shortcuts/repo/repo.go b/shortcuts/repo/repo.go index ce53461..769a380 100644 --- a/shortcuts/repo/repo.go +++ b/shortcuts/repo/repo.go @@ -109,82 +109,28 @@ func Shortcuts(translators ...*i18n.Translator) []*common.Shortcut { }, }, { - Name: "topics", - Description: tr.T("cmd.repo.topics.short"), + Name: "blame", + Description: tr.T("cmd.repo.blame.short"), Flags: []common.Flag{ - {Name: "keyword", Short: "k", Usage: tr.T("flag.repo.topics.keyword")}, - {Name: "page", Short: "p", Usage: tr.T("flag.page"), Default: "1"}, - {Name: "limit", Short: "l", Usage: tr.T("flag.limit"), Default: "20"}, - }, - Run: func(ctx *common.RuntimeContext) error { - q := url.Values{} - q.Set("page", ctx.Arg("page")) - q.Set("limit", ctx.Arg("limit")) - if keyword := ctx.Arg("keyword"); keyword != "" { - q.Set("keyword", keyword) - } - env, err := ctx.CallAPIWithQuery("GET", "/v1/project_topics", q) - if err != nil { - return err - } - return ctx.Output(env) - }, - }, - { - Name: "topic-add", - Description: tr.T("cmd.repo.topic_add.short"), - Flags: []common.Flag{ - {Name: "name", Short: "n", Usage: tr.T("flag.repo.topic.name"), Required: true}, - {Name: "project-id", Usage: tr.T("flag.repo.project_id")}, + {Name: "path", Short: "p", Usage: tr.T("flag.repo.blame.path"), Required: true}, + {Name: "ref", Short: "r", Usage: tr.T("flag.repo.tree.ref"), Default: "master"}, }, Run: func(ctx *common.RuntimeContext) error { if err := ctx.ResolveOwnerRepo(); err != nil { return err } - name, err := ctx.RequireArg("name") + path, err := ctx.RequireArg("path") if err != nil { return err } - projectID, err := resolveRepoProjectID(ctx) - if err != nil { - return err - } - payload := map[string]interface{}{ - "name": name, - "project_id": projectID, - } - env, err := ctx.CallAPI("POST", "/v1/project_topics", payload) - if err != nil { - return err - } - return ctx.Output(env) - }, - }, - { - Name: "topic-remove", - Description: tr.T("cmd.repo.topic_remove.short"), - Flags: []common.Flag{ - {Name: "topic-id", Short: "t", Usage: tr.T("flag.repo.topic.id"), Required: true}, - {Name: "project-id", Usage: tr.T("flag.repo.project_id")}, - }, - Run: func(ctx *common.RuntimeContext) error { - if err := ctx.ResolveOwnerRepo(); err != nil { - return err - } - topicID, err := ctx.RequireArg("topic-id") - if err != nil { - return err - } - if _, err := strconv.Atoi(topicID); err != nil { - return fmt.Errorf("--topic-id must be an integer, got %q", topicID) - } - projectID, err := resolveRepoProjectID(ctx) - if err != nil { - return err + ref := ctx.Arg("ref") + if ref == "" { + ref = "master" } q := url.Values{} - q.Set("project_id", projectID) - env, err := ctx.CallAPIWithQuery("DELETE", "/v1/project_topics/"+topicID, q) + q.Set("filepath", path) + q.Set("sha", ref) + env, err := ctx.CallAPIWithQuery("GET", "/v1"+ctx.RepoPath()+"/blame", q) if err != nil { return err } diff --git a/shortcuts/repo/repo_test.go b/shortcuts/repo/repo_test.go index d5e4ff2..c8eac94 100644 --- a/shortcuts/repo/repo_test.go +++ b/shortcuts/repo/repo_test.go @@ -42,15 +42,6 @@ func findShortcut(t *testing.T, name string) *common.Shortcut { return nil } -func decodeJSON(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 -} - func writeJSON(t *testing.T, w http.ResponseWriter, v interface{}) { t.Helper() w.Header().Set("Content-Type", "application/json") @@ -645,58 +636,34 @@ func assertEqual(t *testing.T, got interface{}, want interface{}) { } } -func TestRepoTopicsListsWithKeyword(t *testing.T) { +func TestRepoBlameUsesBlameEndpoint(t *testing.T) { var query string server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - if r.Method != "GET" || r.URL.Path != "/v1/project_topics.json" { + if r.Method != "GET" || r.URL.Path != "/v1/owner/repo/blame.json" { t.Fatalf("unexpected request: %s %s", r.Method, r.URL.Path) } query = r.URL.RawQuery - writeJSON(t, w, map[string]interface{}{"total_count": 1, "project_topics": []interface{}{}}) + writeJSON(t, w, map[string]interface{}{"file_name": "README.md", "blame_parts": []interface{}{}}) })) defer server.Close() - err := runShortcut(t, server, "topics", map[string]string{"page": "1", "limit": "20", "keyword": "go"}) + err := runShortcut(t, server, "blame", map[string]string{"path": "README.md", "ref": "develop"}) if err != nil { - t.Fatalf("topics failed: %v", err) + t.Fatalf("blame failed: %v", err) } - if !strings.Contains(query, "keyword=go") { - t.Fatalf("expected keyword in query, got %q", query) + if !strings.Contains(query, "filepath=README.md") || !strings.Contains(query, "sha=develop") { + t.Fatalf("unexpected query: %q", query) } } -func TestRepoTopicAddResolvesProjectID(t *testing.T) { - var payload map[string]interface{} - server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - switch { - case r.Method == "GET" && r.URL.Path == "/owner/repo.json": - writeJSON(t, w, map[string]interface{}{"id": float64(42)}) - case r.Method == "POST" && r.URL.Path == "/v1/project_topics.json": - payload = decodeJSON(t, r) - writeJSON(t, w, map[string]interface{}{"status": float64(0)}) - default: - t.Fatalf("unexpected request: %s %s", r.Method, r.URL.Path) - } - })) - defer server.Close() - - err := runShortcut(t, server, "topic-add", map[string]string{"name": "golang"}) - if err != nil { - t.Fatalf("topic-add failed: %v", err) - } - if payload["name"] != "golang" || payload["project_id"] != "42" { - t.Fatalf("unexpected payload: %#v", payload) - } -} - -func TestRepoTopicRemoveRejectsNonIntegerID(t *testing.T) { +func TestRepoBlameRequiresPath(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { t.Fatalf("unexpected request: %s %s", r.Method, r.URL.Path) })) defer server.Close() - err := runShortcut(t, server, "topic-remove", map[string]string{"topic-id": "abc"}) + err := runShortcut(t, server, "blame", map[string]string{}) if err == nil { - t.Fatal("expected error for non-integer --topic-id") + t.Fatal("expected error when --path missing") } }