新增仓库洞察快捷命令 #116
|
|
@ -196,6 +196,15 @@ gitlink-cli repo +info --owner Gitlink --repo forgeplus
|
|||
# Read repository README
|
||||
gitlink-cli repo +readme --owner Gitlink --repo forgeplus --ref master
|
||||
|
||||
# Show repository language statistics
|
||||
gitlink-cli repo +languages --owner Gitlink --repo forgeplus
|
||||
|
||||
# List repository contributors
|
||||
gitlink-cli repo +contributors --owner Gitlink --repo forgeplus
|
||||
|
||||
# Show repository code statistics
|
||||
gitlink-cli repo +code-stats --owner Gitlink --repo forgeplus --ref master
|
||||
|
||||
# Create a repository
|
||||
gitlink-cli repo +create -n my-project -d "Project description"
|
||||
|
||||
|
|
|
|||
|
|
@ -207,6 +207,15 @@ gitlink-cli repo +info --owner Gitlink --repo forgeplus
|
|||
# 读取仓库 README
|
||||
gitlink-cli repo +readme --owner Gitlink --repo forgeplus --ref master
|
||||
|
||||
# 查看仓库语言统计
|
||||
gitlink-cli repo +languages --owner Gitlink --repo forgeplus
|
||||
|
||||
# 列出仓库贡献者
|
||||
gitlink-cli repo +contributors --owner Gitlink --repo forgeplus
|
||||
|
||||
# 查看仓库代码统计
|
||||
gitlink-cli repo +code-stats --owner Gitlink --repo forgeplus --ref master
|
||||
|
||||
# 创建仓库
|
||||
gitlink-cli repo +create -n my-project -d "项目描述"
|
||||
|
||||
|
|
|
|||
|
|
@ -59,9 +59,19 @@ func TestLoginTokenFlag(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func setTempHome(t *testing.T) string {
|
||||
t.Helper()
|
||||
dir := t.TempDir()
|
||||
t.Setenv("HOME", dir)
|
||||
t.Setenv("USERPROFILE", dir)
|
||||
t.Setenv("HOMEDRIVE", "")
|
||||
t.Setenv("HOMEPATH", "")
|
||||
return dir
|
||||
}
|
||||
|
||||
func TestStatusCmdNotLoggedIn(t *testing.T) {
|
||||
keyring.MockInitWithError(errors.New("keychain unavailable"))
|
||||
t.Setenv("HOME", t.TempDir())
|
||||
setTempHome(t)
|
||||
t.Setenv("GITLINK_TOKEN", "")
|
||||
_ = internalAuth.DeleteToken()
|
||||
|
||||
|
|
@ -76,7 +86,7 @@ func TestStatusCmdNotLoggedIn(t *testing.T) {
|
|||
|
||||
func TestStatusCmdEnvToken(t *testing.T) {
|
||||
keyring.MockInitWithError(errors.New("keychain unavailable"))
|
||||
t.Setenv("HOME", t.TempDir())
|
||||
setTempHome(t)
|
||||
t.Setenv("GITLINK_TOKEN", "env-token-123")
|
||||
_ = internalAuth.DeleteToken()
|
||||
|
||||
|
|
@ -86,8 +96,7 @@ func TestStatusCmdEnvToken(t *testing.T) {
|
|||
|
||||
func TestStatusCmdStoredToken(t *testing.T) {
|
||||
keyring.MockInitWithError(errors.New("keychain unavailable"))
|
||||
dir := t.TempDir()
|
||||
t.Setenv("HOME", dir)
|
||||
dir := setTempHome(t)
|
||||
t.Setenv("GITLINK_TOKEN", "")
|
||||
|
||||
os.MkdirAll(dir+"/.config/gitlink-cli", 0700)
|
||||
|
|
@ -99,8 +108,7 @@ func TestStatusCmdStoredToken(t *testing.T) {
|
|||
|
||||
func TestStatusCmdEnvAndStoredToken(t *testing.T) {
|
||||
keyring.MockInitWithError(errors.New("keychain unavailable"))
|
||||
dir := t.TempDir()
|
||||
t.Setenv("HOME", dir)
|
||||
dir := setTempHome(t)
|
||||
t.Setenv("GITLINK_TOKEN", "env-token")
|
||||
|
||||
os.MkdirAll(dir+"/.config/gitlink-cli", 0700)
|
||||
|
|
@ -114,7 +122,7 @@ func TestStatusCmdEnvAndStoredToken(t *testing.T) {
|
|||
|
||||
func TestStatusCmdStoredTokenButLoadFails(t *testing.T) {
|
||||
keyring.MockInitWithError(errors.New("keychain unavailable"))
|
||||
t.Setenv("HOME", t.TempDir())
|
||||
setTempHome(t)
|
||||
t.Setenv("GITLINK_TOKEN", "")
|
||||
// Don't create credentials file — LoadToken returns empty
|
||||
|
||||
|
|
@ -126,7 +134,7 @@ func TestStatusCmdStoredTokenButLoadFails(t *testing.T) {
|
|||
|
||||
func TestLogoutCmdError(t *testing.T) {
|
||||
keyring.MockInitWithError(errors.New("keychain unavailable"))
|
||||
t.Setenv("HOME", t.TempDir())
|
||||
setTempHome(t)
|
||||
t.Setenv("GITLINK_TOKEN", "")
|
||||
// Don't create credentials dir — DeleteToken will fail
|
||||
|
||||
|
|
@ -139,8 +147,7 @@ func TestLogoutCmdError(t *testing.T) {
|
|||
|
||||
func TestLogoutCmd(t *testing.T) {
|
||||
keyring.MockInitWithError(errors.New("keychain unavailable"))
|
||||
home := t.TempDir()
|
||||
t.Setenv("HOME", home)
|
||||
home := setTempHome(t)
|
||||
t.Setenv("GITLINK_TOKEN", "")
|
||||
|
||||
// Store a token first so DeleteToken has something to delete
|
||||
|
|
@ -159,8 +166,7 @@ func TestLogoutCmd(t *testing.T) {
|
|||
|
||||
func TestLoginWithToken(t *testing.T) {
|
||||
keyring.MockInitWithError(errors.New("keychain unavailable"))
|
||||
home := t.TempDir()
|
||||
t.Setenv("HOME", home)
|
||||
home := setTempHome(t)
|
||||
t.Setenv("GITLINK_TOKEN", "")
|
||||
|
||||
// Mock stdin
|
||||
|
|
@ -196,7 +202,7 @@ func TestLoginWithToken(t *testing.T) {
|
|||
|
||||
func TestLoginWithTokenEmpty(t *testing.T) {
|
||||
keyring.MockInitWithError(errors.New("keychain unavailable"))
|
||||
t.Setenv("HOME", t.TempDir())
|
||||
setTempHome(t)
|
||||
t.Setenv("GITLINK_TOKEN", "")
|
||||
|
||||
oldStdin := os.Stdin
|
||||
|
|
@ -219,7 +225,7 @@ func TestLoginWithTokenEmpty(t *testing.T) {
|
|||
|
||||
func TestLoginWithPasswordNoTerminal(t *testing.T) {
|
||||
keyring.MockInitWithError(errors.New("keychain unavailable"))
|
||||
t.Setenv("HOME", t.TempDir())
|
||||
setTempHome(t)
|
||||
t.Setenv("GITLINK_TOKEN", "")
|
||||
|
||||
// term.ReadPassword will fail because test has no terminal
|
||||
|
|
|
|||
|
|
@ -0,0 +1,11 @@
|
|||
# 仓库洞察 Shortcut
|
||||
|
||||
本次变更新增一组只读仓库洞察命令,让用户和 AI Agent 不必记忆 Raw API 路径即可快速获取仓库语言、贡献者和代码统计信息。
|
||||
|
||||
新增命令:
|
||||
|
||||
- `repo +languages`:查看仓库语言占比。
|
||||
- `repo +contributors`:列出仓库贡献者及贡献次数。
|
||||
- `repo +code-stats`:查看仓库提交、作者、新增/删除行等代码统计,支持 `--ref` 筛选。
|
||||
|
||||
这些命令都复用现有 `RuntimeContext` 的仓库解析、API 调用和输出封装,保持 Shortcut 实现风格一致。单元测试覆盖了 API path、HTTP method、query 参数透传和错误返回路径,并同步更新 README、中文 README 与 `gitlink-repo` Skill 文档。实现过程中也实测了公开仓库接口,保留稳定可用的仓库洞察能力,没有暴露当前服务端会返回业务失败的贡献者代码行统计接口。
|
||||
|
|
@ -13,6 +13,9 @@ func tempHome(t *testing.T) string {
|
|||
t.Helper()
|
||||
dir := t.TempDir()
|
||||
t.Setenv("HOME", dir)
|
||||
t.Setenv("USERPROFILE", dir)
|
||||
t.Setenv("HOMEDRIVE", "")
|
||||
t.Setenv("HOMEPATH", "")
|
||||
return dir
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -58,9 +58,12 @@
|
|||
"cmd.release.short": "Release operations",
|
||||
"cmd.release.view.short": "View release details",
|
||||
"cmd.repo.create.short": "Create a new repository",
|
||||
"cmd.repo.code_stats.short": "Show repository code statistics",
|
||||
"cmd.repo.contributors.short": "List repository contributors",
|
||||
"cmd.repo.delete.short": "Delete a repository",
|
||||
"cmd.repo.fork.short": "Fork a repository",
|
||||
"cmd.repo.info.short": "Show repository details",
|
||||
"cmd.repo.languages.short": "Show repository language statistics",
|
||||
"cmd.repo.list.short": "List repositories for a user or organization",
|
||||
"cmd.repo.short": "Repository operations",
|
||||
"cmd.root.long": "Manage repositories, issues, pull requests, releases, CI and workflows on GitLink.",
|
||||
|
|
@ -150,6 +153,7 @@
|
|||
"flag.release.prerelease": "Mark as prerelease (true/false)",
|
||||
"flag.release.tag": "Tag name",
|
||||
"flag.release.target": "Target branch",
|
||||
"flag.ref": "Branch, tag, or commit SHA",
|
||||
"flag.repo": "Repository name (auto-detected from git remote)",
|
||||
"flag.repo.category": "Filter: manage/mirror/sync/fork/all (default: manage)",
|
||||
"flag.repo.description": "Repository description",
|
||||
|
|
|
|||
|
|
@ -58,9 +58,12 @@
|
|||
"cmd.release.short": "发布操作",
|
||||
"cmd.release.view.short": "查看发布详情",
|
||||
"cmd.repo.create.short": "创建新仓库",
|
||||
"cmd.repo.code_stats.short": "显示仓库代码统计",
|
||||
"cmd.repo.contributors.short": "列出仓库贡献者",
|
||||
"cmd.repo.delete.short": "删除仓库",
|
||||
"cmd.repo.fork.short": "Fork 仓库",
|
||||
"cmd.repo.info.short": "显示仓库详情",
|
||||
"cmd.repo.languages.short": "显示仓库语言统计",
|
||||
"cmd.repo.list.short": "列出用户或组织的仓库",
|
||||
"cmd.repo.short": "仓库操作",
|
||||
"cmd.root.long": "用于管理 GitLink 上的仓库、议题、拉取请求、发布、CI 和工作流。",
|
||||
|
|
@ -150,6 +153,7 @@
|
|||
"flag.release.prerelease": "标记为预发布(true/false)",
|
||||
"flag.release.tag": "标签名称",
|
||||
"flag.release.target": "目标分支",
|
||||
"flag.ref": "分支、标签或 Commit SHA",
|
||||
"flag.repo": "仓库名称(自动从 git remote 检测)",
|
||||
"flag.repo.category": "筛选:manage/mirror/sync/fork/all(默认:manage)",
|
||||
"flag.repo.description": "仓库描述",
|
||||
|
|
|
|||
|
|
@ -79,6 +79,55 @@ func Shortcuts(translators ...*i18n.Translator) []*common.Shortcut {
|
|||
return ctx.Output(env)
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "languages",
|
||||
Description: tr.T("cmd.repo.languages.short"),
|
||||
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: tr.T("cmd.repo.contributors.short"),
|
||||
Run: func(ctx *common.RuntimeContext) error {
|
||||
if err := ctx.ResolveOwnerRepo(); err != nil {
|
||||
return err
|
||||
}
|
||||
env, err := ctx.CallAPI("GET", ctx.RepoPath()+"/contributors", nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return ctx.Output(env)
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "code-stats",
|
||||
Description: tr.T("cmd.repo.code_stats.short"),
|
||||
Flags: []common.Flag{
|
||||
{Name: "ref", Usage: tr.T("flag.ref")},
|
||||
},
|
||||
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)
|
||||
}
|
||||
env, err := ctx.CallAPIWithQuery("GET", "/v1"+ctx.RepoPath()+"/code_stats", q)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return ctx.Output(env)
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "create",
|
||||
Description: tr.T("cmd.repo.create.short"),
|
||||
|
|
|
|||
|
|
@ -112,6 +112,77 @@ func TestRepoInfo(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
// --- insights ---
|
||||
|
||||
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": "100.0%",
|
||||
})
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
err := runShortcut(t, server, "languages", nil)
|
||||
if err != nil {
|
||||
t.Fatalf("languages failed: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
writeJSON(w, map[string]interface{}{
|
||||
"total_count": float64(1),
|
||||
"list": []interface{}{
|
||||
map[string]interface{}{"login": "alice", "contributions": float64(3)},
|
||||
},
|
||||
})
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
err := runShortcut(t, server, "contributors", nil)
|
||||
if err != nil {
|
||||
t.Fatalf("contributors failed: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRepoCodeStatsWithRef(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/code_stats.json" {
|
||||
t.Fatalf("unexpected path: %s", r.URL.Path)
|
||||
}
|
||||
if got := r.URL.Query().Get("ref"); got != "release/1.0" {
|
||||
t.Fatalf("ref = %q", got)
|
||||
}
|
||||
writeJSON(w, map[string]interface{}{
|
||||
"author_count": float64(2),
|
||||
"commit_count": float64(17),
|
||||
"additions": float64(120),
|
||||
"deletions": float64(8),
|
||||
})
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
err := runShortcut(t, server, "code-stats", map[string]string{"ref": "release/1.0"})
|
||||
if err != nil {
|
||||
t.Fatalf("code-stats failed: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// --- fork ---
|
||||
|
||||
func TestRepoFork(t *testing.T) {
|
||||
|
|
@ -250,6 +321,19 @@ func TestRepoInfoHTTPError(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
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")
|
||||
}
|
||||
}
|
||||
|
||||
func TestRepoForkHTTPError(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
|
|
|
|||
|
|
@ -22,6 +22,9 @@ metadata:
|
|||
|----------|------|----------|
|
||||
| `repo +list` | 仓库列表 | 否(公开项目) |
|
||||
| `repo +info` | 仓库详情 | 否(公开项目) |
|
||||
| `repo +languages` | 仓库语言统计 | 否(公开项目) |
|
||||
| `repo +contributors` | 仓库贡献者列表 | 否(公开项目) |
|
||||
| `repo +code-stats` | 仓库代码统计,可按分支/标签/Commit 筛选 | 否(公开项目) |
|
||||
| `repo +create` | 创建仓库 | 是 |
|
||||
| `repo +fork` | Fork 仓库 | 是 |
|
||||
| `repo +delete` | 删除仓库 | 是 |
|
||||
|
|
@ -39,6 +42,15 @@ gitlink-cli repo +info
|
|||
# 列出用户的仓库
|
||||
gitlink-cli repo +list --user zhangsan
|
||||
|
||||
# 查看仓库语言统计
|
||||
gitlink-cli repo +languages --owner Gitlink --repo forgeplus
|
||||
|
||||
# 列出仓库贡献者
|
||||
gitlink-cli repo +contributors --owner Gitlink --repo forgeplus
|
||||
|
||||
# 查看指定 ref 的代码统计
|
||||
gitlink-cli repo +code-stats --owner Gitlink --repo forgeplus --ref master
|
||||
|
||||
# 创建仓库
|
||||
gitlink-cli repo +create --name my-project --description "项目描述"
|
||||
|
||||
|
|
@ -57,12 +69,6 @@ Shortcuts 未覆盖的仓库操作可用 Raw API:
|
|||
# 获取 README
|
||||
gitlink-cli api GET /:owner/:repo/readme
|
||||
|
||||
# 获取贡献者列表
|
||||
gitlink-cli api GET /:owner/:repo/contributors
|
||||
|
||||
# 获取语言统计
|
||||
gitlink-cli api GET /:owner/:repo/languages
|
||||
|
||||
# 获取提交列表
|
||||
gitlink-cli api GET /:owner/:repo/commits --query 'page=1&limit=20'
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue