feat(repo): add insight and interaction shortcuts
This commit is contained in:
parent
b45241dcda
commit
cedd8b6263
27
README.md
27
README.md
|
|
@ -79,7 +79,7 @@ The official [GitLink](https://www.gitlink.org.cn) CLI tool — built for humans
|
|||
|
||||
| Category | Capabilities |
|
||||
|----------|-------------|
|
||||
| 📦 Repo | List, create, fork, delete repositories, view repo info |
|
||||
| 📦 Repo | List, create, fork, delete repositories, view repo info, insights, and interactions |
|
||||
| 🐛 Issue | Create, update, close, batch close, comment on issues |
|
||||
| 🔖 Label | Create, list, update, delete issue labels |
|
||||
| 🔀 PR | Create, merge, review pull requests, view changed files |
|
||||
|
|
@ -196,6 +196,29 @@ gitlink-cli repo +info --owner Gitlink --repo forgeplus
|
|||
# Read repository README
|
||||
gitlink-cli repo +readme --owner Gitlink --repo forgeplus --ref master
|
||||
|
||||
# Show language breakdown
|
||||
gitlink-cli repo +languages --owner Gitlink --repo forgeplus
|
||||
|
||||
# List contributors
|
||||
gitlink-cli repo +contributors --owner Gitlink --repo forgeplus
|
||||
|
||||
# Show contributor code-line stats for a branch, tag, or commit
|
||||
gitlink-cli repo +contributor-stats --owner Gitlink --repo forgeplus --ref master --pass-year 1
|
||||
|
||||
# Show repository code stats
|
||||
gitlink-cli repo +code-stats --owner Gitlink --repo forgeplus --ref master
|
||||
|
||||
# List watchers and stargazers in a time range
|
||||
gitlink-cli repo +watchers --owner Gitlink --repo forgeplus --start-at 1714521600 --end-at 1717200000
|
||||
gitlink-cli repo +stargazers --owner Gitlink --repo forgeplus --start-at 1714521600 --end-at 1717200000
|
||||
|
||||
# Preview and apply repository interaction actions
|
||||
gitlink-cli repo +follow --owner Gitlink --repo forgeplus --dry-run
|
||||
gitlink-cli repo +follow --owner Gitlink --repo forgeplus
|
||||
gitlink-cli repo +unfollow --owner Gitlink --repo forgeplus --project-id 123
|
||||
gitlink-cli repo +like --owner Gitlink --repo forgeplus
|
||||
gitlink-cli repo +unlike --owner Gitlink --repo forgeplus --project-id 123
|
||||
|
||||
# Create a repository
|
||||
gitlink-cli repo +create -n my-project -d "Project description"
|
||||
|
||||
|
|
@ -587,7 +610,7 @@ See [skills/README.md](skills/README.md) for details.
|
|||
| Skill | Description |
|
||||
|-------|-------------|
|
||||
| `gitlink-shared` | Authentication, global parameters, safety rules, API notes |
|
||||
| `gitlink-repo` | Repository operations (create, view, delete, fork, etc.) |
|
||||
| `gitlink-repo` | Repository operations (create, view, delete, fork, insights, etc.) |
|
||||
| `gitlink-issue` | Issue operations (create, update, close, comment, etc.) |
|
||||
| `gitlink-pr` | Pull request operations (create, merge, review, etc.) |
|
||||
| `gitlink-member` | Repository member and invite link management |
|
||||
|
|
|
|||
|
|
@ -79,7 +79,7 @@
|
|||
|
||||
| 分类 | 能力 |
|
||||
|------|------|
|
||||
| 📦 仓库 | 列出、创建、Fork、删除仓库,查看仓库信息 |
|
||||
| 📦 仓库 | 列出、创建、Fork、删除仓库,查看仓库信息、洞察数据和互动状态 |
|
||||
| 🐛 Issue | 创建、更新、关闭、批量关闭、评论 Issue |
|
||||
| 🔖 标签 | 创建、列出、更新、删除 Issue 标签 |
|
||||
| 🔀 PR | 创建、合并、Review Pull Request,查看变更文件 |
|
||||
|
|
@ -207,6 +207,29 @@ 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 +contributor-stats --owner Gitlink --repo forgeplus --ref master --pass-year 1
|
||||
|
||||
# 查看仓库代码统计
|
||||
gitlink-cli repo +code-stats --owner Gitlink --repo forgeplus --ref master
|
||||
|
||||
# 按时间范围查看关注者和点赞者
|
||||
gitlink-cli repo +watchers --owner Gitlink --repo forgeplus --start-at 1714521600 --end-at 1717200000
|
||||
gitlink-cli repo +stargazers --owner Gitlink --repo forgeplus --start-at 1714521600 --end-at 1717200000
|
||||
|
||||
# 预览并执行仓库互动操作
|
||||
gitlink-cli repo +follow --owner Gitlink --repo forgeplus --dry-run
|
||||
gitlink-cli repo +follow --owner Gitlink --repo forgeplus
|
||||
gitlink-cli repo +unfollow --owner Gitlink --repo forgeplus --project-id 123
|
||||
gitlink-cli repo +like --owner Gitlink --repo forgeplus
|
||||
gitlink-cli repo +unlike --owner Gitlink --repo forgeplus --project-id 123
|
||||
|
||||
# 创建仓库
|
||||
gitlink-cli repo +create -n my-project -d "项目描述"
|
||||
|
||||
|
|
@ -466,7 +489,7 @@ git push gitlink
|
|||
| Skill | 说明 |
|
||||
|-------|------|
|
||||
| `gitlink-shared` | 认证、全局参数、安全规则、API 注意事项 |
|
||||
| `gitlink-repo` | 仓库操作(创建、查看、删除、Fork 等) |
|
||||
| `gitlink-repo` | 仓库操作(创建、查看、删除、Fork、洞察数据等) |
|
||||
| `gitlink-issue` | Issue 操作(创建、更新、关闭、评论等) |
|
||||
| `gitlink-pr` | Pull Request 操作(创建、合并、Review 等) |
|
||||
| `gitlink-member` | 仓库成员与邀请链接管理 |
|
||||
|
|
|
|||
|
|
@ -0,0 +1,51 @@
|
|||
# Repo Insight Shortcuts
|
||||
|
||||
## Summary
|
||||
|
||||
Adds read-only repository insight shortcuts so maintainers and agents can inspect project health without falling back to Raw API calls.
|
||||
|
||||
## Commands
|
||||
|
||||
| Command | Purpose |
|
||||
|---------|---------|
|
||||
| `gitlink-cli repo +languages` | Show repository language statistics |
|
||||
| `gitlink-cli repo +contributors` | List repository contributors |
|
||||
| `gitlink-cli repo +contributor-stats` | List contributor statistics with additions and deletions |
|
||||
| `gitlink-cli repo +code-stats` | Show repository code statistics |
|
||||
| `gitlink-cli repo +watchers` | List repository watchers |
|
||||
| `gitlink-cli repo +stargazers` | List repository stargazers |
|
||||
| `gitlink-cli repo +follow` | Follow a repository |
|
||||
| `gitlink-cli repo +unfollow` | Unfollow a repository |
|
||||
| `gitlink-cli repo +like` | Like a repository |
|
||||
| `gitlink-cli repo +unlike` | Unlike a repository |
|
||||
|
||||
## Validation
|
||||
|
||||
- `repo +contributor-stats --pass-year` must be a positive integer.
|
||||
- `repo +watchers` and `repo +stargazers` accept optional `--start-at` and `--end-at` Unix timestamps.
|
||||
- Time range timestamps must be non-negative, and `--start-at` cannot be greater than `--end-at`.
|
||||
- `repo +follow`, `repo +unfollow`, `repo +like`, and `repo +unlike` accept optional `--project-id`; if omitted, the project ID is resolved from `--owner/--repo`.
|
||||
- Repository interaction actions support `--dry-run` so callers can preview the resolved project ID and endpoint before changing remote state.
|
||||
|
||||
## Tests
|
||||
|
||||
Unit tests cover endpoint paths, query parameter mapping, optional ref and time-range filters, project ID auto-resolution, dry-run previews, and invalid argument handling before any API request is sent.
|
||||
|
||||
## 中文说明
|
||||
|
||||
### 变更内容
|
||||
|
||||
- 新增 `repo +languages`、`repo +contributors`、`repo +contributor-stats`、`repo +code-stats`、`repo +watchers`、`repo +stargazers` 等仓库洞察命令。
|
||||
- 新增 `repo +follow`、`repo +unfollow`、`repo +like`、`repo +unlike` 仓库互动命令,并支持 `--project-id` 和 `--dry-run`。
|
||||
- `repo +contributor-stats` 和 `repo +code-stats` 使用 v1 API,支持 `--ref` 和 `--pass-year` 参数。
|
||||
- `repo +watchers` 和 `repo +stargazers` 支持 `--start-at` / `--end-at` 时间范围,并在请求前校验时间戳。
|
||||
- 更新 README、README.zh-CN、`gitlink-repo` Skill 和变更说明,减少仓库分析场景对 Raw API 的依赖。
|
||||
- 提交者:王越
|
||||
|
||||
### 验证
|
||||
|
||||
- `GOPROXY=https://goproxy.cn,direct go test ./...`
|
||||
- `go run . repo --help`
|
||||
- `go run . repo +contributor-stats --help`
|
||||
- `go run . repo +watchers --help`
|
||||
- `git diff --check`
|
||||
|
|
@ -3,6 +3,8 @@ package repo
|
|||
import (
|
||||
"fmt"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/gitlink-org/gitlink-cli/internal/i18n"
|
||||
"github.com/gitlink-org/gitlink-cli/shortcuts/common"
|
||||
|
|
@ -79,6 +81,81 @@ func Shortcuts(translators ...*i18n.Translator) []*common.Shortcut {
|
|||
return ctx.Output(env)
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "languages",
|
||||
Description: "Show repository language statistics",
|
||||
Run: runLanguages,
|
||||
},
|
||||
{
|
||||
Name: "contributors",
|
||||
Description: "List repository contributors",
|
||||
Run: runContributors,
|
||||
},
|
||||
{
|
||||
Name: "contributor-stats",
|
||||
Description: "List contributor statistics with code line counts",
|
||||
Flags: []common.Flag{
|
||||
{Name: "ref", Usage: "Branch, tag, or commit SHA"},
|
||||
{Name: "pass-year", Usage: "Number of past years to include"},
|
||||
},
|
||||
Run: runContributorStats,
|
||||
},
|
||||
{
|
||||
Name: "code-stats",
|
||||
Description: "Show repository code statistics",
|
||||
Flags: []common.Flag{
|
||||
{Name: "ref", Usage: "Branch, tag, or commit SHA"},
|
||||
},
|
||||
Run: runCodeStats,
|
||||
},
|
||||
{
|
||||
Name: "watchers",
|
||||
Description: "List repository watchers",
|
||||
Flags: communityListFlags(),
|
||||
Run: func(ctx *common.RuntimeContext) error {
|
||||
return runCommunityList(ctx, "watchers")
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "stargazers",
|
||||
Description: "List repository stargazers",
|
||||
Flags: communityListFlags(),
|
||||
Run: func(ctx *common.RuntimeContext) error {
|
||||
return runCommunityList(ctx, "stargazers")
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "follow",
|
||||
Description: "Follow a repository",
|
||||
Flags: repoInteractionFlags(),
|
||||
Run: func(ctx *common.RuntimeContext) error {
|
||||
return runRepoFollowAction(ctx, "POST", "/watchers/follow", "follow")
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "unfollow",
|
||||
Description: "Unfollow a repository",
|
||||
Flags: repoInteractionFlags(),
|
||||
Run: func(ctx *common.RuntimeContext) error {
|
||||
return runRepoFollowAction(ctx, "DELETE", "/watchers/unfollow", "unfollow")
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "like",
|
||||
Description: "Like a repository",
|
||||
Flags: repoInteractionFlags(),
|
||||
Run: func(ctx *common.RuntimeContext) error {
|
||||
return runRepoPraiseAction(ctx, "POST", "like")
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "unlike",
|
||||
Description: "Unlike a repository",
|
||||
Flags: repoInteractionFlags(),
|
||||
Run: func(ctx *common.RuntimeContext) error {
|
||||
return runRepoPraiseAction(ctx, "DELETE", "unlike")
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "create",
|
||||
Description: tr.T("cmd.repo.create.short"),
|
||||
|
|
@ -158,3 +235,241 @@ func shortcutTranslator(translators ...*i18n.Translator) *i18n.Translator {
|
|||
}
|
||||
return i18n.Default()
|
||||
}
|
||||
|
||||
func runLanguages(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)
|
||||
}
|
||||
|
||||
func runContributors(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)
|
||||
}
|
||||
|
||||
func runContributorStats(ctx *common.RuntimeContext) error {
|
||||
if err := ctx.ResolveOwnerRepo(); err != nil {
|
||||
return err
|
||||
}
|
||||
q := url.Values{}
|
||||
setRepoQueryIfPresent(q, "ref", ctx.Arg("ref"))
|
||||
if passYear := ctx.Arg("pass-year"); strings.TrimSpace(passYear) != "" {
|
||||
year, err := parseRepoPositiveInt(passYear, "pass-year")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
q.Set("pass_year", strconv.Itoa(year))
|
||||
}
|
||||
env, err := ctx.CallAPIWithQuery("GET", "/v1"+ctx.RepoPath()+"/contributors/stat", q)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return ctx.Output(env)
|
||||
}
|
||||
|
||||
func runCodeStats(ctx *common.RuntimeContext) error {
|
||||
if err := ctx.ResolveOwnerRepo(); err != nil {
|
||||
return err
|
||||
}
|
||||
q := url.Values{}
|
||||
setRepoQueryIfPresent(q, "ref", ctx.Arg("ref"))
|
||||
env, err := ctx.CallAPIWithQuery("GET", "/v1"+ctx.RepoPath()+"/code_stats", q)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return ctx.Output(env)
|
||||
}
|
||||
|
||||
func runCommunityList(ctx *common.RuntimeContext, path string) error {
|
||||
if err := ctx.ResolveOwnerRepo(); err != nil {
|
||||
return err
|
||||
}
|
||||
q, err := communityTimeRangeQuery(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
env, err := ctx.CallAPIWithQuery("GET", ctx.RepoPath()+"/"+path, q)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return ctx.Output(env)
|
||||
}
|
||||
|
||||
func communityListFlags() []common.Flag {
|
||||
return []common.Flag{
|
||||
{Name: "start-at", Usage: "Start timestamp"},
|
||||
{Name: "end-at", Usage: "End timestamp"},
|
||||
}
|
||||
}
|
||||
|
||||
func repoInteractionFlags() []common.Flag {
|
||||
return []common.Flag{
|
||||
{Name: "project-id", Usage: "GitLink project ID. If omitted, it is resolved from --owner/--repo."},
|
||||
{Name: "dry-run", Usage: "Preview the action without changing repository state", Bool: true, Default: "false"},
|
||||
}
|
||||
}
|
||||
|
||||
func runRepoFollowAction(ctx *common.RuntimeContext, method, path, action string) error {
|
||||
if err := ctx.ResolveOwnerRepo(); err != nil {
|
||||
return err
|
||||
}
|
||||
projectID, err := resolveRepoProjectID(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
query := url.Values{}
|
||||
query.Set("target_type", "project")
|
||||
query.Set("id", projectID)
|
||||
if ctx.Arg("dry-run") == "true" {
|
||||
return repoInteractionDryRun(ctx, method, path, action, projectID, query)
|
||||
}
|
||||
env, err := ctx.CallAPIWithQuery(method, path, query)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return ctx.Output(env)
|
||||
}
|
||||
|
||||
func runRepoPraiseAction(ctx *common.RuntimeContext, method, action string) error {
|
||||
if err := ctx.ResolveOwnerRepo(); err != nil {
|
||||
return err
|
||||
}
|
||||
projectID, err := resolveRepoProjectID(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
path := fmt.Sprintf("/projects/%s/praise_tread/%s", projectID, action)
|
||||
if ctx.Arg("dry-run") == "true" {
|
||||
return repoInteractionDryRun(ctx, method, path, action, projectID, nil)
|
||||
}
|
||||
env, err := ctx.CallAPI(method, path, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return ctx.Output(env)
|
||||
}
|
||||
|
||||
func repoInteractionDryRun(ctx *common.RuntimeContext, method, path, action, projectID string, query url.Values) error {
|
||||
preview := map[string]interface{}{
|
||||
"dry_run": true,
|
||||
"action": action,
|
||||
"method": method,
|
||||
"path": path,
|
||||
"repository": fmt.Sprintf("%s/%s", ctx.Owner, ctx.Repo),
|
||||
"project_id": projectID,
|
||||
}
|
||||
if len(query) > 0 {
|
||||
preview["query"] = query.Encode()
|
||||
}
|
||||
return ctx.OutputData(preview)
|
||||
}
|
||||
|
||||
func resolveRepoProjectID(ctx *common.RuntimeContext) (string, error) {
|
||||
if raw := strings.TrimSpace(ctx.Arg("project-id")); raw != "" {
|
||||
return normalizeRepoProjectID(raw)
|
||||
}
|
||||
env, err := ctx.CallAPI("GET", ctx.RepoPath(), nil)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("resolve project id: %w", err)
|
||||
}
|
||||
data, ok := env.Data.(map[string]interface{})
|
||||
if !ok {
|
||||
return "", fmt.Errorf("resolve project id: unexpected repository response")
|
||||
}
|
||||
for _, key := range []string{"id", "project_id"} {
|
||||
if id := repoProjectIDString(data[key]); id != "" {
|
||||
return id, nil
|
||||
}
|
||||
}
|
||||
return "", fmt.Errorf("resolve project id: repository response did not include id")
|
||||
}
|
||||
|
||||
func normalizeRepoProjectID(value string) (string, error) {
|
||||
value = strings.TrimSpace(value)
|
||||
parsed, err := strconv.ParseInt(value, 10, 64)
|
||||
if err != nil || parsed <= 0 {
|
||||
return "", fmt.Errorf("invalid --project-id %q: use a positive numeric project ID", value)
|
||||
}
|
||||
return strconv.FormatInt(parsed, 10), nil
|
||||
}
|
||||
|
||||
func repoProjectIDString(value interface{}) string {
|
||||
switch v := value.(type) {
|
||||
case float64:
|
||||
if v > 0 {
|
||||
return strconv.FormatInt(int64(v), 10)
|
||||
}
|
||||
case int:
|
||||
if v > 0 {
|
||||
return strconv.Itoa(v)
|
||||
}
|
||||
case int64:
|
||||
if v > 0 {
|
||||
return strconv.FormatInt(v, 10)
|
||||
}
|
||||
case string:
|
||||
id, err := normalizeRepoProjectID(v)
|
||||
if err == nil {
|
||||
return id
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func communityTimeRangeQuery(ctx *common.RuntimeContext) (url.Values, error) {
|
||||
q := url.Values{}
|
||||
start, hasStart, err := parseOptionalRepoNonNegativeInt(ctx.Arg("start-at"), "start-at")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
end, hasEnd, err := parseOptionalRepoNonNegativeInt(ctx.Arg("end-at"), "end-at")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if hasStart {
|
||||
q.Set("start_at", strconv.Itoa(start))
|
||||
}
|
||||
if hasEnd {
|
||||
q.Set("end_at", strconv.Itoa(end))
|
||||
}
|
||||
if hasStart && hasEnd && start > end {
|
||||
return nil, fmt.Errorf("--start-at must be less than or equal to --end-at")
|
||||
}
|
||||
return q, nil
|
||||
}
|
||||
|
||||
func setRepoQueryIfPresent(q url.Values, key, value string) {
|
||||
if value := strings.TrimSpace(value); value != "" {
|
||||
q.Set(key, value)
|
||||
}
|
||||
}
|
||||
|
||||
func parseOptionalRepoNonNegativeInt(value, name string) (int, bool, error) {
|
||||
if strings.TrimSpace(value) == "" {
|
||||
return 0, false, nil
|
||||
}
|
||||
parsed, err := strconv.Atoi(strings.TrimSpace(value))
|
||||
if err != nil || parsed < 0 {
|
||||
return 0, false, fmt.Errorf("invalid --%s %q: use a non-negative integer", name, value)
|
||||
}
|
||||
return parsed, true, nil
|
||||
}
|
||||
|
||||
func parseRepoPositiveInt(value, name string) (int, error) {
|
||||
parsed, err := strconv.Atoi(strings.TrimSpace(value))
|
||||
if err != nil || parsed <= 0 {
|
||||
return 0, fmt.Errorf("invalid --%s %q: use a positive integer", name, value)
|
||||
}
|
||||
return parsed, nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ package repo
|
|||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
|
@ -14,12 +15,18 @@ func runShortcut(t *testing.T, server *httptest.Server, name string, args map[st
|
|||
t.Helper()
|
||||
s := findShortcut(t, name)
|
||||
ctx := &common.RuntimeContext{
|
||||
Client: &client.Client{HTTP: server.Client(), BaseURL: server.URL},
|
||||
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)
|
||||
}
|
||||
|
||||
|
|
@ -34,9 +41,20 @@ func findShortcut(t *testing.T, name string) *common.Shortcut {
|
|||
return nil
|
||||
}
|
||||
|
||||
func writeJSON(w http.ResponseWriter, v interface{}) {
|
||||
func writeJSON(t *testing.T, w http.ResponseWriter, v interface{}) {
|
||||
t.Helper()
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(v)
|
||||
if err := json.NewEncoder(w).Encode(v); err != nil {
|
||||
t.Fatalf("write response: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func writeText(t *testing.T, w http.ResponseWriter, code int, text string) {
|
||||
t.Helper()
|
||||
w.WriteHeader(code)
|
||||
if _, err := w.Write([]byte(text)); err != nil {
|
||||
t.Fatalf("write response: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// --- list ---
|
||||
|
|
@ -49,7 +67,7 @@ func TestRepoListDefault(t *testing.T) {
|
|||
if r.URL.Path != "/projects.json" {
|
||||
t.Fatalf("unexpected path: %s", r.URL.Path)
|
||||
}
|
||||
writeJSON(w, map[string]interface{}{
|
||||
writeJSON(t, w, map[string]interface{}{
|
||||
"total_count": float64(2),
|
||||
"data": []interface{}{map[string]interface{}{"name": "repo1"}},
|
||||
})
|
||||
|
|
@ -67,7 +85,7 @@ func TestRepoListForUser(t *testing.T) {
|
|||
if r.URL.Path != "/users/alice/projects.json" {
|
||||
t.Fatalf("unexpected path: %s", r.URL.Path)
|
||||
}
|
||||
writeJSON(w, map[string]interface{}{"total_count": float64(0), "data": []interface{}{}})
|
||||
writeJSON(t, w, map[string]interface{}{"total_count": float64(0), "data": []interface{}{}})
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
|
|
@ -82,7 +100,7 @@ func TestRepoListWithCategory(t *testing.T) {
|
|||
if r.URL.Query().Get("category") != "mirror" {
|
||||
t.Fatalf("expected category=mirror, got %s", r.URL.Query().Get("category"))
|
||||
}
|
||||
writeJSON(w, map[string]interface{}{"data": []interface{}{}})
|
||||
writeJSON(t, w, map[string]interface{}{"data": []interface{}{}})
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
|
|
@ -92,14 +110,14 @@ func TestRepoListWithCategory(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
// --- info ---
|
||||
// --- info/readme/insights ---
|
||||
|
||||
func TestRepoInfo(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.Path != "/owner/repo.json" {
|
||||
t.Fatalf("unexpected path: %s", r.URL.Path)
|
||||
}
|
||||
writeJSON(w, map[string]interface{}{
|
||||
writeJSON(t, w, map[string]interface{}{
|
||||
"name": "repo",
|
||||
"description": "test repo",
|
||||
})
|
||||
|
|
@ -112,7 +130,203 @@ func TestRepoInfo(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
// --- fork ---
|
||||
func TestRepoReadmeUsesRepositoryReadmeEndpoint(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
assertRequest(t, r, "GET", "/owner/repo/readme.json")
|
||||
assertEqual(t, r.URL.Query().Get("ref"), "main")
|
||||
assertEqual(t, r.URL.Query().Get("filepath"), "docs")
|
||||
writeJSON(t, w, map[string]interface{}{
|
||||
"type": "file",
|
||||
"name": "README.md",
|
||||
"content": "# docs\n",
|
||||
})
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
err := runShortcut(t, server, "readme", map[string]string{
|
||||
"ref": "main",
|
||||
"path": "docs",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("readme shortcut failed: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRepoLanguagesUsesLanguagesEndpoint(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
assertRequest(t, r, "GET", "/owner/repo/languages.json")
|
||||
writeJSON(t, w, map[string]interface{}{"Go": "92.4%", "Shell": "7.6%"})
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
if err := runShortcut(t, server, "languages", nil); err != nil {
|
||||
t.Fatalf("languages shortcut failed: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRepoContributorsUsesContributorsEndpoint(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
assertRequest(t, r, "GET", "/owner/repo/contributors.json")
|
||||
writeJSON(t, w, map[string]interface{}{"total_count": 1, "list": []interface{}{}})
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
if err := runShortcut(t, server, "contributors", nil); err != nil {
|
||||
t.Fatalf("contributors shortcut failed: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRepoContributorStatsBuildsQuery(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
assertRequest(t, r, "GET", "/v1/owner/repo/contributors/stat.json")
|
||||
assertEqual(t, r.URL.Query().Get("ref"), "main")
|
||||
assertEqual(t, r.URL.Query().Get("pass_year"), "2")
|
||||
writeJSON(t, w, map[string]interface{}{"total_count": 1, "contributors": []interface{}{}})
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
err := runShortcut(t, server, "contributor-stats", map[string]string{
|
||||
"ref": " main ",
|
||||
"pass-year": "2",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("contributor-stats shortcut failed: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRepoCodeStatsUsesRefQuery(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
assertRequest(t, r, "GET", "/v1/owner/repo/code_stats.json")
|
||||
assertEqual(t, r.URL.Query().Get("ref"), "release/v1")
|
||||
writeJSON(t, w, map[string]interface{}{"author_count": 1, "commit_count": 3})
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
if err := runShortcut(t, server, "code-stats", map[string]string{"ref": "release/v1"}); err != nil {
|
||||
t.Fatalf("code-stats shortcut failed: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRepoWatchersBuildsTimeRangeQuery(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
assertRequest(t, r, "GET", "/owner/repo/watchers.json")
|
||||
assertEqual(t, r.URL.Query().Get("start_at"), "1714521600")
|
||||
assertEqual(t, r.URL.Query().Get("end_at"), "1717200000")
|
||||
writeJSON(t, w, map[string]interface{}{"count": 1, "users": []interface{}{}})
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
err := runShortcut(t, server, "watchers", map[string]string{
|
||||
"start-at": "1714521600",
|
||||
"end-at": "1717200000",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("watchers shortcut failed: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRepoStargazersUsesStargazersEndpoint(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
assertRequest(t, r, "GET", "/owner/repo/stargazers.json")
|
||||
writeJSON(t, w, map[string]interface{}{"count": 0, "users": []interface{}{}})
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
if err := runShortcut(t, server, "stargazers", nil); err != nil {
|
||||
t.Fatalf("stargazers shortcut failed: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRepoFollowResolvesProjectID(t *testing.T) {
|
||||
requests := 0
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
requests++
|
||||
switch requests {
|
||||
case 1:
|
||||
assertRequest(t, r, "GET", "/owner/repo.json")
|
||||
writeJSON(t, w, map[string]interface{}{"id": float64(123)})
|
||||
case 2:
|
||||
assertRequest(t, r, "POST", "/watchers/follow.json")
|
||||
assertEqual(t, r.URL.Query().Get("target_type"), "project")
|
||||
assertEqual(t, r.URL.Query().Get("id"), "123")
|
||||
writeJSON(t, w, map[string]interface{}{"status": 0, "message": "success", "watched": true})
|
||||
default:
|
||||
t.Fatalf("unexpected extra request: %s %s", r.Method, r.URL.Path)
|
||||
}
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
if err := runShortcut(t, server, "follow", nil); err != nil {
|
||||
t.Fatalf("follow shortcut failed: %v", err)
|
||||
}
|
||||
assertEqual(t, requests, 2)
|
||||
}
|
||||
|
||||
func TestRepoUnfollowUsesExplicitProjectID(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
assertRequest(t, r, "DELETE", "/watchers/unfollow.json")
|
||||
assertEqual(t, r.URL.Query().Get("target_type"), "project")
|
||||
assertEqual(t, r.URL.Query().Get("id"), "456")
|
||||
writeJSON(t, w, map[string]interface{}{"status": 0, "message": "success", "watched": false})
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
if err := runShortcut(t, server, "unfollow", map[string]string{"project-id": "456"}); err != nil {
|
||||
t.Fatalf("unfollow shortcut failed: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRepoLikeUsesExplicitProjectID(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
assertRequest(t, r, "POST", "/projects/456/praise_tread/like.json")
|
||||
writeJSON(t, w, map[string]interface{}{"status": 0, "message": "success"})
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
if err := runShortcut(t, server, "like", map[string]string{"project-id": "456"}); err != nil {
|
||||
t.Fatalf("like shortcut failed: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRepoUnlikeResolvesStringProjectID(t *testing.T) {
|
||||
requests := 0
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
requests++
|
||||
switch requests {
|
||||
case 1:
|
||||
assertRequest(t, r, "GET", "/owner/repo.json")
|
||||
writeJSON(t, w, map[string]interface{}{"project_id": "789"})
|
||||
case 2:
|
||||
assertRequest(t, r, "DELETE", "/projects/789/praise_tread/unlike.json")
|
||||
writeJSON(t, w, map[string]interface{}{"status": 0, "message": "success"})
|
||||
default:
|
||||
t.Fatalf("unexpected extra request: %s %s", r.Method, r.URL.Path)
|
||||
}
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
if err := runShortcut(t, server, "unlike", nil); err != nil {
|
||||
t.Fatalf("unlike shortcut failed: %v", err)
|
||||
}
|
||||
assertEqual(t, requests, 2)
|
||||
}
|
||||
|
||||
func TestRepoInteractionDryRunDoesNotCallAPIWithExplicitProjectID(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
t.Fatalf("dry-run with explicit project-id should not call API, got: %s %s", r.Method, r.URL.Path)
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
err := runShortcut(t, server, "like", map[string]string{
|
||||
"project-id": "456",
|
||||
"dry-run": "true",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("dry-run shortcut failed: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// --- fork/delete/create ---
|
||||
|
||||
func TestRepoFork(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
|
|
@ -122,7 +336,7 @@ func TestRepoFork(t *testing.T) {
|
|||
if r.URL.Path != "/owner/repo/forks.json" {
|
||||
t.Fatalf("unexpected path: %s", r.URL.Path)
|
||||
}
|
||||
writeJSON(w, map[string]interface{}{"message": "forked"})
|
||||
writeJSON(t, w, map[string]interface{}{"message": "forked"})
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
|
|
@ -132,8 +346,6 @@ func TestRepoFork(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
// --- delete ---
|
||||
|
||||
func TestRepoDelete(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != "DELETE" {
|
||||
|
|
@ -142,7 +354,7 @@ func TestRepoDelete(t *testing.T) {
|
|||
if r.URL.Path != "/owner/repo.json" {
|
||||
t.Fatalf("unexpected path: %s", r.URL.Path)
|
||||
}
|
||||
writeJSON(w, map[string]interface{}{"message": "deleted"})
|
||||
writeJSON(t, w, map[string]interface{}{"message": "deleted"})
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
|
|
@ -152,18 +364,16 @@ func TestRepoDelete(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
// --- create ---
|
||||
|
||||
func TestRepoCreate(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
switch {
|
||||
case r.URL.Path == "/users/me.json" && r.Method == "GET":
|
||||
writeJSON(w, map[string]interface{}{
|
||||
writeJSON(t, w, map[string]interface{}{
|
||||
"login": "creator",
|
||||
"user_id": float64(42),
|
||||
})
|
||||
case r.URL.Path == "/creator/new-repo.json" && r.Method == "POST":
|
||||
writeJSON(w, map[string]interface{}{"name": "new-repo"})
|
||||
writeJSON(t, w, map[string]interface{}{"name": "new-repo"})
|
||||
default:
|
||||
t.Fatalf("unexpected request: %s %s", r.Method, r.URL.Path)
|
||||
}
|
||||
|
|
@ -181,13 +391,15 @@ func TestRepoCreateWithOptions(t *testing.T) {
|
|||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
switch r.URL.Path {
|
||||
case "/users/me.json":
|
||||
writeJSON(w, map[string]interface{}{
|
||||
writeJSON(t, w, map[string]interface{}{
|
||||
"login": "creator",
|
||||
"user_id": float64(42),
|
||||
})
|
||||
case "/creator/my-repo.json":
|
||||
json.NewDecoder(r.Body).Decode(&body)
|
||||
writeJSON(w, map[string]interface{}{"name": "my-repo"})
|
||||
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
|
||||
t.Fatalf("decode request body: %v", err)
|
||||
}
|
||||
writeJSON(t, w, map[string]interface{}{"name": "my-repo"})
|
||||
default:
|
||||
t.Fatalf("unexpected request: %s %s", r.Method, r.URL.Path)
|
||||
}
|
||||
|
|
@ -210,6 +422,55 @@ func TestRepoCreateWithOptions(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
// --- validation/error paths ---
|
||||
|
||||
func TestRepoInsightValidation(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
t.Fatalf("invalid input should not call API, got: %s %s", r.Method, r.URL.Path)
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
cases := []struct {
|
||||
name string
|
||||
shortcut string
|
||||
args map[string]string
|
||||
}{
|
||||
{
|
||||
name: "invalid pass year",
|
||||
shortcut: "contributor-stats",
|
||||
args: map[string]string{"pass-year": "0"},
|
||||
},
|
||||
{
|
||||
name: "invalid start timestamp",
|
||||
shortcut: "watchers",
|
||||
args: map[string]string{"start-at": "abc"},
|
||||
},
|
||||
{
|
||||
name: "start after end",
|
||||
shortcut: "stargazers",
|
||||
args: map[string]string{"start-at": "20", "end-at": "10"},
|
||||
},
|
||||
{
|
||||
name: "invalid project id",
|
||||
shortcut: "follow",
|
||||
args: map[string]string{"project-id": "repo"},
|
||||
},
|
||||
{
|
||||
name: "negative project id",
|
||||
shortcut: "unlike",
|
||||
args: map[string]string{"project-id": "-1"},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
if err := runShortcut(t, server, tc.shortcut, tc.args); err == nil {
|
||||
t.Fatal("expected validation error")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestRepoCreateFailsWithoutName(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
t.Fatal("no API call should be made")
|
||||
|
|
@ -222,12 +483,9 @@ func TestRepoCreateFailsWithoutName(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
// --- HTTP error paths ---
|
||||
|
||||
func TestRepoListHTTPError(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
w.Write([]byte("server error"))
|
||||
writeText(t, w, http.StatusInternalServerError, "server error")
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
|
|
@ -239,8 +497,7 @@ func TestRepoListHTTPError(t *testing.T) {
|
|||
|
||||
func TestRepoInfoHTTPError(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
w.Write([]byte("server error"))
|
||||
writeText(t, w, http.StatusInternalServerError, "server error")
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
|
|
@ -252,8 +509,7 @@ func TestRepoInfoHTTPError(t *testing.T) {
|
|||
|
||||
func TestRepoForkHTTPError(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
w.Write([]byte("server error"))
|
||||
writeText(t, w, http.StatusInternalServerError, "server error")
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
|
|
@ -265,8 +521,7 @@ func TestRepoForkHTTPError(t *testing.T) {
|
|||
|
||||
func TestRepoDeleteHTTPError(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
w.Write([]byte("server error"))
|
||||
writeText(t, w, http.StatusInternalServerError, "server error")
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
|
|
@ -278,8 +533,7 @@ func TestRepoDeleteHTTPError(t *testing.T) {
|
|||
|
||||
func TestRepoCreateGetUserHTTPError(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
w.Write([]byte("server error"))
|
||||
writeText(t, w, http.StatusInternalServerError, "server error")
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
|
|
@ -291,7 +545,7 @@ func TestRepoCreateGetUserHTTPError(t *testing.T) {
|
|||
|
||||
func TestRepoCreateUserNoLogin(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
writeJSON(w, map[string]interface{}{"user_id": float64(42)})
|
||||
writeJSON(t, w, map[string]interface{}{"user_id": float64(42)})
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
|
|
@ -300,3 +554,17 @@ func TestRepoCreateUserNoLogin(t *testing.T) {
|
|||
t.Fatal("expected error when user response has no login")
|
||||
}
|
||||
}
|
||||
|
||||
func assertRequest(t *testing.T, r *http.Request, method, path string) {
|
||||
t.Helper()
|
||||
if r.Method != method || r.URL.Path != path {
|
||||
t.Fatalf("unexpected request: %s %s", r.Method, r.URL.Path)
|
||||
}
|
||||
}
|
||||
|
||||
func assertEqual(t *testing.T, got interface{}, want interface{}) {
|
||||
t.Helper()
|
||||
if fmt.Sprintf("%v", got) != fmt.Sprintf("%v", want) {
|
||||
t.Fatalf("got %v (%T), want %v (%T)", got, got, want, want)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -134,7 +134,7 @@ skills/
|
|||
| Skill | 说明 | 常用命令 |
|
||||
|-------|------|----------|
|
||||
| **gitlink-shared** | 认证、全局参数、API 参考、安全规则、分支约定 | `auth login`, `auth status` |
|
||||
| **gitlink-repo** | 仓库管理 | `repo +list`, `repo +create`, `repo +info`, `repo +fork` |
|
||||
| **gitlink-repo** | 仓库管理与洞察 | `repo +list`, `repo +info`, `repo +languages`, `repo +contributors`, `repo +code-stats`, `repo +follow`, `repo +like` |
|
||||
| **gitlink-issue** | Issue 管理 | `issue +create`, `issue +list`, `issue +view`, `issue +close`, `issue +batch-close` |
|
||||
| **gitlink-pr** | Pull Request | `pr +list`, `pr +create`, `pr +view`, `pr +merge`, `pr +versions`, `pr +version-diff`, `pr +reviews`, `pr +review` |
|
||||
| **gitlink-member** | 仓库成员管理 | `member +list`, `member +add`, `member +batch-add`, `member +role`, `member +invite-link` |
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
---
|
||||
name: gitlink-repo
|
||||
version: 1.0.0
|
||||
description: "仓库管理:创建、查看、Fork、删除仓库,查看分支、提交、贡献者等。当用户需要操作 GitLink 仓库时触发。"
|
||||
description: "仓库管理:创建、查看、Fork、删除仓库,查看 README、语言统计、贡献者、关注者,并执行关注/点赞等互动操作。当用户需要操作或分析 GitLink 仓库时触发。"
|
||||
metadata:
|
||||
requires:
|
||||
bins: ["gitlink-cli"]
|
||||
|
|
@ -22,6 +22,17 @@ metadata:
|
|||
|----------|------|----------|
|
||||
| `repo +list` | 仓库列表 | 否(公开项目) |
|
||||
| `repo +info` | 仓库详情 | 否(公开项目) |
|
||||
| `repo +readme` | README 内容 | 否(公开项目) |
|
||||
| `repo +languages` | 仓库语言统计 | 否(公开项目) |
|
||||
| `repo +contributors` | 仓库贡献者列表 | 否(公开项目) |
|
||||
| `repo +contributor-stats` | 贡献者代码行统计 | 否(公开项目) |
|
||||
| `repo +code-stats` | 仓库代码统计 | 否(公开项目) |
|
||||
| `repo +watchers` | 关注者列表 | 否(公开项目) |
|
||||
| `repo +stargazers` | 点赞者列表 | 否(公开项目) |
|
||||
| `repo +follow` | 关注仓库 | 是 |
|
||||
| `repo +unfollow` | 取消关注仓库 | 是 |
|
||||
| `repo +like` | 点赞仓库 | 是 |
|
||||
| `repo +unlike` | 取消点赞仓库 | 是 |
|
||||
| `repo +create` | 创建仓库 | 是 |
|
||||
| `repo +fork` | Fork 仓库 | 是 |
|
||||
| `repo +delete` | 删除仓库 | 是 |
|
||||
|
|
@ -39,6 +50,25 @@ 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
|
||||
|
||||
# 查看代码统计
|
||||
gitlink-cli repo +contributor-stats --owner Gitlink --repo forgeplus --ref master --pass-year 1
|
||||
gitlink-cli repo +code-stats --owner Gitlink --repo forgeplus --ref master
|
||||
|
||||
# 查看社区关注数据
|
||||
gitlink-cli repo +watchers --owner Gitlink --repo forgeplus --start-at 1714521600 --end-at 1717200000
|
||||
gitlink-cli repo +stargazers --owner Gitlink --repo forgeplus --start-at 1714521600 --end-at 1717200000
|
||||
|
||||
# 预览并执行仓库互动操作
|
||||
gitlink-cli repo +follow --owner Gitlink --repo forgeplus --dry-run
|
||||
gitlink-cli repo +follow --owner Gitlink --repo forgeplus
|
||||
gitlink-cli repo +unfollow --owner Gitlink --repo forgeplus --project-id 123
|
||||
gitlink-cli repo +like --owner Gitlink --repo forgeplus
|
||||
gitlink-cli repo +unlike --owner Gitlink --repo forgeplus --project-id 123
|
||||
|
||||
# 创建仓库
|
||||
gitlink-cli repo +create --name my-project --description "项目描述"
|
||||
|
||||
|
|
@ -54,15 +84,6 @@ gitlink-cli repo +delete --owner myuser --repo old-project
|
|||
Shortcuts 未覆盖的仓库操作可用 Raw API:
|
||||
|
||||
```bash
|
||||
# 获取 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