feat(trace): 新增代码溯源分析快捷命令
This commit is contained in:
parent
71ca2bb683
commit
c9d0aad59a
21
README.md
21
README.md
|
|
@ -115,6 +115,7 @@ The official [GitLink](https://www.gitlink.org.cn) CLI tool — built for humans
|
|||
| ⚙️ Pipeline | Run, inspect, enable, disable, delete pipeline workflows and logs |
|
||||
| 🔔 Webhook | Manage repo webhooks and test deliveries |
|
||||
| 📖 Wiki | List, view, create, update, and delete wiki pages |
|
||||
| 🧭 Trace | Initialize code trace analysis, start scans, list results, fetch reports |
|
||||
| 🔍 Search | Search repositories, users |
|
||||
| 📊 Dataset | Query research datasets by project |
|
||||
| 👤 User | View user profiles and info |
|
||||
|
|
@ -296,6 +297,24 @@ gitlink-cli wiki +update --owner Gitlink --repo forgeplus --project-id 12345 -n
|
|||
gitlink-cli wiki +delete --owner Gitlink --repo forgeplus --project-id 12345 -n old-page
|
||||
```
|
||||
|
||||
### Code Trace Analysis
|
||||
|
||||
```bash
|
||||
# Initialize code trace analysis for the current account
|
||||
gitlink-cli trace +init
|
||||
|
||||
# Start a trace scan for a repository branch
|
||||
gitlink-cli trace +start --owner Gitlink --repo forgeplus --branch master --dry-run
|
||||
gitlink-cli trace +start --owner Gitlink --repo forgeplus --branch master
|
||||
|
||||
# List scan results and fetch a generated report
|
||||
gitlink-cli trace +results --owner Gitlink --repo forgeplus --page 1 --limit 20
|
||||
gitlink-cli trace +report --owner Gitlink --repo forgeplus --task-id 12345
|
||||
|
||||
# Re-run analysis for an existing project result
|
||||
gitlink-cli trace +rescan --owner Gitlink --repo forgeplus --project-id 67890 --dry-run
|
||||
```
|
||||
|
||||
### Member Management
|
||||
|
||||
```bash
|
||||
|
|
@ -745,6 +764,7 @@ See [skills/README.md](./skills/README.md) for details.
|
|||
| `gitlink-release` | Release management (create, edit, update, view, delete, etc.) |
|
||||
| `gitlink-ci` | CI/CD operations (builds, logs, etc.) |
|
||||
| `gitlink-pipeline` | Pipeline workflow operations (runs, logs, enable, disable, delete, etc.) |
|
||||
| `gitlink-trace` | Code trace analysis (initialize account, start scans, results, reports) |
|
||||
| `gitlink-search` | Search (repositories, users, etc.) |
|
||||
| `gitlink-org` | Organization management (members, teams, etc.) |
|
||||
| `gitlink-user` | User management (profile info, etc.) |
|
||||
|
|
@ -779,6 +799,7 @@ gitlink-cli/
|
|||
│ ├── org/ # Organization shortcuts
|
||||
│ ├── ci/ # CI shortcuts
|
||||
│ ├── pipeline/ # Pipeline shortcuts
|
||||
│ ├── trace/ # Code trace analysis shortcuts
|
||||
│ ├── search/ # Search shortcuts
|
||||
│ ├── user/ # User shortcuts
|
||||
│ └── register.go # Registration entry point
|
||||
|
|
|
|||
|
|
@ -114,6 +114,7 @@
|
|||
| 🔧 CI | 查看构建、日志、CI/CD 操作 |
|
||||
| ⚙️ Pipeline | 运行、查看、启停、删除流水线工作流并查询日志 |
|
||||
| 📖 Wiki | 列出、查看、创建、更新、删除 Wiki 页面 |
|
||||
| 🧭 代码溯源 | 初始化代码溯源分析、发起扫描、查看结果并获取报告 |
|
||||
| 🔍 搜索 | 搜索仓库、用户 |
|
||||
| 📊 数据集 | 按项目查询科研数据集 |
|
||||
| 👤 用户 | 查看用户资料和信息 |
|
||||
|
|
@ -307,6 +308,24 @@ gitlink-cli wiki +update --owner Gitlink --repo forgeplus --project-id 12345 -n
|
|||
gitlink-cli wiki +delete --owner Gitlink --repo forgeplus --project-id 12345 -n old-page
|
||||
```
|
||||
|
||||
### 代码溯源分析
|
||||
|
||||
```bash
|
||||
# 初始化当前账号的代码溯源分析能力
|
||||
gitlink-cli trace +init
|
||||
|
||||
# 对仓库分支发起代码溯源扫描
|
||||
gitlink-cli trace +start --owner Gitlink --repo forgeplus --branch master --dry-run
|
||||
gitlink-cli trace +start --owner Gitlink --repo forgeplus --branch master
|
||||
|
||||
# 查看扫描结果并获取报告
|
||||
gitlink-cli trace +results --owner Gitlink --repo forgeplus --page 1 --limit 20
|
||||
gitlink-cli trace +report --owner Gitlink --repo forgeplus --task-id 12345
|
||||
|
||||
# 对已有项目结果重新扫描
|
||||
gitlink-cli trace +rescan --owner Gitlink --repo forgeplus --project-id 67890 --dry-run
|
||||
```
|
||||
|
||||
### 成员管理
|
||||
|
||||
```bash
|
||||
|
|
@ -619,6 +638,7 @@ git push gitlink
|
|||
| `gitlink-org` | 组织管理(成员、团队等) |
|
||||
| `gitlink-ci` | CI/CD 操作(构建、日志等) |
|
||||
| `gitlink-pipeline` | 流水线工作流操作(运行、日志、启停、删除等) |
|
||||
| `gitlink-trace` | 代码溯源分析(初始化、发起扫描、查看结果、获取报告) |
|
||||
| `gitlink-search` | 搜索功能(仓库、用户等) |
|
||||
| `gitlink-user` | 用户管理(个人信息等) |
|
||||
| `gitlink-pm` | 项目管理(Sprint、看板、周报等) |
|
||||
|
|
@ -651,6 +671,7 @@ gitlink-cli/
|
|||
│ ├── org/ # 组织 shortcuts
|
||||
│ ├── ci/ # CI shortcuts
|
||||
│ ├── pipeline/ # Pipeline shortcuts
|
||||
│ ├── trace/ # 代码溯源 shortcuts
|
||||
│ ├── search/ # 搜索 shortcuts
|
||||
│ ├── user/ # 用户 shortcuts
|
||||
│ └── register.go # 注册入口
|
||||
|
|
|
|||
|
|
@ -0,0 +1,13 @@
|
|||
# Trace Shortcuts
|
||||
|
||||
新增 `trace` Shortcut 组,封装 GitLink 代码溯源分析相关接口,支持:
|
||||
|
||||
- `trace +init`
|
||||
- `trace +results`
|
||||
- `trace +start`
|
||||
- `trace +rescan`
|
||||
- `trace +report`
|
||||
|
||||
该能力覆盖账号初始化、按分支发起分析、分页查看分析结果、按项目重新扫描以及获取分析报告。`trace +start` 和 `trace +rescan` 支持 `--dry-run`,便于在自动化脚本或 AI Agent 执行前预览实际请求,降低误触发平台任务的风险。
|
||||
|
||||
同时补充了单元测试、README 示例、AI Agent Skill 文档和 i18n 文案,确保命令帮助、中文/英文提示和自动化使用场景保持一致。
|
||||
|
|
@ -93,6 +93,12 @@
|
|||
"cmd.search.repos.short": "Search repositories",
|
||||
"cmd.search.short": "Search operations",
|
||||
"cmd.search.users.short": "Search users",
|
||||
"cmd.trace.init.short": "Initialize code trace analysis account",
|
||||
"cmd.trace.report.short": "Fetch a code trace analysis report",
|
||||
"cmd.trace.rescan.short": "Rescan a code trace analysis result",
|
||||
"cmd.trace.results.short": "List code trace analysis results",
|
||||
"cmd.trace.short": "Code trace analysis operations",
|
||||
"cmd.trace.start.short": "Start code trace analysis for a branch",
|
||||
"cmd.user.info.short": "Show user profile",
|
||||
"cmd.user.me.short": "Show current authenticated user",
|
||||
"cmd.user.short": "User operations",
|
||||
|
|
@ -217,6 +223,9 @@
|
|||
"flag.search.keyword": "Search keyword",
|
||||
"flag.sort_by": "Sort field",
|
||||
"flag.sort_direction": "Sort direction: asc, desc",
|
||||
"flag.trace.branch": "Branch name to analyze",
|
||||
"flag.trace.project_id": "project_id from trace analysis results",
|
||||
"flag.trace.task_id": "task_id from trace analysis results",
|
||||
"flag.user": "User login (default: current user)",
|
||||
"flag.user.login": "User login name",
|
||||
"flag.webhook.active": "Whether the webhook is active: true or false",
|
||||
|
|
|
|||
|
|
@ -93,6 +93,12 @@
|
|||
"cmd.search.repos.short": "搜索仓库",
|
||||
"cmd.search.short": "搜索操作",
|
||||
"cmd.search.users.short": "搜索用户",
|
||||
"cmd.trace.init.short": "初始化代码溯源分析账号",
|
||||
"cmd.trace.report.short": "获取代码溯源分析报告",
|
||||
"cmd.trace.rescan.short": "重新扫描代码溯源分析结果",
|
||||
"cmd.trace.results.short": "列出代码溯源分析结果",
|
||||
"cmd.trace.short": "代码溯源分析操作",
|
||||
"cmd.trace.start.short": "对指定分支发起代码溯源分析",
|
||||
"cmd.user.info.short": "显示用户资料",
|
||||
"cmd.user.me.short": "显示当前认证用户",
|
||||
"cmd.user.short": "用户操作",
|
||||
|
|
@ -217,6 +223,9 @@
|
|||
"flag.search.keyword": "搜索关键词",
|
||||
"flag.sort_by": "排序字段",
|
||||
"flag.sort_direction": "排序方向:asc、desc",
|
||||
"flag.trace.branch": "要分析的分支名称",
|
||||
"flag.trace.project_id": "代码溯源分析结果中的 project_id",
|
||||
"flag.trace.task_id": "代码溯源分析结果中的 task_id",
|
||||
"flag.user": "用户登录名(默认:当前用户)",
|
||||
"flag.user.login": "用户登录名",
|
||||
"flag.webhook.active": "Webhook 是否启用:true 或 false",
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ import (
|
|||
"github.com/gitlink-org/gitlink-cli/shortcuts/release"
|
||||
"github.com/gitlink-org/gitlink-cli/shortcuts/repo"
|
||||
"github.com/gitlink-org/gitlink-cli/shortcuts/search"
|
||||
"github.com/gitlink-org/gitlink-cli/shortcuts/trace"
|
||||
"github.com/gitlink-org/gitlink-cli/shortcuts/user"
|
||||
"github.com/gitlink-org/gitlink-cli/shortcuts/webhook"
|
||||
"github.com/gitlink-org/gitlink-cli/shortcuts/wiki"
|
||||
|
|
@ -57,6 +58,7 @@ func RegisterAll(root *cobra.Command, translators ...*i18n.Translator) {
|
|||
"wiki": wiki.Shortcuts(),
|
||||
"health": health.Shortcuts(tr),
|
||||
"ignore": ignore.Shortcuts(),
|
||||
"trace": trace.Shortcuts(tr),
|
||||
"workflow": workflow.Shortcuts(),
|
||||
}
|
||||
|
||||
|
|
@ -82,6 +84,7 @@ func RegisterAll(root *cobra.Command, translators ...*i18n.Translator) {
|
|||
"wiki": "Wiki page management",
|
||||
"health": "Project health data collection",
|
||||
"ignore": tr.T("cmd.ignore.short"),
|
||||
"trace": tr.T("cmd.trace.short"),
|
||||
"workflow": "AI agent workflow analysis",
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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", "trace",
|
||||
}
|
||||
|
||||
groupSet := map[string]bool{}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,176 @@
|
|||
package trace
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"github.com/gitlink-org/gitlink-cli/internal/i18n"
|
||||
"github.com/gitlink-org/gitlink-cli/shortcuts/common"
|
||||
)
|
||||
|
||||
// Shortcuts returns code trace analysis shortcuts.
|
||||
func Shortcuts(translators ...*i18n.Translator) []*common.Shortcut {
|
||||
tr := shortcutTranslator(translators...)
|
||||
return []*common.Shortcut{
|
||||
{
|
||||
Name: "init",
|
||||
Description: tr.T("cmd.trace.init.short"),
|
||||
Run: func(ctx *common.RuntimeContext) error {
|
||||
env, err := ctx.CallAPI("POST", "/api/traces/trace_users", nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return ctx.Output(env)
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "results",
|
||||
Description: tr.T("cmd.trace.results.short"),
|
||||
Flags: []common.Flag{
|
||||
{Name: "page", Short: "p", Usage: tr.T("flag.page"), Default: "1"},
|
||||
{Name: "limit", Short: "l", Usage: tr.T("flag.limit"), Default: "15"},
|
||||
},
|
||||
Run: func(ctx *common.RuntimeContext) error {
|
||||
if err := ctx.ResolveOwnerRepo(); err != nil {
|
||||
return err
|
||||
}
|
||||
q := url.Values{}
|
||||
setQueryIfPresent(q, "page", ctx.Arg("page"))
|
||||
setQueryIfPresent(q, "limit", ctx.Arg("limit"))
|
||||
env, err := ctx.CallAPIWithQuery("GET", traceRepoPath(ctx)+"/task_results", q)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return ctx.Output(env)
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "start",
|
||||
Description: tr.T("cmd.trace.start.short"),
|
||||
Flags: []common.Flag{
|
||||
{Name: "branch", Short: "b", Usage: tr.T("flag.trace.branch"), Required: true},
|
||||
{Name: "dry-run", Usage: tr.T("flag.dry_run"), Bool: true, Default: "false"},
|
||||
},
|
||||
Run: runStart,
|
||||
},
|
||||
{
|
||||
Name: "rescan",
|
||||
Description: tr.T("cmd.trace.rescan.short"),
|
||||
Flags: []common.Flag{
|
||||
{Name: "project-id", Usage: tr.T("flag.trace.project_id"), Required: true},
|
||||
{Name: "dry-run", Usage: tr.T("flag.dry_run"), Bool: true, Default: "false"},
|
||||
},
|
||||
Run: runRescan,
|
||||
},
|
||||
{
|
||||
Name: "report",
|
||||
Description: tr.T("cmd.trace.report.short"),
|
||||
Flags: []common.Flag{
|
||||
{Name: "task-id", Usage: tr.T("flag.trace.task_id"), Required: true},
|
||||
},
|
||||
Run: func(ctx *common.RuntimeContext) error {
|
||||
if err := ctx.ResolveOwnerRepo(); err != nil {
|
||||
return err
|
||||
}
|
||||
taskID, err := ctx.RequireArg("task-id")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
q := url.Values{}
|
||||
q.Set("task_id", taskID)
|
||||
env, err := ctx.CallAPIWithQuery("GET", traceRepoPath(ctx)+"/task_pdf", q)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return ctx.Output(env)
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func shortcutTranslator(translators ...*i18n.Translator) *i18n.Translator {
|
||||
if len(translators) > 0 && translators[0] != nil {
|
||||
return translators[0]
|
||||
}
|
||||
return i18n.Default()
|
||||
}
|
||||
|
||||
func runStart(ctx *common.RuntimeContext) error {
|
||||
if err := ctx.ResolveOwnerRepo(); err != nil {
|
||||
return err
|
||||
}
|
||||
branch, err := requiredTrimmedArg(ctx, "branch")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
payload := map[string]interface{}{"branch_name": branch}
|
||||
path := traceRepoPath(ctx) + "/tasks"
|
||||
if ctx.Arg("dry-run") == "true" {
|
||||
return ctx.OutputData(traceDryRun(ctx, "POST", path, payload, nil))
|
||||
}
|
||||
env, err := ctx.CallAPI("POST", path, payload)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return ctx.Output(env)
|
||||
}
|
||||
|
||||
func runRescan(ctx *common.RuntimeContext) error {
|
||||
if err := ctx.ResolveOwnerRepo(); err != nil {
|
||||
return err
|
||||
}
|
||||
projectID, err := requiredTrimmedArg(ctx, "project-id")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
path := traceRepoPath(ctx) + "/reload_task"
|
||||
q := url.Values{}
|
||||
q.Set("project_id", projectID)
|
||||
if ctx.Arg("dry-run") == "true" {
|
||||
return ctx.OutputData(traceDryRun(ctx, "GET", path, nil, q))
|
||||
}
|
||||
env, err := ctx.CallAPIWithQuery("GET", path, q)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return ctx.Output(env)
|
||||
}
|
||||
|
||||
func traceRepoPath(ctx *common.RuntimeContext) string {
|
||||
return fmt.Sprintf("/api/traces/%s/%s", ctx.Owner, ctx.Repo)
|
||||
}
|
||||
|
||||
func requiredTrimmedArg(ctx *common.RuntimeContext, name string) (string, error) {
|
||||
value, err := ctx.RequireArg(name)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
value = strings.TrimSpace(value)
|
||||
if value == "" {
|
||||
return "", fmt.Errorf("required flag --%s is missing", name)
|
||||
}
|
||||
return value, nil
|
||||
}
|
||||
|
||||
func traceDryRun(ctx *common.RuntimeContext, method, path string, payload interface{}, query url.Values) map[string]interface{} {
|
||||
result := map[string]interface{}{
|
||||
"repository": fmt.Sprintf("%s/%s", ctx.Owner, ctx.Repo),
|
||||
"dry_run": true,
|
||||
"method": method,
|
||||
"path": path,
|
||||
}
|
||||
if payload != nil {
|
||||
result["payload"] = payload
|
||||
}
|
||||
if len(query) > 0 {
|
||||
result["query"] = query.Encode()
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func setQueryIfPresent(q url.Values, key, value string) {
|
||||
if strings.TrimSpace(value) != "" {
|
||||
q.Set(key, strings.TrimSpace(value))
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,200 @@
|
|||
package trace
|
||||
|
||||
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 runTraceShortcut(t *testing.T, server *httptest.Server, name string, args map[string]string) error {
|
||||
t.Helper()
|
||||
s := findTraceShortcut(t, name)
|
||||
ctx := &common.RuntimeContext{
|
||||
Client: &client.Client{
|
||||
HTTP: server.Client(),
|
||||
BaseURL: server.URL,
|
||||
},
|
||||
Owner: "owner",
|
||||
Repo: "repo",
|
||||
Format: "json",
|
||||
Args: args,
|
||||
}
|
||||
if ctx.Args == nil {
|
||||
ctx.Args = map[string]string{}
|
||||
}
|
||||
return s.Run(ctx)
|
||||
}
|
||||
|
||||
func findTraceShortcut(t *testing.T, name string) *common.Shortcut {
|
||||
t.Helper()
|
||||
for _, s := range Shortcuts() {
|
||||
if s.Name == name {
|
||||
return s
|
||||
}
|
||||
}
|
||||
t.Fatalf("shortcut %q not found", name)
|
||||
return nil
|
||||
}
|
||||
|
||||
func writeJSON(t *testing.T, w http.ResponseWriter, value interface{}) {
|
||||
t.Helper()
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
if err := json.NewEncoder(w).Encode(value); err != nil {
|
||||
t.Fatalf("write response: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
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: %v", err)
|
||||
}
|
||||
return payload
|
||||
}
|
||||
|
||||
func TestTraceInit(t *testing.T) {
|
||||
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 != "/api/traces/trace_users.json" {
|
||||
t.Fatalf("unexpected path: %s", r.URL.Path)
|
||||
}
|
||||
writeJSON(t, w, map[string]interface{}{"status": float64(0), "message": "success"})
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
if err := runTraceShortcut(t, server, "init", nil); err != nil {
|
||||
t.Fatalf("init failed: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTraceResults(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 != "/api/traces/owner/repo/task_results.json" {
|
||||
t.Fatalf("unexpected path: %s", r.URL.Path)
|
||||
}
|
||||
if r.URL.Query().Get("page") != "2" {
|
||||
t.Fatalf("expected page=2, got %s", r.URL.Query().Get("page"))
|
||||
}
|
||||
if r.URL.Query().Get("limit") != "50" {
|
||||
t.Fatalf("expected limit=50, got %s", r.URL.Query().Get("limit"))
|
||||
}
|
||||
writeJSON(t, w, map[string]interface{}{"data": []interface{}{map[string]interface{}{"task_id": "task-1"}}})
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
err := runTraceShortcut(t, server, "results", map[string]string{"page": "2", "limit": "50"})
|
||||
if err != nil {
|
||||
t.Fatalf("results failed: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTraceStart(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 != "/api/traces/owner/repo/tasks.json" {
|
||||
t.Fatalf("unexpected path: %s", r.URL.Path)
|
||||
}
|
||||
payload = decodeJSON(t, r)
|
||||
writeJSON(t, w, map[string]interface{}{"status": float64(0), "message": "success"})
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
err := runTraceShortcut(t, server, "start", map[string]string{"branch": " feature/scan "})
|
||||
if err != nil {
|
||||
t.Fatalf("start failed: %v", err)
|
||||
}
|
||||
if payload["branch_name"] != "feature/scan" {
|
||||
t.Fatalf("unexpected branch_name: %#v", payload["branch_name"])
|
||||
}
|
||||
}
|
||||
|
||||
func TestTraceStartDryRunDoesNotCallAPI(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
t.Fatalf("dry-run should not call API: %s %s", r.Method, r.URL.Path)
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
err := runTraceShortcut(t, server, "start", map[string]string{"branch": "main", "dry-run": "true"})
|
||||
if err != nil {
|
||||
t.Fatalf("start dry-run failed: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTraceStartRejectsBlankBranch(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
t.Fatalf("blank branch should not call API")
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
err := runTraceShortcut(t, server, "start", map[string]string{"branch": " "})
|
||||
if err == nil {
|
||||
t.Fatal("expected blank branch error, got nil")
|
||||
}
|
||||
}
|
||||
|
||||
func TestTraceRescan(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 != "/api/traces/owner/repo/reload_task.json" {
|
||||
t.Fatalf("unexpected path: %s", r.URL.Path)
|
||||
}
|
||||
if r.URL.Query().Get("project_id") != "project-1" {
|
||||
t.Fatalf("unexpected project_id: %s", r.URL.Query().Get("project_id"))
|
||||
}
|
||||
writeJSON(t, w, map[string]interface{}{"status": float64(0), "message": "success"})
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
err := runTraceShortcut(t, server, "rescan", map[string]string{"project-id": " project-1 "})
|
||||
if err != nil {
|
||||
t.Fatalf("rescan failed: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTraceRescanDryRunDoesNotCallAPI(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
t.Fatalf("dry-run should not call API: %s %s", r.Method, r.URL.Path)
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
err := runTraceShortcut(t, server, "rescan", map[string]string{"project-id": "project-1", "dry-run": "true"})
|
||||
if err != nil {
|
||||
t.Fatalf("rescan dry-run failed: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTraceReport(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 != "/api/traces/owner/repo/task_pdf.json" {
|
||||
t.Fatalf("unexpected path: %s", r.URL.Path)
|
||||
}
|
||||
if r.URL.Query().Get("task_id") != "task-1" {
|
||||
t.Fatalf("unexpected task_id: %s", r.URL.Query().Get("task_id"))
|
||||
}
|
||||
writeJSON(t, w, map[string]interface{}{"status": float64(0), "message": "success"})
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
err := runTraceShortcut(t, server, "report", map[string]string{"task-id": "task-1"})
|
||||
if err != nil {
|
||||
t.Fatalf("report failed: %v", err)
|
||||
}
|
||||
}
|
||||
|
|
@ -103,6 +103,8 @@ skills/
|
|||
│ └── SKILL.md # Pipeline 操作指南
|
||||
├── gitlink-wiki/ # Wiki 页面管理
|
||||
│ └── SKILL.md # Wiki 操作指南
|
||||
├── gitlink-trace/ # 代码溯源分析
|
||||
│ └── SKILL.md # 代码溯源分析操作指南
|
||||
├── gitlink-pm/ # 项目管理
|
||||
│ └── SKILL.md # PM 操作指南
|
||||
├── gitlink-health/ # 项目健康度分析
|
||||
|
|
@ -144,6 +146,7 @@ skills/
|
|||
| **gitlink-ci** | CI/CD | `ci +builds`, `ci +logs` |
|
||||
| **gitlink-pipeline** | 流水线工作流 | `pipeline +runs`, `pipeline +run`, `pipeline +logs` |
|
||||
| **gitlink-wiki** | Wiki 页面管理 | `wiki +list`, `wiki +view`, `wiki +create`, `wiki +update`, `wiki +delete` |
|
||||
| **gitlink-trace** | 代码溯源分析 | `trace +init`, `trace +start`, `trace +results`, `trace +report` |
|
||||
| **gitlink-pm** | 项目管理 | 通过 Raw API 访问 |
|
||||
| **gitlink-workflow** | AI 工作流 | Issue 分类、PR Review、Release Notes |
|
||||
| **gitlink-health** | 开源项目健康度 | 详情见SKILL.md |
|
||||
|
|
|
|||
|
|
@ -0,0 +1,52 @@
|
|||
---
|
||||
name: gitlink-trace
|
||||
version: 1.0.0
|
||||
description: "代码溯源分析:初始化账号、发起分支扫描、查看分析结果、重新扫描并获取报告。"
|
||||
metadata:
|
||||
requires:
|
||||
bins: ["gitlink-cli"]
|
||||
cliHelp: "gitlink-cli trace --help"
|
||||
---
|
||||
|
||||
# gitlink-trace(代码溯源分析)
|
||||
|
||||
Read [`../gitlink-shared/SKILL.md`](../gitlink-shared/SKILL.md) first for authentication, global flags, owner/repo resolution, and output formatting.
|
||||
|
||||
**CRITICAL**: `trace +start` and `trace +rescan` trigger platform-side analysis jobs. Use `--dry-run` first when the user is not explicitly asking to start or rerun a scan.
|
||||
|
||||
## Shortcuts
|
||||
|
||||
| Shortcut | 说明 |
|
||||
|----------|------|
|
||||
| `trace +init` | 初始化当前账号的代码溯源分析能力 |
|
||||
| `trace +results` | 查看仓库代码溯源分析结果,支持 `--page` 和 `--limit` |
|
||||
| `trace +start` | 对指定分支发起代码溯源分析 |
|
||||
| `trace +rescan` | 按 `project_id` 重新扫描已有代码溯源结果 |
|
||||
| `trace +report` | 按 `task_id` 获取代码溯源分析报告 |
|
||||
|
||||
## Usage
|
||||
|
||||
```bash
|
||||
# 初始化当前账号
|
||||
gitlink-cli trace +init
|
||||
|
||||
# 查看最近的分析结果
|
||||
gitlink-cli trace +results --owner Gitlink --repo forgeplus --page 1 --limit 20 --format json
|
||||
|
||||
# 先预览,再对 master 分支发起扫描
|
||||
gitlink-cli trace +start --owner Gitlink --repo forgeplus --branch master --dry-run
|
||||
gitlink-cli trace +start --owner Gitlink --repo forgeplus --branch master
|
||||
|
||||
# 重新扫描已有结果
|
||||
gitlink-cli trace +rescan --owner Gitlink --repo forgeplus --project-id 67890 --dry-run
|
||||
gitlink-cli trace +rescan --owner Gitlink --repo forgeplus --project-id 67890
|
||||
|
||||
# 获取分析报告
|
||||
gitlink-cli trace +report --owner Gitlink --repo forgeplus --task-id 12345 --format json
|
||||
```
|
||||
|
||||
## Agent Notes
|
||||
|
||||
- Prefer `--format json` when consuming result IDs for a later `trace +report` or `trace +rescan` command.
|
||||
- Treat `task_id` and `project_id` as platform-provided identifiers from `trace +results`; do not invent them.
|
||||
- If owner/repo are omitted, the CLI can resolve them from the current Git remote, but explicit flags are safer in automation.
|
||||
Loading…
Reference in New Issue