merge: resolve client.go conflict, keep XML declaration fix
This commit is contained in:
commit
9b08029643
|
|
@ -0,0 +1,41 @@
|
|||
version: 2
|
||||
name: gitlink_cli_ci
|
||||
description: "gitlink-cli 代码提交时自动执行 CI 检查(构建、测试、格式化)"
|
||||
trigger:
|
||||
webhook: gitlink@1.0.0
|
||||
event:
|
||||
- ref: push
|
||||
ruleset-operator: AND
|
||||
global:
|
||||
concurrent: 1
|
||||
workflow:
|
||||
- ref: start
|
||||
name: 开始
|
||||
task: start
|
||||
- ref: git_clone_0
|
||||
name: 拉取代码
|
||||
task: git_clone@1.2.9
|
||||
input:
|
||||
remote_url: '"https://gitlink.org.cn/jiangtx/gitlink-cli.git"'
|
||||
ref: '"refs/heads/jtx_branch"'
|
||||
commit_id: '""'
|
||||
depth: 1
|
||||
needs:
|
||||
- start
|
||||
- ref: ssh_cmd_0
|
||||
name: CI 检查
|
||||
task: ssh_cmd@1.1.1
|
||||
input:
|
||||
ssh_pass: ((gitlink_cli_ci.ssh_pass))
|
||||
ssh_ip: '"121.41.212.97"'
|
||||
ssh_port: '"22"'
|
||||
ssh_user: '"root"'
|
||||
ssh_cmd: >-
|
||||
"cd /root && rm -rf gitlink-cli && git clone --depth=1 -b jtx_branch https://gitlink.org.cn/jiangtx/gitlink-cli.git && cd gitlink-cli && export PATH=$PATH:/usr/local/go/bin && export GOPROXY=https://goproxy.cn,direct && go version && go build ./... && go vet ./... && go test -race ./... && output=$(gofmt -s -l .) && if [ -n \"$output\" ]; then echo '格式化检查失败:' && echo \"$output\" && exit 1; fi && echo '所有 CI 检查通过'"
|
||||
needs:
|
||||
- git_clone_0
|
||||
- ref: end
|
||||
name: 结束
|
||||
task: end
|
||||
needs:
|
||||
- ssh_cmd_0
|
||||
|
|
@ -15,7 +15,7 @@ jobs:
|
|||
|
||||
- uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: '1.22'
|
||||
go-version: '1.26.1'
|
||||
|
||||
- name: Build
|
||||
run: go build ./...
|
||||
|
|
|
|||
|
|
@ -107,7 +107,7 @@ func (c *Client) Do(method, path string, body interface{}, query url.Values) (*o
|
|||
}
|
||||
}
|
||||
|
||||
// Detect HTML response (avoid returning login page as normal data)
|
||||
// Detect HTML responses (GitLink returns login pages when auth is missing)
|
||||
if detectHTMLResponse(respData) {
|
||||
msg := "服务器返回了 HTML 页面而非 JSON 数据"
|
||||
suggestion := suggestHTMLFix()
|
||||
|
|
@ -122,7 +122,6 @@ func (c *Client) Do(method, path string, body interface{}, query url.Values) (*o
|
|||
// Parse JSON
|
||||
var raw map[string]interface{}
|
||||
if err := json.Unmarshal(respData, &raw); err != nil {
|
||||
// Not JSON, return as-is
|
||||
return output.SuccessEnvelope(string(respData), nil), nil
|
||||
}
|
||||
|
||||
|
|
@ -262,3 +261,4 @@ func suggestFix(code int) string {
|
|||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import (
|
|||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
|
|
@ -525,3 +526,67 @@ func TestShouldAppendJSONSuffixSkipsExistingJSONPath(t *testing.T) {
|
|||
t.Fatal("existing .json path should not get another suffix")
|
||||
}
|
||||
}
|
||||
|
||||
func TestDetectHTMLResponse(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
body string
|
||||
wantHTML bool
|
||||
}{
|
||||
{"正常 JSON", `{"key":"value"}`, false},
|
||||
{"DOCTYPE 开头", `<!DOCTYPE html><html>...</html>`, true},
|
||||
{"html 小写开头", `<html><head>...</head></html>`, true},
|
||||
{"HTML 大写开头", `<HTML><HEAD>...</HEAD></HTML>`, true},
|
||||
{"doctype 小写开头", `<!doctype html><html lang="en">`, true},
|
||||
{"空响应体", "", false},
|
||||
{"纯文本", `just some text`, false},
|
||||
{"空白后 HTML", ` <!DOCTYPE html>`, true},
|
||||
{"JSON 数组", `[1,2,3]`, false},
|
||||
{"HTML 片段(无前缀)", `<body>content</body>`, false},
|
||||
{"XML 声明后跟 HTML", `<?xml version="1.0"?><!DOCTYPE html>`, true},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := detectHTMLResponse([]byte(tt.body)); got != tt.wantHTML {
|
||||
t.Errorf("detectHTMLResponse(%q) = %v, want %v", tt.body, got, tt.wantHTML)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestClientDoHTMLResponse(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "text/html")
|
||||
w.Write([]byte(`<!DOCTYPE html><html><head><title>Sign in</title></head><body>Please log in</body></html>`))
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
c := &Client{HTTP: server.Client(), BaseURL: server.URL}
|
||||
env, err := c.Do("GET", "/api/test", nil, nil)
|
||||
if err == nil {
|
||||
t.Fatal("expected error for HTML response")
|
||||
}
|
||||
if env == nil {
|
||||
t.Fatal("expected envelope for HTML response")
|
||||
}
|
||||
if env.OK {
|
||||
t.Fatal("expected OK=false for HTML response")
|
||||
}
|
||||
apiErr, ok := err.(*APIError)
|
||||
if !ok {
|
||||
t.Fatalf("expected *APIError, got %T", err)
|
||||
}
|
||||
if apiErr.Code != "HTML_RESPONSE" {
|
||||
t.Fatalf("Code = %v, want HTML_RESPONSE", apiErr.Code)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSuggestHTMLFix(t *testing.T) {
|
||||
msg := suggestHTMLFix()
|
||||
if msg == "" {
|
||||
t.Fatal("suggestHTMLFix should return a non-empty message")
|
||||
}
|
||||
if !strings.Contains(msg, "gitlink-cli auth login") {
|
||||
t.Fatal("suggestHTMLFix should mention auth login")
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -380,6 +380,72 @@ func Shortcuts(translators ...*i18n.Translator) []*common.Shortcut {
|
|||
return ctx.Output(env)
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "commits",
|
||||
Description: "List commits in a pull request",
|
||||
Flags: []common.Flag{
|
||||
{Name: "id", Short: "i", Usage: "PR number", 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", prV1Path(ctx, id)+"/commits", nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return ctx.Output(env)
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "branches",
|
||||
Description: "List branches for pull request creation",
|
||||
Flags: []common.Flag{},
|
||||
Run: func(ctx *common.RuntimeContext) error {
|
||||
if err := ctx.ResolveOwnerRepo(); err != nil {
|
||||
return err
|
||||
}
|
||||
env, err := ctx.CallAPI("GET", ctx.RepoPath()+"/pulls/get_branches", nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return ctx.Output(env)
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "check-merge",
|
||||
Description: "Check if two branches can be merged",
|
||||
Flags: []common.Flag{
|
||||
{Name: "head", Usage: "Source branch", Required: true},
|
||||
{Name: "base", Usage: "Target branch", Required: true},
|
||||
},
|
||||
Run: func(ctx *common.RuntimeContext) error {
|
||||
if err := ctx.ResolveOwnerRepo(); err != nil {
|
||||
return err
|
||||
}
|
||||
head, err := ctx.RequireArg("head")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
base, err := ctx.RequireArg("base")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
payload := map[string]interface{}{
|
||||
"head": head,
|
||||
"base": base,
|
||||
}
|
||||
env, err := ctx.CallAPI("POST", ctx.RepoPath()+"/pulls/check_can_merge", payload)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return ctx.Output(env)
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -426,6 +426,120 @@ func TestPRDiffHTTPError(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 != "/v1/owner/repo/pulls/42/commits.json" {
|
||||
t.Fatalf("unexpected path: %s", r.URL.Path)
|
||||
}
|
||||
writeJSON(t, w, []interface{}{
|
||||
map[string]interface{}{"sha": "abc1234", "message": "fix: bug"},
|
||||
})
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
err := runPRShortcut(t, server, "commits", map[string]string{"id": "42"})
|
||||
if err != nil {
|
||||
t.Fatalf("commits failed: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPRCommitsHTTPError(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
w.Write([]byte("server error"))
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
err := runPRShortcut(t, server, "commits", map[string]string{"id": "42"})
|
||||
if err == nil {
|
||||
t.Fatal("expected error for HTTP 500")
|
||||
}
|
||||
}
|
||||
|
||||
// --- branches ---
|
||||
|
||||
func TestPRBranches(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/get_branches.json" {
|
||||
t.Fatalf("unexpected path: %s", r.URL.Path)
|
||||
}
|
||||
writeJSON(t, w, []interface{}{
|
||||
map[string]interface{}{"name": "master"},
|
||||
map[string]interface{}{"name": "develop"},
|
||||
})
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
err := runPRShortcut(t, server, "branches", map[string]string{})
|
||||
if err != nil {
|
||||
t.Fatalf("branches failed: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPRBranchesHTTPError(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
w.Write([]byte("server error"))
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
err := runPRShortcut(t, server, "branches", map[string]string{})
|
||||
if err == nil {
|
||||
t.Fatal("expected error for HTTP 500")
|
||||
}
|
||||
}
|
||||
|
||||
// --- check-merge ---
|
||||
|
||||
func TestPRCheckMerge(t *testing.T) {
|
||||
var payload map[string]interface{}
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != "POST" {
|
||||
t.Fatalf("expected POST, got %s", r.Method)
|
||||
}
|
||||
if r.URL.Path != "/owner/repo/pulls/check_can_merge.json" {
|
||||
t.Fatalf("unexpected path: %s", r.URL.Path)
|
||||
}
|
||||
payload = decodeJSON(t, r)
|
||||
writeJSON(t, w, map[string]interface{}{"can_merge": true})
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
err := runPRShortcut(t, server, "check-merge", map[string]string{
|
||||
"head": "feature/x",
|
||||
"base": "master",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("check-merge failed: %v", err)
|
||||
}
|
||||
assertEqual(t, payload["head"], "feature/x")
|
||||
assertEqual(t, payload["base"], "master")
|
||||
}
|
||||
|
||||
func TestPRCheckMergeHTTPError(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
w.Write([]byte("server error"))
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
err := runPRShortcut(t, server, "check-merge", map[string]string{
|
||||
"head": "feature/x",
|
||||
"base": "master",
|
||||
})
|
||||
if err == nil {
|
||||
t.Fatal("expected error for HTTP 500")
|
||||
}
|
||||
}
|
||||
|
||||
func runPRShortcut(t *testing.T, server *httptest.Server, name string, args map[string]string) error {
|
||||
t.Helper()
|
||||
shortcut := findPRShortcut(t, name)
|
||||
|
|
|
|||
|
|
@ -149,6 +149,116 @@ func Shortcuts(translators ...*i18n.Translator) []*common.Shortcut {
|
|||
return ctx.Output(env)
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "languages",
|
||||
Description: "Show language breakdown of a repository",
|
||||
Run: func(ctx *common.RuntimeContext) error {
|
||||
if err := ctx.ResolveOwnerRepo(); err != nil {
|
||||
return err
|
||||
}
|
||||
env, err := ctx.CallAPI("GET", ctx.RepoPath()+"/languages", nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return ctx.Output(env)
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "contributors",
|
||||
Description: "List contributors of a repository",
|
||||
Flags: []common.Flag{
|
||||
{Name: "page", Short: "p", Usage: "Page number", Default: "1"},
|
||||
{Name: "limit", Short: "l", Usage: "Items per page", Default: "20"},
|
||||
},
|
||||
Run: func(ctx *common.RuntimeContext) error {
|
||||
if err := ctx.ResolveOwnerRepo(); err != nil {
|
||||
return err
|
||||
}
|
||||
q := url.Values{}
|
||||
q.Set("page", ctx.Arg("page"))
|
||||
q.Set("limit", ctx.Arg("limit"))
|
||||
env, err := ctx.CallAPIWithQuery("GET", ctx.RepoPath()+"/contributors", q)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return ctx.Output(env)
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "files",
|
||||
Description: "List files in a repository directory",
|
||||
Flags: []common.Flag{
|
||||
{Name: "ref", Short: "r", Usage: "Branch, tag, or commit SHA"},
|
||||
{Name: "path", Short: "p", Usage: "Directory path (default: repository root)"},
|
||||
},
|
||||
Run: func(ctx *common.RuntimeContext) error {
|
||||
if err := ctx.ResolveOwnerRepo(); err != nil {
|
||||
return err
|
||||
}
|
||||
q := url.Values{}
|
||||
if ref := ctx.Arg("ref"); ref != "" {
|
||||
q.Set("ref", ref)
|
||||
}
|
||||
if p := ctx.Arg("path"); p != "" {
|
||||
q.Set("filepath", p)
|
||||
}
|
||||
env, err := ctx.CallAPIWithQuery("GET", ctx.RepoPath()+"/files", q)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return ctx.Output(env)
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "tags",
|
||||
Description: "List tags of a repository",
|
||||
Flags: []common.Flag{
|
||||
{Name: "page", Short: "p", Usage: "Page number", Default: "1"},
|
||||
{Name: "limit", Short: "l", Usage: "Items per page", Default: "20"},
|
||||
},
|
||||
Run: func(ctx *common.RuntimeContext) error {
|
||||
if err := ctx.ResolveOwnerRepo(); err != nil {
|
||||
return err
|
||||
}
|
||||
q := url.Values{}
|
||||
q.Set("page", ctx.Arg("page"))
|
||||
q.Set("limit", ctx.Arg("limit"))
|
||||
env, err := ctx.CallAPIWithQuery("GET", ctx.RepoPath()+"/tags", q)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return ctx.Output(env)
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "commits",
|
||||
Description: "List commits of a repository",
|
||||
Flags: []common.Flag{
|
||||
{Name: "sha", Short: "s", Usage: "Branch name, tag, or commit SHA"},
|
||||
{Name: "path", Short: "p", Usage: "Filter commits by file path"},
|
||||
{Name: "page", Short: "P", Usage: "Page number", Default: "1"},
|
||||
{Name: "limit", Short: "l", Usage: "Items per page", Default: "20"},
|
||||
},
|
||||
Run: func(ctx *common.RuntimeContext) error {
|
||||
if err := ctx.ResolveOwnerRepo(); err != nil {
|
||||
return err
|
||||
}
|
||||
q := url.Values{}
|
||||
q.Set("page", ctx.Arg("page"))
|
||||
q.Set("limit", ctx.Arg("limit"))
|
||||
if sha := ctx.Arg("sha"); sha != "" {
|
||||
q.Set("sha", sha)
|
||||
}
|
||||
if p := ctx.Arg("path"); p != "" {
|
||||
q.Set("path", p)
|
||||
}
|
||||
env, err := ctx.CallAPIWithQuery("GET", ctx.RepoPath()+"/commits", q)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return ctx.Output(env)
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -300,3 +300,247 @@ func TestRepoCreateUserNoLogin(t *testing.T) {
|
|||
t.Fatal("expected error when user response has no login")
|
||||
}
|
||||
}
|
||||
|
||||
// --- languages ---
|
||||
|
||||
func TestRepoLanguages(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/languages.json" {
|
||||
t.Fatalf("unexpected path: %s", r.URL.Path)
|
||||
}
|
||||
writeJSON(w, map[string]interface{}{
|
||||
"Go": float64(85.5),
|
||||
"Shell": float64(14.5),
|
||||
})
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
err := runShortcut(t, server, "languages", nil)
|
||||
if err != nil {
|
||||
t.Fatalf("languages failed: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRepoLanguagesHTTPError(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
w.Write([]byte("server error"))
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
err := runShortcut(t, server, "languages", nil)
|
||||
if err == nil {
|
||||
t.Fatal("expected error for HTTP 500")
|
||||
}
|
||||
}
|
||||
|
||||
// --- contributors ---
|
||||
|
||||
func TestRepoContributors(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/contributors.json" {
|
||||
t.Fatalf("unexpected path: %s", r.URL.Path)
|
||||
}
|
||||
if r.URL.Query().Get("page") != "1" {
|
||||
t.Fatalf("expected page=1, got %s", r.URL.Query().Get("page"))
|
||||
}
|
||||
if r.URL.Query().Get("limit") != "20" {
|
||||
t.Fatalf("expected limit=20, got %s", r.URL.Query().Get("limit"))
|
||||
}
|
||||
writeJSON(w, map[string]interface{}{
|
||||
"total_count": float64(1),
|
||||
"data": []interface{}{map[string]interface{}{"login": "alice", "contributions": float64(42)}},
|
||||
})
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
err := runShortcut(t, server, "contributors", map[string]string{"page": "1", "limit": "20"})
|
||||
if err != nil {
|
||||
t.Fatalf("contributors failed: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRepoContributorsHTTPError(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
w.Write([]byte("server error"))
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
err := runShortcut(t, server, "contributors", map[string]string{"page": "1", "limit": "20"})
|
||||
if err == nil {
|
||||
t.Fatal("expected error for HTTP 500")
|
||||
}
|
||||
}
|
||||
|
||||
// --- files ---
|
||||
|
||||
func TestRepoFiles(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/files.json" {
|
||||
t.Fatalf("unexpected path: %s", r.URL.Path)
|
||||
}
|
||||
writeJSON(w, []interface{}{
|
||||
map[string]interface{}{"name": "README.md", "type": "file"},
|
||||
map[string]interface{}{"name": "src", "type": "dir"},
|
||||
})
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
err := runShortcut(t, server, "files", nil)
|
||||
if err != nil {
|
||||
t.Fatalf("files failed: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRepoFilesWithRef(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.Query().Get("ref") != "main" {
|
||||
t.Fatalf("expected ref=main, got %s", r.URL.Query().Get("ref"))
|
||||
}
|
||||
if r.URL.Query().Get("filepath") != "src" {
|
||||
t.Fatalf("expected filepath=src, got %s", r.URL.Query().Get("filepath"))
|
||||
}
|
||||
writeJSON(w, []interface{}{})
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
err := runShortcut(t, server, "files", map[string]string{"ref": "main", "path": "src"})
|
||||
if err != nil {
|
||||
t.Fatalf("files with ref failed: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRepoFilesHTTPError(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
w.Write([]byte("server error"))
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
err := runShortcut(t, server, "files", nil)
|
||||
if err == nil {
|
||||
t.Fatal("expected error for HTTP 500")
|
||||
}
|
||||
}
|
||||
|
||||
// --- tags ---
|
||||
|
||||
func TestRepoTags(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/tags.json" {
|
||||
t.Fatalf("unexpected path: %s", r.URL.Path)
|
||||
}
|
||||
if r.URL.Query().Get("page") != "1" {
|
||||
t.Fatalf("expected page=1, got %s", r.URL.Query().Get("page"))
|
||||
}
|
||||
if r.URL.Query().Get("limit") != "20" {
|
||||
t.Fatalf("expected limit=20, got %s", r.URL.Query().Get("limit"))
|
||||
}
|
||||
writeJSON(w, map[string]interface{}{
|
||||
"total_count": float64(1),
|
||||
"data": []interface{}{map[string]interface{}{"name": "v1.0.0"}},
|
||||
})
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
err := runShortcut(t, server, "tags", map[string]string{"page": "1", "limit": "20"})
|
||||
if err != nil {
|
||||
t.Fatalf("tags failed: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRepoTagsHTTPError(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
w.Write([]byte("server error"))
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
err := runShortcut(t, server, "tags", map[string]string{"page": "1", "limit": "20"})
|
||||
if err == nil {
|
||||
t.Fatal("expected error for HTTP 500")
|
||||
}
|
||||
}
|
||||
|
||||
// --- commits ---
|
||||
|
||||
func TestRepoCommits(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/commits.json" {
|
||||
t.Fatalf("unexpected path: %s", r.URL.Path)
|
||||
}
|
||||
if r.URL.Query().Get("page") != "1" {
|
||||
t.Fatalf("expected page=1, got %s", r.URL.Query().Get("page"))
|
||||
}
|
||||
if r.URL.Query().Get("limit") != "20" {
|
||||
t.Fatalf("expected limit=20, got %s", r.URL.Query().Get("limit"))
|
||||
}
|
||||
writeJSON(w, map[string]interface{}{
|
||||
"total_count": float64(1),
|
||||
"data": []interface{}{
|
||||
map[string]interface{}{"sha": "abc123", "message": "initial commit"},
|
||||
},
|
||||
})
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
err := runShortcut(t, server, "commits", map[string]string{"page": "1", "limit": "20"})
|
||||
if err != nil {
|
||||
t.Fatalf("commits failed: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRepoCommitsWithFilters(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.Query().Get("sha") != "main" {
|
||||
t.Fatalf("expected sha=main, got %s", r.URL.Query().Get("sha"))
|
||||
}
|
||||
if r.URL.Query().Get("path") != "src/main.go" {
|
||||
t.Fatalf("expected path=src/main.go, got %s", r.URL.Query().Get("path"))
|
||||
}
|
||||
writeJSON(w, map[string]interface{}{
|
||||
"total_count": float64(1),
|
||||
"data": []interface{}{},
|
||||
})
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
err := runShortcut(t, server, "commits", map[string]string{
|
||||
"sha": "main",
|
||||
"path": "src/main.go",
|
||||
"page": "1",
|
||||
"limit": "20",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("commits with filters failed: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRepoCommitsHTTPError(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
w.Write([]byte("server error"))
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
err := runShortcut(t, server, "commits", map[string]string{"page": "1", "limit": "20"})
|
||||
if err == nil {
|
||||
t.Fatal("expected error for HTTP 500")
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ package user
|
|||
|
||||
import (
|
||||
"fmt"
|
||||
"net/url"
|
||||
|
||||
"github.com/gitlink-org/gitlink-cli/internal/i18n"
|
||||
"github.com/gitlink-org/gitlink-cli/shortcuts/common"
|
||||
|
|
@ -39,6 +40,88 @@ func Shortcuts(translators ...*i18n.Translator) []*common.Shortcut {
|
|||
return ctx.Output(env)
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "heatmap",
|
||||
Description: "Show user contribution heatmap",
|
||||
Flags: []common.Flag{
|
||||
{Name: "login", Short: "l", Usage: "User login name", Required: true},
|
||||
{Name: "year", Short: "y", Usage: "Year (e.g. 2026)"},
|
||||
},
|
||||
Run: func(ctx *common.RuntimeContext) error {
|
||||
login, err := ctx.RequireArg("login")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
path := fmt.Sprintf("/users/%s/headmaps", login)
|
||||
if year := ctx.Arg("year"); year != "" {
|
||||
q := url.Values{}
|
||||
q.Set("year", year)
|
||||
env, err := ctx.CallAPIWithQuery("GET", path, q)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return ctx.Output(env)
|
||||
}
|
||||
env, err := ctx.CallAPI("GET", path, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return ctx.Output(env)
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "stats",
|
||||
Description: "Show user development statistics",
|
||||
Flags: []common.Flag{
|
||||
{Name: "login", Short: "l", Usage: "User login name", Required: true},
|
||||
{Name: "start-time", Usage: "Start date (YYYY-MM-DD)"},
|
||||
{Name: "end-time", Usage: "End date (YYYY-MM-DD)"},
|
||||
},
|
||||
Run: func(ctx *common.RuntimeContext) error {
|
||||
login, err := ctx.RequireArg("login")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
path := fmt.Sprintf("/users/%s/statistics/develop", login)
|
||||
q := url.Values{}
|
||||
if st := ctx.Arg("start-time"); st != "" {
|
||||
q.Set("start_time", st)
|
||||
}
|
||||
if et := ctx.Arg("end-time"); et != "" {
|
||||
q.Set("end_time", et)
|
||||
}
|
||||
if len(q) > 0 {
|
||||
env, err := ctx.CallAPIWithQuery("GET", path, q)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return ctx.Output(env)
|
||||
}
|
||||
env, err := ctx.CallAPI("GET", path, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return ctx.Output(env)
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "trends",
|
||||
Description: "Show user project trends",
|
||||
Flags: []common.Flag{
|
||||
{Name: "login", Short: "l", Usage: "User login name", Required: true},
|
||||
},
|
||||
Run: func(ctx *common.RuntimeContext) error {
|
||||
login, err := ctx.RequireArg("login")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
env, err := ctx.CallAPI("GET", fmt.Sprintf("/users/%s/project_trends", login), nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return ctx.Output(env)
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -119,3 +119,192 @@ func TestUserInfoHTTPError(t *testing.T) {
|
|||
t.Fatal("expected error for HTTP 500")
|
||||
}
|
||||
}
|
||||
|
||||
// --- heatmap ---
|
||||
|
||||
func TestUserHeatmap(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.Path != "/users/alice/headmaps.json" {
|
||||
t.Fatalf("unexpected path: %s", r.URL.Path)
|
||||
}
|
||||
if r.URL.Query().Get("year") != "" {
|
||||
t.Fatalf("expected no year query param, got %s", r.URL.Query().Get("year"))
|
||||
}
|
||||
writeJSON(w, map[string]interface{}{
|
||||
"contributions": []interface{}{
|
||||
map[string]interface{}{"date": "2026-01-01", "count": float64(5)},
|
||||
},
|
||||
})
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
err := runShortcut(t, server, "heatmap", map[string]string{"login": "alice"})
|
||||
if err != nil {
|
||||
t.Fatalf("heatmap failed: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUserHeatmapWithYear(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.Path != "/users/alice/headmaps.json" {
|
||||
t.Fatalf("unexpected path: %s", r.URL.Path)
|
||||
}
|
||||
if r.URL.Query().Get("year") != "2025" {
|
||||
t.Fatalf("expected year=2025, got %s", r.URL.Query().Get("year"))
|
||||
}
|
||||
writeJSON(w, map[string]interface{}{
|
||||
"contributions": []interface{}{},
|
||||
})
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
err := runShortcut(t, server, "heatmap", map[string]string{"login": "alice", "year": "2025"})
|
||||
if err != nil {
|
||||
t.Fatalf("heatmap with year failed: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUserHeatmapMissingLogin(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
t.Fatal("no API call expected")
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
err := runShortcut(t, server, "heatmap", map[string]string{})
|
||||
if err == nil {
|
||||
t.Fatal("expected error for missing login")
|
||||
}
|
||||
}
|
||||
|
||||
func TestUserHeatmapHTTPError(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
w.Write([]byte("server error"))
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
err := runShortcut(t, server, "heatmap", map[string]string{"login": "alice"})
|
||||
if err == nil {
|
||||
t.Fatal("expected error for HTTP 500")
|
||||
}
|
||||
}
|
||||
|
||||
// --- stats ---
|
||||
|
||||
func TestUserStats(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.Path != "/users/alice/statistics/develop.json" {
|
||||
t.Fatalf("unexpected path: %s", r.URL.Path)
|
||||
}
|
||||
if r.URL.Query().Get("start_time") != "" || r.URL.Query().Get("end_time") != "" {
|
||||
t.Fatal("expected no time query params")
|
||||
}
|
||||
writeJSON(w, map[string]interface{}{
|
||||
"pull_request_count": float64(10),
|
||||
"commit_count": float64(42),
|
||||
})
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
err := runShortcut(t, server, "stats", map[string]string{"login": "alice"})
|
||||
if err != nil {
|
||||
t.Fatalf("stats failed: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUserStatsWithTimeRange(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.Path != "/users/alice/statistics/develop.json" {
|
||||
t.Fatalf("unexpected path: %s", r.URL.Path)
|
||||
}
|
||||
if r.URL.Query().Get("start_time") != "2026-01-01" {
|
||||
t.Fatalf("expected start_time=2026-01-01, got %s", r.URL.Query().Get("start_time"))
|
||||
}
|
||||
if r.URL.Query().Get("end_time") != "2026-03-31" {
|
||||
t.Fatalf("expected end_time=2026-03-31, got %s", r.URL.Query().Get("end_time"))
|
||||
}
|
||||
writeJSON(w, map[string]interface{}{
|
||||
"pull_request_count": float64(5),
|
||||
"commit_count": float64(20),
|
||||
})
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
err := runShortcut(t, server, "stats", map[string]string{
|
||||
"login": "alice",
|
||||
"start-time": "2026-01-01",
|
||||
"end-time": "2026-03-31",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("stats with time range failed: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUserStatsMissingLogin(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
t.Fatal("no API call expected")
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
err := runShortcut(t, server, "stats", map[string]string{})
|
||||
if err == nil {
|
||||
t.Fatal("expected error for missing login")
|
||||
}
|
||||
}
|
||||
|
||||
func TestUserStatsHTTPError(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
w.Write([]byte("server error"))
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
err := runShortcut(t, server, "stats", map[string]string{"login": "alice"})
|
||||
if err == nil {
|
||||
t.Fatal("expected error for HTTP 500")
|
||||
}
|
||||
}
|
||||
|
||||
// --- trends ---
|
||||
|
||||
func TestUserTrends(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.Path != "/users/alice/project_trends.json" {
|
||||
t.Fatalf("unexpected path: %s", r.URL.Path)
|
||||
}
|
||||
writeJSON(w, []interface{}{
|
||||
map[string]interface{}{"id": float64(1), "name": "created project"},
|
||||
})
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
err := runShortcut(t, server, "trends", map[string]string{"login": "alice"})
|
||||
if err != nil {
|
||||
t.Fatalf("trends failed: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUserTrendsMissingLogin(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
t.Fatal("no API call expected")
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
err := runShortcut(t, server, "trends", map[string]string{})
|
||||
if err == nil {
|
||||
t.Fatal("expected error for missing login")
|
||||
}
|
||||
}
|
||||
|
||||
func TestUserTrendsHTTPError(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
w.Write([]byte("server error"))
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
err := runShortcut(t, server, "trends", map[string]string{"login": "alice"})
|
||||
if err == nil {
|
||||
t.Fatal("expected error for HTTP 500")
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue