Merge pull request #378: feat(repo): add +blame for file attribution
# Conflicts: # README.md # shortcuts/repo/repo.go # shortcuts/repo/repo_test.go
This commit is contained in:
commit
10f82c10a2
73
README.md
73
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 <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 <shell> --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 <blob-sha> --decode
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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": "仓库名称",
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue