forked from Gitlink/gitlink-cli
Compare commits
90 Commits
master
...
pr-skill-c
| Author | SHA1 | Date |
|---|---|---|
|
|
23a2cb81fd | |
|
|
9749a4c832 | |
|
|
11ecb9b79a | |
|
|
71ca2bb683 | |
|
|
593b5e8f0d | |
|
|
dbf0f45ce3 | |
|
|
2c9a8b6192 | |
|
|
982f2cb336 | |
|
|
0703d3eba9 | |
|
|
ef7a2c6ac1 | |
|
|
6eebfeb40d | |
|
|
6454645f50 | |
|
|
d3485fce45 | |
|
|
67b46eece4 | |
|
|
1806382459 | |
|
|
46154f7c5e | |
|
|
a61a8c4ef4 | |
|
|
789db5e5ae | |
|
|
ecc5578cf4 | |
|
|
8c29d98a57 | |
|
|
1c84cac060 | |
|
|
706a1396ad | |
|
|
29ca94db76 | |
|
|
7f731eb04f | |
|
|
da4c8c0d10 | |
|
|
2729cc69ba | |
|
|
7b880cb8ad | |
|
|
fb2d60f67a | |
|
|
968ea0d7b4 | |
|
|
85a67a875f | |
|
|
17c555620b | |
|
|
9be51d34fc | |
|
|
52b7093846 | |
|
|
d0ca6b24ec | |
|
|
462ae7b012 | |
|
|
220aa70e51 | |
|
|
21966bf1ea | |
|
|
caad409185 | |
|
|
60e8ab7768 | |
|
|
21ebb2f89c | |
|
|
f1dd01bebc | |
|
|
0da1d8d7bf | |
|
|
a580562922 | |
|
|
6e45fbb3e1 | |
|
|
2d963ebf41 | |
|
|
7947d4dfd6 | |
|
|
68aa9ae6e6 | |
|
|
27cc58328d | |
|
|
ee0d882e1c | |
|
|
ca578a74be | |
|
|
55805aa13d | |
|
|
dd4ed35448 | |
|
|
723e698db4 | |
|
|
03a9d7a942 | |
|
|
34f14b079f | |
|
|
f3e5d4aba0 | |
|
|
64dad994ae | |
|
|
7e863398aa | |
|
|
9c085fb246 | |
|
|
a9ee74efb0 | |
|
|
d6321e7af5 | |
|
|
dec10afaeb | |
|
|
35e50a1828 | |
|
|
dedba6e49f | |
|
|
a1d3a85ab8 | |
|
|
0b48ec90d8 | |
|
|
b884a7478c | |
|
|
6353987267 | |
|
|
3255884c6e | |
|
|
82ac20ae74 | |
|
|
5711aa3654 | |
|
|
a2fd36b4a6 | |
|
|
b319c44b02 | |
|
|
33ad8e62a0 | |
|
|
9bfa59c7e8 | |
|
|
cedd8b6263 | |
|
|
b45241dcda | |
|
|
633bed3360 | |
|
|
c263a2fa34 | |
|
|
6636175bd8 | |
|
|
83fe18f8c0 | |
|
|
0e13e8bdd4 | |
|
|
559e1a74f3 | |
|
|
17e8c113f7 | |
|
|
41b89976aa | |
|
|
0a578bbab0 | |
|
|
0da45def2b | |
|
|
f4820c03b4 | |
|
|
d478de59a3 | |
|
|
7d8c33a938 |
|
|
@ -1,41 +0,0 @@
|
|||
version: 2
|
||||
name: wyx_gitlink_cli_build
|
||||
description: "master 分支收到合并后编译构建并验证新命令是否注册成功"
|
||||
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: git clone
|
||||
task: git_clone@1.2.9
|
||||
input:
|
||||
remote_url: '"https://gitlink.org.cn/jiangtx/gitlink-cli.git"'
|
||||
ref: '"refs/heads/master"'
|
||||
commit_id: '""'
|
||||
depth: 1
|
||||
needs:
|
||||
- start
|
||||
- ref: ssh_cmd_0
|
||||
name: 编译并验证命令
|
||||
task: ssh_cmd@1.1.1
|
||||
input:
|
||||
ssh_ip: '"121.41.212.97"'
|
||||
ssh_port: '"22"'
|
||||
ssh_user: '"root"'
|
||||
ssh_pass: ((gitlink_cli.wyx_ssh_pass))
|
||||
ssh_cmd: >-
|
||||
"cd /root && rm -rf gitlink-cli && git clone --depth=1 -b master https://gitlink.org.cn/jiangtx/gitlink-cli.git && cd gitlink-cli && export PATH=$PATH:/usr/local/go/bin && export GOPROXY=https://goproxy.cn,direct && echo '>>> 编译二进制' && go build -o gitlink-cli . && echo '>>> 验证根命令' && ./gitlink-cli --help | head -20 && echo '>>> 验证 pm 模块(6条命令)' && ./gitlink-cli pm --help && echo '>>> 验证 wiki 模块(5条命令)' && ./gitlink-cli wiki --help && echo '>>> 验证 export 模块(3条命令)' && ./gitlink-cli export --help && echo '>>> 验证 alias 命令' && ./gitlink-cli alias --help && echo '>>> 验证 browse 命令' && ./gitlink-cli browse --help && echo '>>> 验证 status 命令' && ./gitlink-cli status && echo '✅ 所有命令验证通过'"
|
||||
needs:
|
||||
- git_clone_0
|
||||
- ref: end
|
||||
name: 结束
|
||||
task: end
|
||||
needs:
|
||||
- ssh_cmd_0
|
||||
|
|
@ -1,41 +0,0 @@
|
|||
version: 2
|
||||
name: wyx_gitlink_cli_ci
|
||||
description: "master 分支收到合并后自动执行 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: git clone
|
||||
task: git_clone@1.2.9
|
||||
input:
|
||||
remote_url: '"https://gitlink.org.cn/jiangtx/gitlink-cli.git"'
|
||||
ref: '"refs/heads/master"'
|
||||
commit_id: '""'
|
||||
depth: 1
|
||||
needs:
|
||||
- start
|
||||
- ref: ssh_cmd_0
|
||||
name: CI 检查
|
||||
task: ssh_cmd@1.1.1
|
||||
input:
|
||||
ssh_ip: '"121.41.212.97"'
|
||||
ssh_port: '"22"'
|
||||
ssh_user: '"root"'
|
||||
ssh_pass: ((gitlink_cli.wyx_ssh_pass))
|
||||
ssh_cmd: >-
|
||||
"cd /root && rm -rf gitlink-cli && git clone --depth=1 -b master 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 && echo '>>> 1. 编译检查' && go build ./... && echo '>>> 2. 静态分析' && go vet ./... && echo '>>> 3. 单元测试 + 覆盖率门禁' && go test -race -coverprofile=coverage.out ./... && go tool cover -func=coverage.out | grep '^total:' && COV=$(go tool cover -func=coverage.out | grep '^total:' | awk '{print int($3)}') && echo '覆盖率: '$COV'% (门禁阈值 78%)' && [ $COV -ge 78 ] && echo '✅ 覆盖率达标' && echo '>>> 4. 格式化修复' && gofmt -s -w . && echo '✅ 所有 CI 检查通过'"
|
||||
needs:
|
||||
- git_clone_0
|
||||
- ref: end
|
||||
name: 结束
|
||||
task: end
|
||||
needs:
|
||||
- ssh_cmd_0
|
||||
|
|
@ -1,41 +0,0 @@
|
|||
version: 2
|
||||
name: jtx_gitlink_cli
|
||||
description: "gitlink-cli 项目:代码提交时自动测试、构建并部署到服务器"
|
||||
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: git clone
|
||||
task: git_clone@1.2.9
|
||||
input:
|
||||
remote_url: '"https://gitlink.org.cn/jiangtx/gitlink-cli.git"'
|
||||
ref: '"refs/heads/master"'
|
||||
commit_id: '""'
|
||||
depth: 1
|
||||
needs:
|
||||
- start
|
||||
- ref: ssh_cmd_0
|
||||
name: 测试并部署到服务器
|
||||
task: ssh_cmd@1.1.1
|
||||
input:
|
||||
ssh_pass: ((jtx_gitlink_cli.jtx_gitlink_cli_ssh))
|
||||
ssh_ip: '"121.41.212.97"'
|
||||
ssh_port: '"22"'
|
||||
ssh_user: '"root"'
|
||||
ssh_cmd: >-
|
||||
"cd /root && rm -rf gitlink-cli && git clone --depth=1 https://gitlink.org.cn/jiangtx/gitlink-cli.git && cd gitlink-cli && docker run --rm -v $(pwd):/build -w /build docker.1ms.run/library/golang:1.26-alpine go test ./... && docker build --no-cache -t gitlink-cli . && (docker stop gitlink-cli || true) && (docker rm gitlink-cli || true) && docker run -d --restart=always --name gitlink-cli gitlink-cli version"
|
||||
needs:
|
||||
- git_clone_0
|
||||
- ref: end
|
||||
name: 结束
|
||||
task: end
|
||||
needs:
|
||||
- ssh_cmd_0
|
||||
|
|
@ -1,41 +0,0 @@
|
|||
version: 2
|
||||
name: gitlink_web_deploy
|
||||
description: "gitlink-web 科研服务:master 合并后仅同步代码并重启服务(不触碰 systemd 单元与 API_KEY,密钥由 deploy.py 一次性配在服务器上)"
|
||||
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: git clone
|
||||
task: git_clone@1.2.9
|
||||
input:
|
||||
remote_url: '"https://gitlink.org.cn/jiangtx/gitlink-cli.git"'
|
||||
ref: '"refs/heads/master"'
|
||||
commit_id: '""'
|
||||
depth: 1
|
||||
needs:
|
||||
- start
|
||||
- ref: ssh_cmd_0
|
||||
name: 同步代码并重启 gitlink-web
|
||||
task: ssh_cmd@1.1.1
|
||||
input:
|
||||
ssh_ip: '"121.41.212.97"'
|
||||
ssh_port: '"22"'
|
||||
ssh_user: '"root"'
|
||||
ssh_pass: ((gitlink_cli.wyx_ssh_pass))
|
||||
ssh_cmd: >-
|
||||
"set -e && cd /root && rm -rf gitlink-cli && git clone --depth=1 -b master https://gitlink.org.cn/jiangtx/gitlink-cli.git && echo '>>> 1. 同步代码(gitlink-web → /opt/gitlink-web,不动 systemd 与 API_KEY)' && mkdir -p /opt/gitlink-web/{templates,skills,reports} && cp -rf gitlink-cli/gitlink-web/* /opt/gitlink-web/ && echo '>>> 2. 更新 Python 依赖' && cd /opt/gitlink-web && pip3 install -r requirements.txt -q 2>&1 | tail -3 && echo '>>> 3. 重启服务(沿用既有 systemd 单元,API_KEY 已在服务器上)' && systemctl restart gitlink-web && sleep 3 && echo '>>> 4. 健康检查' && (curl -fsS http://127.0.0.1/skills/ -o /dev/null && echo '✅ 服务已更新上线:http://121.41.212.97/skills/' || (echo '⚠️ 健康检查失败,若首次部署需先跑 gitlink-web/deploy.py 配好 API_KEY 与 systemd 单元' && systemctl status gitlink-web --no-pager -l | head -20)) && echo '>>> 5. 最近日志' && journalctl -u gitlink-web -n 5 --no-pager"
|
||||
needs:
|
||||
- git_clone_0
|
||||
- ref: end
|
||||
name: 结束
|
||||
task: end
|
||||
needs:
|
||||
- ssh_cmd_0
|
||||
|
|
@ -1,41 +0,0 @@
|
|||
version: 2
|
||||
name: wyx_gitlink_cli_build
|
||||
description: "master 分支收到合并后编译构建并验证新命令是否注册成功"
|
||||
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: git clone
|
||||
task: git_clone@1.2.9
|
||||
input:
|
||||
remote_url: '"https://gitlink.org.cn/jiangtx/gitlink-cli.git"'
|
||||
ref: '"refs/heads/master"'
|
||||
commit_id: '""'
|
||||
depth: 1
|
||||
needs:
|
||||
- start
|
||||
- ref: ssh_cmd_0
|
||||
name: 编译并验证命令
|
||||
task: ssh_cmd@1.1.1
|
||||
input:
|
||||
ssh_ip: '"121.41.212.97"'
|
||||
ssh_port: '"22"'
|
||||
ssh_user: '"root"'
|
||||
ssh_pass: ((gitlink_cli.wyx_ssh_pass))
|
||||
ssh_cmd: >-
|
||||
"cd /root && rm -rf gitlink-cli && git clone --depth=1 -b master https://gitlink.org.cn/jiangtx/gitlink-cli.git && cd gitlink-cli && export PATH=$PATH:/usr/local/go/bin && export GOPROXY=https://goproxy.cn,direct && echo '>>> 编译二进制' && go build -o gitlink-cli . && echo '>>> 验证根命令' && ./gitlink-cli --help | head -20 && echo '>>> 验证 pm 模块(6条命令)' && ./gitlink-cli pm --help && echo '>>> 验证 wiki 模块(5条命令)' && ./gitlink-cli wiki --help && echo '>>> 验证 export 模块(3条命令)' && ./gitlink-cli export --help && echo '>>> 验证 alias 命令' && ./gitlink-cli alias --help && echo '>>> 验证 browse 命令' && ./gitlink-cli browse --help && echo '>>> 验证 status 命令' && ./gitlink-cli status && echo '>>> 验证批量操作命令' && ./gitlink-cli issue +batch-close --help && ./gitlink-cli issue +series-update --help && ./gitlink-cli member +batch-add --help && echo '✅ 所有命令验证通过(含3条批量操作命令)'"
|
||||
needs:
|
||||
- git_clone_0
|
||||
- ref: end
|
||||
name: 结束
|
||||
task: end
|
||||
needs:
|
||||
- ssh_cmd_0
|
||||
|
|
@ -1,41 +0,0 @@
|
|||
version: 2
|
||||
name: wyx_gitlink_cli_ci
|
||||
description: "master 分支收到合并后自动执行 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: git clone
|
||||
task: git_clone@1.2.9
|
||||
input:
|
||||
remote_url: '"https://gitlink.org.cn/jiangtx/gitlink-cli.git"'
|
||||
ref: '"refs/heads/master"'
|
||||
commit_id: '""'
|
||||
depth: 1
|
||||
needs:
|
||||
- start
|
||||
- ref: ssh_cmd_0
|
||||
name: CI 检查
|
||||
task: ssh_cmd@1.1.1
|
||||
input:
|
||||
ssh_ip: '"121.41.212.97"'
|
||||
ssh_port: '"22"'
|
||||
ssh_user: '"root"'
|
||||
ssh_pass: ((gitlink_cli.wyx_ssh_pass))
|
||||
ssh_cmd: >-
|
||||
"cd /root && rm -rf gitlink-cli && git clone --depth=1 -b master 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 && echo '>>> 1. 编译检查' && go build ./... && echo '>>> 2. 静态分析' && go vet ./... && echo '>>> 3. 单元测试' && go test -race ./... && echo '>>> 4. 格式化修复' && gofmt -s -w . && echo '✅ 所有 CI 检查通过'"
|
||||
needs:
|
||||
- git_clone_0
|
||||
- ref: end
|
||||
name: 结束
|
||||
task: end
|
||||
needs:
|
||||
- ssh_cmd_0
|
||||
|
|
@ -1,41 +0,0 @@
|
|||
version: 2
|
||||
name: wyx_gitlink_cli_release
|
||||
description: "部署流水线:打 tag 时交叉编译 Linux/Windows/macOS 三平台二进制并发布 GitLink Release"
|
||||
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: git clone(含 tag 历史)
|
||||
task: git_clone@1.2.9
|
||||
input:
|
||||
remote_url: '"https://gitlink.org.cn/jiangtx/gitlink-cli.git"'
|
||||
ref: '"refs/heads/master"'
|
||||
commit_id: '""'
|
||||
depth: 1
|
||||
needs:
|
||||
- start
|
||||
- ref: ssh_cmd_0
|
||||
name: 交叉编译并发布 Release
|
||||
task: ssh_cmd@1.1.1
|
||||
input:
|
||||
ssh_ip: '"121.41.212.97"'
|
||||
ssh_port: '"22"'
|
||||
ssh_user: '"root"'
|
||||
ssh_pass: ((gitlink_cli.wyx_ssh_pass))
|
||||
ssh_cmd: >-
|
||||
"cd /root && rm -rf release && mkdir release && cd release && git clone https://gitlink.org.cn/jiangtx/gitlink-cli.git && cd gitlink-cli && git fetch --tags && export PATH=$PATH:/usr/local/go/bin && export GOPROXY=https://goproxy.cn,direct && VERSION=$(git describe --tags --abbrev=0 2>/dev/null || echo dev) && echo '>>> 发布版本: '$VERSION && echo '>>> 1. 编译 Linux amd64' && GOOS=linux GOARCH=amd64 go build -o gitlink-cli-linux-amd64 . && echo '>>> 2. 编译 Windows amd64' && GOOS=windows GOARCH=amd64 go build -o gitlink-cli-windows-amd64.exe . && echo '>>> 3. 编译 macOS arm64' && GOOS=darwin GOARCH=arm64 go build -o gitlink-cli-darwin-arm64 . && echo '>>> 4. 发布 GitLink Release(用服务器已登录的 gitlink-cli)' && gitlink-cli release +create --owner jiangtx --repo gitlink-cli --tag $VERSION --name $VERSION --target master --body '本次发布版本 $VERSION,含 Linux/Windows/macOS 三平台二进制。新增 pm/wiki/export/alias/browse/status 等模块,详见变更说明。' && echo '✅ Release '$VERSION' 发布完成'"
|
||||
needs:
|
||||
- git_clone_0
|
||||
- ref: end
|
||||
name: 结束
|
||||
task: end
|
||||
needs:
|
||||
- ssh_cmd_0
|
||||
|
|
@ -1,15 +0,0 @@
|
|||
# 统一文本文件为 LF 行尾。
|
||||
# Windows 上 core.autocrlf 会把 LF 转成 CRLF,导致 gofmt -s -l / CI make fmt
|
||||
# 误报整个文件被改动。固定 LF 后所有平台一致。
|
||||
* text=auto eol=lf
|
||||
|
||||
# 二进制文件(显式标记,避免被当作文本转码)
|
||||
*.png binary
|
||||
*.jpg binary
|
||||
*.jpeg binary
|
||||
*.gif binary
|
||||
*.ico binary
|
||||
*.pdf binary
|
||||
*.exe binary
|
||||
*.zip binary
|
||||
*.gz binary
|
||||
|
|
@ -15,7 +15,7 @@ jobs:
|
|||
|
||||
- uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: '1.26.1'
|
||||
go-version: '1.22'
|
||||
|
||||
- name: Build
|
||||
run: go build ./...
|
||||
|
|
@ -23,8 +23,8 @@ jobs:
|
|||
- name: Lint
|
||||
run: make lint
|
||||
|
||||
- name: Test with coverage gate
|
||||
run: make cover
|
||||
- name: Test
|
||||
run: make test
|
||||
|
||||
- name: Check formatting
|
||||
run: make fmt
|
||||
|
|
|
|||
|
|
@ -26,14 +26,5 @@ jobs:
|
|||
- name: Scan i18n key references
|
||||
run: go run ./internal/i18n/cmd/check --scan-code
|
||||
|
||||
- name: Run Go tests with coverage gate
|
||||
run: make cover
|
||||
|
||||
- name: Upload coverage artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: coverage-${{ github.sha }}
|
||||
path: |
|
||||
coverage.out
|
||||
coverage.html
|
||||
if-no-files-found: ignore
|
||||
- name: Run Go tests
|
||||
run: go test ./...
|
||||
|
|
|
|||
|
|
@ -1,14 +1,3 @@
|
|||
|
||||
coverage
|
||||
coverage.out
|
||||
|
||||
# 验证临时产物
|
||||
coverage.verify.out
|
||||
coverage.html
|
||||
|
||||
# 课程任务材料(非代码)
|
||||
课程实践任务及要求*.pdf
|
||||
|
||||
# 构建产物二进制(go build / make build 本地产出,不入库;分发走 Release 附件)
|
||||
gitlink-cli.exe
|
||||
/gitlink-cli
|
||||
/gitlink-cli.exe
|
||||
|
|
|
|||
20
Dockerfile
20
Dockerfile
|
|
@ -1,20 +0,0 @@
|
|||
FROM docker.1ms.run/library/golang:1.26-alpine AS builder
|
||||
|
||||
WORKDIR /build
|
||||
|
||||
COPY go.mod go.sum ./
|
||||
RUN go mod download
|
||||
|
||||
COPY . .
|
||||
|
||||
RUN CGO_ENABLED=0 go build -ldflags "-s -w" -o gitlink-cli .
|
||||
|
||||
FROM docker.1ms.run/library/alpine:3.20
|
||||
|
||||
RUN apk add --no-cache ca-certificates git
|
||||
|
||||
COPY --from=builder /build/gitlink-cli /usr/local/bin/gitlink-cli
|
||||
|
||||
RUN chmod +x /usr/local/bin/gitlink-cli
|
||||
|
||||
ENTRYPOINT ["gitlink-cli"]
|
||||
10
Makefile
10
Makefile
|
|
@ -28,17 +28,9 @@ fmt:
|
|||
exit 1; \
|
||||
fi
|
||||
|
||||
COVER_THRESHOLD ?= 78
|
||||
|
||||
cover:
|
||||
go test -coverprofile=coverage.out ./...
|
||||
@go tool cover -func=coverage.out | grep '^total:'
|
||||
@COV=$$(go tool cover -func=coverage.out | grep '^total:' | awk '{print int($$3)}'); \
|
||||
echo "总覆盖率: $$COV% (门禁阈值: $(COVER_THRESHOLD)%)"; \
|
||||
if [ $$COV -lt $(COVER_THRESHOLD) ]; then \
|
||||
echo "❌ 覆盖率 $$COV% 低于门禁阈值 $(COVER_THRESHOLD)%"; exit 1; \
|
||||
fi; \
|
||||
echo "✓ 覆盖率达标"
|
||||
go tool cover -func=coverage.out
|
||||
|
||||
lint:
|
||||
golangci-lint run ./...
|
||||
|
|
|
|||
834
README.zh-CN.md
834
README.zh-CN.md
|
|
@ -1,266 +1,744 @@
|
|||
# gitlink-cli(jiangtx 小组·竞赛增强版)
|
||||
# gitlink-cli
|
||||
|
||||
[](https://www.gitlink.org.cn/jiangtx/gitlink-cli)
|
||||
[](https://www.gitlink.org.cn/gitlink/gitlink-cli)
|
||||
[](https://license.coscl.org.cn/mulanpsl2)
|
||||
[](https://www.gitlink.org.cn/Gitlink/gitlink-cli)
|
||||
[](https://license.coscl.org.cn/MulanPSL2)
|
||||
[](https://golang.org)
|
||||
[](https://www.npmjs.com/package/@gitlink-ai/cli)
|
||||
[](./.devops/ci.yml)
|
||||
|
||||
> 本仓库是 [gitlink/gitlink-cli](https://www.gitlink.org.cn/gitlink/gitlink-cli) 的竞赛 Fork,由 **jiangtx 小组**为《软件演化与运维》进阶任务 + GitLink 大赛开发。
|
||||
> 在官方 **40+ 命令 / 11 Skill** 基础上,新增 **12 个命令模块 / 26 个 Skill / 端到端自动化工作流 / 科研智能服务**,覆盖开发者协作与科研辅助两大场景,兼容 Claude Code / OpenClaw / Cursor 等 AI Agent。
|
||||
[GitLink(确实开源)](https://www.gitlink.org.cn) 官方 CLI 工具 — 为人类和 AI Agent 双重设计。支持 **macOS、Linux、Windows**,覆盖仓库管理、Issue 追踪、Pull Request、Webhook、成员协作、CI/CD 和 AI 自动化工作流,包含 40+ 命令和 AI Agent [Skills](./skills/README.md)。
|
||||
|
||||
🌐 **科研服务在线演示**:<http://121.41.212.97/skills/>(Flask + DeepSeek + gitlink-cli,14 个 Skill)
|
||||
**[English](./README.md)**
|
||||
|
||||
[安装](#安装与快速开始) · [功能矩阵](#功能矩阵) · [子任务成果](#四个子任务成果) · [Skills](./skills/README.md) · [复现指南](./REPRODUCE.md) · [课程报告](./docs/reports/README.md) · [上游 PR](./docs/UPSTREAM_PRS.md)
|
||||
[安装](#安装与快速上手) · [AI Agent Skills](#ai-agent-skills) · [认证](#配置与使用) · [命令](#使用示例) · [贡献](#相关项目)
|
||||
|
||||
---
|
||||
## 贡献者
|
||||
|
||||
## 小组成员
|
||||
<div style="display: flex; gap: 16px; flex-wrap: wrap; align-items: flex-start;">
|
||||
<div align="center">
|
||||
<a href="https://www.gitlink.org.cn/wangyue111" title="wangyue111"><img src="https://www.gitlink.org.cn/system/lets/letter_avatars/2/W/43_254_70/120.png" width="40" height="40" alt="wangyue111" style="border-radius: 50%;"></a>
|
||||
<br><sub><a href="https://www.gitlink.org.cn/wangyue111">wangyue111</a></sub>
|
||||
</div>
|
||||
<div align="center">
|
||||
<a href="https://www.gitlink.org.cn/wbtiger" title="tigerwang"><img src="https://www.gitlink.org.cn/system/lets/letter_avatars/2/T/14_168_39/120.png" width="40" height="40" alt="wbtiger" style="border-radius: 50%;"></a>
|
||||
<br><sub><a href="https://www.gitlink.org.cn/wbtiger">wbtiger</a></sub>
|
||||
</div>
|
||||
<div align="center">
|
||||
<a href="https://www.gitlink.org.cn/Mengz" title="Mengz"><img src="https://www.gitlink.org.cn/system/lets/letter_avatars/2/M/166_152_185/120.png" width="40" height="40" alt="Mengz" style="border-radius: 50%;"></a>
|
||||
<br><sub><a href="https://www.gitlink.org.cn/Mengz">Mengz</a></sub>
|
||||
</div>
|
||||
<div align="center">
|
||||
<a href="https://www.gitlink.org.cn/yangsai" title="杨赛"><img src="https://www.gitlink.org.cn/system/lets/letter_avatars/2/Y/94_150_149/120.png" width="40" height="40" alt="yangsai" style="border-radius: 50%;"></a>
|
||||
<br><sub><a href="https://www.gitlink.org.cn/yangsai">yangsai</a></sub>
|
||||
</div>
|
||||
<div align="center">
|
||||
<a href="https://www.gitlink.org.cn/mengcheng" title="camelliamc"><img src="https://www.gitlink.org.cn/system/lets/letter_avatars/2/M/206_114_54/120.png" width="40" height="40" alt="mengcheng" style="border-radius: 50%;"></a>
|
||||
<br><sub><a href="https://www.gitlink.org.cn/mengcheng">mengcheng</a></sub>
|
||||
</div>
|
||||
<div align="center">
|
||||
<a href="https://www.gitlink.org.cn/muel" title="赵奕程"><img src="https://www.gitlink.org.cn/images/avatars/User/149182?t=1779603476" width="40" height="40" alt="muel" style="border-radius: 50%;"></a>
|
||||
<br><sub><a href="https://www.gitlink.org.cn/muel">muel</a></sub>
|
||||
</div>
|
||||
<div align="center">
|
||||
<a href="https://www.gitlink.org.cn/Leo77" title="Leo77"><img src="https://www.gitlink.org.cn/system/lets/letter_avatars/2/L/173_120_149/120.png" width="40" height="40" alt="Leo77" style="border-radius: 50%;"></a>
|
||||
<br><sub><a href="https://www.gitlink.org.cn/Leo77">Leo77</a></sub>
|
||||
</div>
|
||||
<div align="center">
|
||||
<a href="https://www.gitlink.org.cn/yingjie" title="yingjie"><img src="https://www.gitlink.org.cn/images/avatars/User/145288?t=1765791899" width="40" height="40" alt="yingjie" style="border-radius: 50%;"></a>
|
||||
<br><sub><a href="https://www.gitlink.org.cn/yingjie">yingjie</a></sub>
|
||||
</div>
|
||||
<div align="center">
|
||||
<a href="https://www.gitlink.org.cn/topshare" title="Kevin Zhang"><img src="https://www.gitlink.org.cn/system/lets/letter_avatars/2/K/65_152_142/120.png" width="40" height="40" alt="topshare" style="border-radius: 50%;"></a>
|
||||
<br><sub><a href="https://www.gitlink.org.cn/topshare">topshare</a></sub>
|
||||
</div>
|
||||
<div align="center">
|
||||
<a href="https://www.gitlink.org.cn/dtwdtw" title="dtwdtw"><img src="https://www.gitlink.org.cn/system/lets/letter_avatars/2/D/53_166_51/120.png" width="40" height="40" alt="dtwdtw" style="border-radius: 50%;"></a>
|
||||
<br><sub><a href="https://www.gitlink.org.cn/dtwdtw">dtwdtw</a></sub>
|
||||
</div>
|
||||
<div align="center">
|
||||
<a href="https://www.gitlink.org.cn/recorder" title="recorder"><img src="https://www.gitlink.org.cn/system/lets/letter_avatars/2/R/141_201_87/120.png" width="40" height="40" alt="recorder" style="border-radius: 50%;"></a>
|
||||
<br><sub><a href="https://www.gitlink.org.cn/recorder">recorder</a></sub>
|
||||
</div>
|
||||
<div align="center">
|
||||
<a href="https://www.gitlink.org.cn/puygob236" title="Jiachen Li"><img src="https://www.gitlink.org.cn/images/avatars/User/149183?t=1778815174" width="40" height="40" alt="puygob236" style="border-radius: 50%;"></a>
|
||||
<br><sub><a href="https://www.gitlink.org.cn/puygob236">puygob236</a></sub>
|
||||
</div>
|
||||
<div align="center">
|
||||
<a href="https://www.gitlink.org.cn/co63oc" title="co63oc"><img src="https://www.gitlink.org.cn/system/lets/letter_avatars/2/C/205_201_141/120.png" width="40" height="40" alt="co63oc" style="border-radius: 50%;"></a>
|
||||
<br><sub><a href="https://www.gitlink.org.cn/co63oc">co63oc</a></sub>
|
||||
</div>
|
||||
<div align="center">
|
||||
<a href="https://www.gitlink.org.cn/lindiwen23" title="lindiwen23"><img src="https://www.gitlink.org.cn/images/avatars/User/141609?t=1748270628" width="40" height="40" alt="lindiwen23" style="border-radius: 50%;"></a>
|
||||
<br><sub><a href="https://www.gitlink.org.cn/lindiwen23">lindiwen23</a></sub>
|
||||
</div>
|
||||
<div align="center">
|
||||
<a href="https://www.gitlink.org.cn/ohanabi" title="ohanabi"><img src="https://www.gitlink.org.cn/images/avatars/User/148166?t=1778230283" width="40" height="40" alt="ohanabi" style="border-radius: 50%;"></a>
|
||||
<br><sub><a href="https://www.gitlink.org.cn/ohanabi">ohanabi</a></sub>
|
||||
</div>
|
||||
<div align="center">
|
||||
<a href="https://www.gitlink.org.cn/jiangtx" title="jiangtx"><img src="https://www.gitlink.org.cn/system/lets/letter_avatars/2/J/67_157_94/120.png" width="40" height="40" alt="jiangtx" style="border-radius: 50%;"></a>
|
||||
<br><sub><a href="https://www.gitlink.org.cn/jiangtx">jiangtx</a></sub>
|
||||
</div>
|
||||
<div align="center">
|
||||
<a href="https://www.gitlink.org.cn/luwanzhou" title="luwanzhou"><img src="https://www.gitlink.org.cn/system/lets/letter_avatars/2/L/165_135_246/120.png" width="40" height="40" alt="luwanzhou" style="border-radius: 50%;"></a>
|
||||
<br><sub><a href="https://www.gitlink.org.cn/luwanzhou">luwanzhou</a></sub>
|
||||
</div>
|
||||
<div align="center">
|
||||
<a href="https://www.gitlink.org.cn/whale_hihihi" title="whale_hihihi"><img src="https://www.gitlink.org.cn/images/avatars/User/137722?t=1778575729" width="40" height="40" alt="whale_hihihi" style="border-radius: 50%;"></a>
|
||||
<br><sub><a href="https://www.gitlink.org.cn/whale_hihihi">whale_hihihi</a></sub>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
| 成员 | 主责领域 |
|
||||
|---|---|
|
||||
| wyx / wyxfzgg | pm / wiki / export / alias·browse·status 命令、安装脚本、DevOps 流水线、编排 Skill |
|
||||
| 林迪文(lindiwen23) | notification / search / ci / org 增强、gitlink-web 科研服务、research-tracker 等 Skill |
|
||||
| whzy | workflow 命令、i18n 本地化、编排 Skill、show/demo 离线演示 |
|
||||
| wangyue789 | pipeline 命令 |
|
||||
| Mengz | webhook 命令 |
|
||||
| 蒋天翔(jiangtx) | repo/pr/user 增强、capability、Skill 收录协调 |
|
||||
| 何开元 | label 命令 |
|
||||
## 为什么选择 gitlink-cli?
|
||||
|
||||
> 个人工作量以 GitLink 提交记录为准(`git shortlog -sn`)。
|
||||
- **Agent-Native 设计** — 开箱即用结构化 [Skills](./skills/README.md),兼容 Claude Code — Agent 零配置即可操作 GitLink
|
||||
- **广泛覆盖** — 仓库、Issue、PR、Webhook、成员、分支、Release、CI、Pipeline、组织、搜索、用户等常用工作流均提供高层命令
|
||||
- **AI 友好 & 优化** — 每条命令都经过真实 Agent 测试,简洁参数、智能默认值、结构化输出
|
||||
- **跨平台** — macOS、Linux、Windows (x64/arm64) 全支持,`npm` 一条命令安装
|
||||
- **开源零门槛** — 木兰宽松许可证第2版(MulanPSL-2.0),`npm install` 即用
|
||||
- **3 分钟上手** — 交互式登录或 `GITLINK_TOKEN` 环境变量,从安装到首次 API 调用仅需 3 步
|
||||
- **安全可控** — OS 原生 keychain 凭证存储,`GITLINK_TOKEN` 环境变量支持 CI/CD 和非交互环境,自动 git remote 上下文解析
|
||||
- **三层架构** — Shortcuts(人+AI友好)→ Raw API(全覆盖)→ Config(配置管理)
|
||||
|
||||
---
|
||||
## 功能一览
|
||||
|
||||
## 为什么用 gitlink-cli(竞赛增强版)
|
||||
| 分类 | 能力 |
|
||||
|------|------|
|
||||
| 📦 仓库 | 列出、创建、Fork、删除仓库,查看仓库信息、洞察数据和互动状态 |
|
||||
| 🐛 Issue | 创建、更新、关闭、批量关闭/更新/删除、评论 Issue |
|
||||
| 🔖 标签 | 创建、列出、更新、删除 Issue 标签 |
|
||||
| 🔀 PR | 创建、合并、Review Pull Request,查看变更文件 |
|
||||
| 👥 成员 | 列出、添加、移除仓库成员,调整角色,生成和接受邀请链接 |
|
||||
| 🌿 分支 | 创建、删除、保护分支 |
|
||||
| 🏷️ 发布 | 创建、编辑、更新、查看、删除 Release |
|
||||
| 🏢 组织 | 管理组织、成员、团队 |
|
||||
| 🔧 CI | 查看构建、日志、CI/CD 操作 |
|
||||
| ⚙️ Pipeline | 运行、查看、启停、删除流水线工作流并查询日志 |
|
||||
| 📖 Wiki | 列出、查看、创建、更新、删除 Wiki 页面 |
|
||||
| 🔍 搜索 | 搜索仓库、用户 |
|
||||
| 📊 数据集 | 按项目查询科研数据集 |
|
||||
| 👤 用户 | 查看用户资料和信息 |
|
||||
| 📊 画像 | 用户开发能力、角色定位、专业定位、近期活动、贡献热力图统计 |
|
||||
| 📋 项目管理 | Sprint 管理、看板、周报 |
|
||||
| 🤖 工作流 | AI 驱动的 Issue 分类、PR Review、Release Notes |
|
||||
|
||||
- **Agent 原生** — 内置结构化 [Skills](./skills/),Claude Code / OpenClaw / Cursor 等 Agent 零配置即可操作 GitLink。
|
||||
- **覆盖广** — 仓库、Issue、PR、Webhook、成员、分支、Release、CI、Pipeline、组织、搜索、用户、**项目管理(PM)、Wiki、数据导出、通知、能力探测、对比、工作流分析** 全覆盖。
|
||||
- **批量操作** — 批量关闭 Issue、批量更新 Issue 状态、批量添加成员(支持 CSV + `--dry-run` 预览)。
|
||||
- **科研赋能** — 科研智能服务覆盖热点追踪 / 项目洞悉 / 合规检查 / 协作匹配 / 进度预警全链路。
|
||||
- **端到端工作流** — 社区运营、代码质量看门人、项目一键初始化、多仓库协同、贡献者成长等编排 Skill。
|
||||
- **跨平台** — macOS / Linux / Windows(x64/arm64),`npm install -g @gitlink-ai/cli` 一键安装。
|
||||
- **三层架构** — Shortcuts(人 & AI 友好)→ Raw API(全覆盖)→ Config(配置管理)。
|
||||
## 安装与快速上手
|
||||
|
||||
---
|
||||
### 前置条件
|
||||
|
||||
## 功能矩阵
|
||||
- Node.js 14+(`npm`/`npx`)— 用于 npm 安装
|
||||
- 支持平台:macOS、Linux、Windows(x64/arm64)
|
||||
- Go 1.26+ — 仅从源码构建时需要
|
||||
|
||||
**21 个命令组(~129 子命令)**
|
||||
### 快速上手(人类用户)
|
||||
|
||||
| 命令组 | 说明 | 本组新增 |
|
||||
|---|---|:-:|
|
||||
| repo / issue / pr / release / branch / user / org / search / ci | 官方核心命令(含本组增强:repo 文件操作、issue 批量、pr commits/versions 等) | 部分 |
|
||||
| **pm** | 项目管理:仪表盘/Sprint/周报/标签/流水线/运行 | ✨ |
|
||||
| **wiki** | Wiki 页面:pages/get/create/update/delete | ✨ |
|
||||
| **export** | 数据导出:issues/prs/contributors → CSV/JSON | ✨ |
|
||||
| **notification** | 通知:list/read/read-all/watch | ✨ |
|
||||
| **capability** | 后端 API 能力探测(跨实例兼容) | ✨ |
|
||||
| **compare** | 分支/标签/commit 对比 | ✨ |
|
||||
| **workflow** | 本地规则工作流:triage/health/repo-report/pr-summary | ✨ |
|
||||
| label / member / milestone / pipeline / webhook | 标签 / 成员(含批量) / 里程碑 / 流水线 / Webhook | ✨ |
|
||||
| alias / browse / status | 开发者体验:别名 / 浏览器跳转 / 状态 | ✨ |
|
||||
| api / auth / config / show / demo / completion / version | 基础设施 + 离线演示 | 部分 |
|
||||
> **AI 助手请注意:** 如果你是帮助用户安装的 AI Agent,请直接跳到 [快速上手(AI Agent)](#快速上手ai-agent),其中包含你需要完成的所有步骤。
|
||||
|
||||
**37 个 AI Agent Skill**(官方 11 + 新增 26) · 详见 [skills/README.md](./skills/README.md)
|
||||
**端到端自动化工作流** · 详见 [examples/workflows/](./examples/workflows/)
|
||||
**科研智能服务** · 详见 [gitlink-web/](./gitlink-web/README.md)
|
||||
#### 安装
|
||||
|
||||
---
|
||||
选择以下**任一**方式:
|
||||
|
||||
## 安装与快速开始
|
||||
|
||||
### 要求
|
||||
|
||||
- Node.js 14+(`npm`/`npx`)用于 npm 安装;Go 1.26+ 用于源码构建;支持 macOS/Linux/Windows。
|
||||
|
||||
### 安装
|
||||
**方式 1 — 从 npm 安装(推荐):**
|
||||
|
||||
```bash
|
||||
# 方式 A:npm(推荐,一条命令装好 CLI + Skills)
|
||||
# 安装 CLI
|
||||
npm install -g @gitlink-ai/cli
|
||||
|
||||
# 方式 B:源码构建(需 Go 1.26+)
|
||||
git clone https://www.gitlink.org.cn/jiangtx/gitlink-cli.git
|
||||
cd gitlink-cli && go build -o gitlink-cli . # Windows: go build -o gitlink-cli.exe .
|
||||
# 安装 CLI Skill(必须,全平台通用)
|
||||
gitlink-cli-install-skills
|
||||
|
||||
# 也可使用 npx 安装 Skill
|
||||
npx skills add ccfos/gitlink-cli/skills -y -g
|
||||
```
|
||||
|
||||
### 配置与登录
|
||||
**方式 2 — 从源码构建:**
|
||||
|
||||
需要 Go 1.26+。
|
||||
|
||||
```bash
|
||||
gitlink-cli config init # 一次性引导配置
|
||||
gitlink-cli auth login # 交互登录(账号密码,推荐)
|
||||
gitlink-cli auth login --token # 或粘贴私有 token
|
||||
export GITLINK_TOKEN="your-token" # 或设环境变量(CI/CD、非交互)
|
||||
gitlink-cli auth status # 确认登录
|
||||
git clone https://www.gitlink.org.cn/Gitlink/gitlink-cli.git
|
||||
cd gitlink-cli
|
||||
make install
|
||||
|
||||
# 安装 CLI Skill(必须)
|
||||
npx skills add ./skills -y -g
|
||||
```
|
||||
|
||||
### 3 分钟体验
|
||||
> **Windows 用户注意:** 请在 PowerShell 或 CMD 中运行 `npm install -g @gitlink-ai/cli`。从源码构建请使用 `go install .` 代替 `make install`。
|
||||
|
||||
#### 配置与使用
|
||||
|
||||
```bash
|
||||
gitlink-cli repo +info --owner Gitlink --repo gitlink-cli # 仓库信息
|
||||
gitlink-cli issue +list --owner Gitlink --repo gitlink-cli # Issue 列表
|
||||
gitlink-cli workflow +triage --owner Gitlink --repo gitlink-cli # AI 分拣
|
||||
gitlink-cli export +contributors --owner Gitlink --repo gitlink-cli --format json # 导出
|
||||
gitlink-cli capability +check # 后端能力探测
|
||||
# 1. 配置(首次使用,交互式引导)
|
||||
gitlink-cli config init
|
||||
|
||||
# 2. 登录(任选其一)
|
||||
gitlink-cli auth login # 用户名密码(推荐)
|
||||
gitlink-cli auth login --token # 或粘贴私人令牌
|
||||
export GITLINK_TOKEN="your-token" # 或设置环境变量(适用于 CI/CD、非交互环境)
|
||||
|
||||
# 3. 开始使用
|
||||
gitlink-cli repo +list
|
||||
```
|
||||
|
||||
---
|
||||
### 快速上手(AI Agent)
|
||||
|
||||
> 以下步骤面向 AI Agent。部分步骤需要用户在浏览器中完成操作。
|
||||
|
||||
**第 1 步 — 安装**
|
||||
|
||||
```bash
|
||||
# 安装 CLI
|
||||
npm install -g @gitlink-ai/cli
|
||||
|
||||
# 安装 CLI Skill(必须,全平台通用)
|
||||
gitlink-cli-install-skills
|
||||
```
|
||||
|
||||
**第 2 步 — 配置**
|
||||
|
||||
```bash
|
||||
gitlink-cli config init
|
||||
```
|
||||
|
||||
**第 3 步 — 登录**
|
||||
|
||||
交互环境:
|
||||
```bash
|
||||
gitlink-cli auth login
|
||||
```
|
||||
|
||||
非交互环境(CI/CD、Trae 沙箱、MCP 等):
|
||||
```bash
|
||||
export GITLINK_TOKEN="your-private-token"
|
||||
```
|
||||
|
||||
> 获取私人令牌:GitLink 网页端 → 个人设置 → 私人令牌。
|
||||
|
||||
**第 4 步 — 验证**
|
||||
|
||||
```bash
|
||||
gitlink-cli user +me
|
||||
```
|
||||
|
||||
## 使用示例
|
||||
|
||||
### 新增命令(本组贡献)
|
||||
### 仓库操作
|
||||
|
||||
```bash
|
||||
# 项目管理
|
||||
gitlink-cli pm +dashboards --owner jiangtx --repo gitlink-cli
|
||||
gitlink-cli pm +weekly --owner jiangtx --repo gitlink-cli
|
||||
# 列出仓库
|
||||
gitlink-cli repo +list
|
||||
|
||||
# Wiki 管理
|
||||
gitlink-cli wiki +pages --owner jiangtx --repo gitlink-cli
|
||||
gitlink-cli wiki +create --owner jiangtx --repo gitlink-cli --title "周报" --content "..."
|
||||
# 查看仓库信息
|
||||
gitlink-cli repo +info --owner Gitlink --repo forgeplus
|
||||
|
||||
# 数据导出(CSV/JSON,科研/报表数据源)
|
||||
gitlink-cli export +issues --owner jiangtx --repo gitlink-cli --format csv --output issues.csv
|
||||
gitlink-cli export +contributors --owner jiangtx --repo gitlink-cli --format json
|
||||
# 读取仓库 README
|
||||
gitlink-cli repo +readme --owner Gitlink --repo forgeplus --ref master
|
||||
|
||||
# 通知管理
|
||||
gitlink-cli notification +list
|
||||
gitlink-cli notification +watch --owner jiangtx --repo gitlink-cli
|
||||
# 列出仓库根目录或指定目录文件
|
||||
gitlink-cli repo +tree --owner Gitlink --repo forgeplus --ref master
|
||||
gitlink-cli repo +tree --owner Gitlink --repo forgeplus --path src --ref main
|
||||
|
||||
# 能力探测 + 对比 + 工作流分析
|
||||
gitlink-cli capability +check
|
||||
gitlink-cli compare +view --head feature --base master
|
||||
gitlink-cli workflow +health --repository Gitlink/gitlink-cli --open-issues 3 --open-prs 1 --has-readme --has-license
|
||||
# 查看语言占比
|
||||
gitlink-cli repo +languages --owner Gitlink --repo forgeplus
|
||||
|
||||
# 批量操作
|
||||
gitlink-cli issue +batch-close --owner jiangtx --repo gitlink-cli --numbers 1,2,3 --dry-run
|
||||
gitlink-cli member +batch-add --owner jiangtx --repo gitlink-cli --user-ids 101,102
|
||||
# 列出贡献者
|
||||
gitlink-cli repo +contributors --owner Gitlink --repo forgeplus
|
||||
|
||||
# 开发者体验
|
||||
gitlink-cli alias +set rl "repo +list"
|
||||
gitlink-cli browse issues/42
|
||||
gitlink-cli status
|
||||
# 查看分支、标签或提交的贡献者代码行统计
|
||||
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 "项目描述"
|
||||
|
||||
# Fork 仓库
|
||||
gitlink-cli repo +fork --owner Gitlink --repo forgeplus
|
||||
```
|
||||
|
||||
### 核心命令(沿用上游)
|
||||
### Webhook 管理
|
||||
|
||||
```bash
|
||||
gitlink-cli repo +list / +info / +create / +fork
|
||||
gitlink-cli issue +create / +list / +view / +close / +comment
|
||||
gitlink-cli pr +list / +create / +view / +merge / +review / +versions
|
||||
gitlink-cli release +list / +create / +view
|
||||
gitlink-cli branch +list / +create / +protect
|
||||
gitlink-cli webhook +list / +create / +test
|
||||
gitlink-cli ci +builds / +logs / +restart
|
||||
gitlink-cli pipeline +runs / +run / +logs
|
||||
gitlink-cli search +repos / +users / +code / +issues
|
||||
gitlink-cli api GET /users/me # Raw API(未封装接口)
|
||||
# 列出 webhook
|
||||
gitlink-cli webhook +list --owner Gitlink --repo forgeplus
|
||||
|
||||
# 创建 webhook
|
||||
gitlink-cli webhook +create --owner Gitlink --repo forgeplus \
|
||||
--url https://example.com/hook --events push,create
|
||||
|
||||
# 测试 webhook
|
||||
gitlink-cli webhook +test --owner Gitlink --repo forgeplus --id 68
|
||||
|
||||
# 查看 webhook 投递任务
|
||||
gitlink-cli webhook +tasks --owner Gitlink --repo forgeplus --id 68
|
||||
```
|
||||
|
||||
### 全局参数
|
||||
### Wiki 管理
|
||||
|
||||
| 参数 | 说明 |
|
||||
|---|---|
|
||||
| `--owner` / `--repo` | 仓库所有者 / 名称(git 仓库内自动从 remote 解析) |
|
||||
| `--format` | 输出格式 json / table / yaml(workflow 另支持 markdown) |
|
||||
| `--debug` | 调试输出 |
|
||||
```bash
|
||||
# 列出 Wiki 页面(目录结构)
|
||||
gitlink-cli wiki +list --owner Gitlink --repo forgeplus --project-id 12345
|
||||
|
||||
---
|
||||
# 查看 Wiki 页面
|
||||
gitlink-cli wiki +view --owner Gitlink --repo forgeplus --project-id 12345 -n home
|
||||
|
||||
## 四个子任务成果
|
||||
# 创建 Wiki 页面
|
||||
gitlink-cli wiki +create --owner Gitlink --repo forgeplus --project-id 12345 \
|
||||
-n getting-started -t "快速开始" -c "# 快速开始指南"
|
||||
|
||||
### 子任务一·扩展 CLI(50%)
|
||||
新增 12 个命令模块、~61 个子命令、3 条批量操作。每个新模块含源码 + 单元测试 + 命令帮助文档 + 变更说明。详见 [doc/commands/](./doc/commands/)、[doc/changes/](./doc/changes/)。
|
||||
# 更新 Wiki 页面标题和/或内容
|
||||
gitlink-cli wiki +update --owner Gitlink --repo forgeplus --project-id 12345 -n home -t "新标题"
|
||||
gitlink-cli wiki +update --owner Gitlink --repo forgeplus --project-id 12345 -n home -c "# 更新后的内容"
|
||||
gitlink-cli wiki +update --owner Gitlink --repo forgeplus --project-id 12345 -n home -t "新标题" -c "新内容"
|
||||
|
||||
### 子任务二·开发 Skill(20%)
|
||||
新增 26 个 Skill,覆盖智能代码审查 / Issue 分拣 / Release Notes / 项目健康度 / 许可证合规 / 新人引导 6 大场景。6 个已通过 Agent 平台验证。详见 [skills/README.md](./skills/README.md)。
|
||||
# 删除 Wiki 页面
|
||||
gitlink-cli wiki +delete --owner Gitlink --repo forgeplus --project-id 12345 -n old-page
|
||||
```
|
||||
|
||||
### 子任务三·端到端自动化工作流(20%)
|
||||
[community-ops-automation](./examples/workflows/community-ops-automation/):串联 `repo+info → issue+list → pr+list → release+list → issue+comment` 共 4–5 条命令,在 `Gitlink/gitlink-cli` 与 `jiangtx/gitlink-cli` 真实项目跑通,含 Python/PowerShell/bash 脚本、JSON 配置、单元测试、SVG 架构图、验证记录。另有 5 个编排 Skill(社区运营/代码质量看门人/项目初始化/多仓库协同/贡献者成长)。
|
||||
### 成员管理
|
||||
|
||||
### 子任务四·应用 GitLink 辅助科研(加分)
|
||||
[科研智能服务 gitlink-web](./gitlink-web/README.md) + [gitlink-research-tracker](./skills/gitlink-research-tracker/) Skill:技术调研(多关键词搜索→成熟度评分→趋势→选型),覆盖热点追踪 / 项目洞悉 / 合规检查 / 协作匹配 / 进度跟踪 5 个科研场景。
|
||||
```bash
|
||||
# 列出仓库成员
|
||||
gitlink-cli member +list --owner Gitlink --repo forgeplus
|
||||
|
||||
### DevOps 流水线(10%)
|
||||
[.devops/](./.devops/) 建木流水线:`ci.yml`(test/vet/fmt)· `build.yml`(编译+命令注册验证)· `gitlink-web.yml`(科研服务部署)。master 合并自动触发。
|
||||
# 添加成员
|
||||
gitlink-cli member +add --owner Gitlink --repo forgeplus --user-id 101
|
||||
|
||||
---
|
||||
# 预览批量添加成员,不修改数据
|
||||
gitlink-cli member +batch-add --owner Gitlink --repo forgeplus --user-ids 101,102 --dry-run
|
||||
|
||||
# 从 CSV 文件批量添加成员
|
||||
gitlink-cli member +batch-add --owner Gitlink --repo forgeplus --from members.csv
|
||||
|
||||
# 调整成员权限
|
||||
gitlink-cli member +role --owner Gitlink --repo forgeplus --user-id 101 --role Developer
|
||||
|
||||
# 生成邀请链接
|
||||
gitlink-cli member +invite-link --owner Gitlink --repo forgeplus --role developer --apply true
|
||||
```
|
||||
|
||||
### Issue 管理
|
||||
|
||||
```bash
|
||||
# 列出 Issue
|
||||
gitlink-cli issue +list --owner Gitlink --repo forgeplus
|
||||
|
||||
# 创建 Issue
|
||||
gitlink-cli issue +create --owner Gitlink --repo forgeplus -t "Bug: 登录失败" -b "复现步骤..."
|
||||
|
||||
# 创建带元数据的 Issue
|
||||
gitlink-cli issue +create --owner Gitlink --repo forgeplus -t "Bug: 登录失败" --priority-id 3 --tag-ids 4,5 --assigner-ids 7
|
||||
|
||||
# 查看 Issue
|
||||
gitlink-cli issue +view --owner Gitlink --repo forgeplus -i 123
|
||||
|
||||
# 更新 Issue 元数据
|
||||
gitlink-cli issue +update --owner Gitlink --repo forgeplus --number 123 --priority-id 4 --branch bugfix/login --due-date 2026-06-15
|
||||
|
||||
# 关闭 Issue
|
||||
gitlink-cli issue +close --owner Gitlink --repo forgeplus -i 123
|
||||
|
||||
# 预览批量关闭,不修改数据
|
||||
gitlink-cli issue +batch-close --owner Gitlink --repo forgeplus --numbers 123,124 --dry-run
|
||||
|
||||
# 从 CSV 文件批量关闭 Issue
|
||||
gitlink-cli issue +batch-close --owner Gitlink --repo forgeplus --from issues.csv
|
||||
|
||||
# 按 API issue id 预览批量更新元数据
|
||||
# 注意:--ids 是 API issue id,不是网页 URL 中的 Issue 编号。
|
||||
gitlink-cli issue +batch-update --owner Gitlink --repo forgeplus --ids 101,102 --status-id 3 --priority-id 2 --dry-run
|
||||
|
||||
# 危险批量删除必须先 dry-run,真实执行还要显式 --yes
|
||||
gitlink-cli issue +batch-delete --owner Gitlink --repo forgeplus --ids 101,102 --dry-run
|
||||
gitlink-cli issue +batch-delete --owner Gitlink --repo forgeplus --ids 101,102 --yes
|
||||
|
||||
# 添加评论
|
||||
gitlink-cli issue +comment --owner Gitlink --repo forgeplus -i 123 -b "已修复"
|
||||
|
||||
# 列出 Issue 负责人
|
||||
gitlink-cli issue +assigners --owner Gitlink --repo forgeplus
|
||||
|
||||
# 列出 Issue 发布人
|
||||
gitlink-cli issue +authors --owner Gitlink --repo forgeplus
|
||||
|
||||
# 列出 Issue 优先级
|
||||
gitlink-cli issue +priorities --owner Gitlink --repo forgeplus
|
||||
|
||||
# 列出 Issue 标签
|
||||
gitlink-cli issue +tags --owner Gitlink --repo forgeplus --only-name
|
||||
|
||||
# 列出 Issue 状态
|
||||
gitlink-cli issue +statuses --owner Gitlink --repo forgeplus
|
||||
```
|
||||
|
||||
`issue +view`、`issue +update`、`issue +close` 和 `issue +comment` 推荐使用
|
||||
`--number` / `-n` 传网页 URL 中的 Issue 编号。`--id` / `-i` 是同一网页 Issue
|
||||
编号的兼容别名,不是数据库内部 ID。
|
||||
|
||||
### 标签管理
|
||||
|
||||
```bash
|
||||
# 列出 Issue 标签
|
||||
gitlink-cli label +list --owner Gitlink --repo forgeplus
|
||||
|
||||
# 按关键词筛选标签
|
||||
gitlink-cli label +list --owner Gitlink --repo forgeplus -k bug
|
||||
|
||||
# 创建标签(颜色默认 #1E90FF)
|
||||
gitlink-cli label +create --owner Gitlink --repo forgeplus -n bug -d "功能缺陷" -c "#FF0000"
|
||||
|
||||
# 更新标签(未指定的字段会被保留)
|
||||
gitlink-cli label +update --owner Gitlink --repo forgeplus -i 42 -c "#00FF00"
|
||||
|
||||
# 删除标签
|
||||
gitlink-cli label +delete --owner Gitlink --repo forgeplus -i 42
|
||||
```
|
||||
|
||||
### Pull Request
|
||||
|
||||
```bash
|
||||
# 列出 PR
|
||||
gitlink-cli pr +list --owner Gitlink --repo forgeplus
|
||||
|
||||
# 创建 PR(同仓库分支)
|
||||
gitlink-cli pr +create --owner Gitlink --repo forgeplus -t "feat: 搜索功能" --head feature/search --base master
|
||||
|
||||
# 创建 PR(从 Fork 仓库)
|
||||
gitlink-cli pr +create --owner Gitlink --repo forgeplus -t "feat: 新功能" --head your_username/forgeplus:feature/my-feature --base master
|
||||
|
||||
# 查看 PR
|
||||
gitlink-cli pr +view --owner Gitlink --repo forgeplus -i 42
|
||||
|
||||
# 合并 PR
|
||||
gitlink-cli pr +merge --owner Gitlink --repo forgeplus -i 42
|
||||
|
||||
# 重开已关闭的 PR
|
||||
gitlink-cli pr +reopen --owner Gitlink --repo forgeplus -i 42
|
||||
|
||||
# 查看 PR 变更文件
|
||||
gitlink-cli pr +files --owner Gitlink --repo forgeplus -i 42
|
||||
|
||||
# 查看 PR patchset/version 列表
|
||||
gitlink-cli pr +versions --owner Gitlink --repo forgeplus -i 42
|
||||
|
||||
# 查看指定 patchset/version diff
|
||||
gitlink-cli pr +version-diff --owner Gitlink --repo forgeplus -i 42 --version-id 16040
|
||||
|
||||
# 查看 PR 审查记录
|
||||
gitlink-cli pr +reviews --owner Gitlink --repo forgeplus -i 42
|
||||
|
||||
# 创建 PR 审查(支持 dry-run 预览)
|
||||
gitlink-cli pr +review --owner Gitlink --repo forgeplus -i 42 --status approved -c "LGTM" --dry-run
|
||||
gitlink-cli pr +review --owner Gitlink --repo forgeplus -i 42 --status approved -c "LGTM"
|
||||
```
|
||||
|
||||
### 发布管理
|
||||
|
||||
```bash
|
||||
# 列出 Release
|
||||
gitlink-cli release +list --owner Gitlink --repo forgeplus
|
||||
|
||||
# 创建 Release,可附带附件 ID
|
||||
gitlink-cli release +create --owner Gitlink --repo forgeplus -t v1.0.0 -n "v1.0.0 正式版" -b "更新内容..." --attachment-ids 12,34
|
||||
|
||||
# 查看 Release
|
||||
gitlink-cli release +view --owner Gitlink --repo forgeplus -i <version_id>
|
||||
|
||||
# 获取编辑数据并保留未传字段更新
|
||||
gitlink-cli release +edit --owner Gitlink --repo forgeplus -i <version_id>
|
||||
gitlink-cli release +update --owner Gitlink --repo forgeplus -i <version_id> -b "更新后的内容" --dry-run
|
||||
|
||||
# 删除前先预览请求
|
||||
gitlink-cli release +delete --owner Gitlink --repo forgeplus -i <version_id> --dry-run
|
||||
```
|
||||
|
||||
### 流水线管理
|
||||
|
||||
```bash
|
||||
# 列出平台流水线
|
||||
gitlink-cli pipeline +list --owner-id 123 --page 1 --limit 20
|
||||
|
||||
# 列出仓库流水线运行记录
|
||||
gitlink-cli pipeline +runs --owner Gitlink --repo forgeplus --ref master --workflow build.yml
|
||||
|
||||
# 运行流水线工作流,先用 dry-run 预览请求
|
||||
gitlink-cli pipeline +run --owner Gitlink --repo forgeplus --ref master --workflow build.yml --dry-run
|
||||
|
||||
# 查看流水线详情、日志和运行结果
|
||||
gitlink-cli pipeline +view --owner Gitlink --repo forgeplus --id 7
|
||||
gitlink-cli pipeline +logs --owner Gitlink --repo forgeplus --run-id 99 --id 7 --index 43
|
||||
gitlink-cli pipeline +results --owner Gitlink --repo forgeplus --run-id 99
|
||||
|
||||
# 启停或删除流水线工作流,写入/删除前先预览
|
||||
gitlink-cli pipeline +disable --owner Gitlink --repo forgeplus --id 7 --workflow build.yml --dry-run
|
||||
gitlink-cli pipeline +delete --owner Gitlink --repo forgeplus --id 7 --dry-run
|
||||
```
|
||||
|
||||
### 忽略文件模板
|
||||
|
||||
```bash
|
||||
# 列出所有可用的 .gitignore 模板
|
||||
gitlink-cli ignore +list
|
||||
|
||||
# 按名称筛选模板
|
||||
gitlink-cli ignore +list --name Go
|
||||
```
|
||||
|
||||
### 搜索
|
||||
|
||||
```bash
|
||||
# 搜索仓库
|
||||
gitlink-cli search +repos -k "machine learning"
|
||||
|
||||
# 搜索用户
|
||||
gitlink-cli search +users -k "zhangsan"
|
||||
```
|
||||
|
||||
### 用户画像
|
||||
|
||||
`profile` 暴露 GitLink 原生的用户画像统计(开发能力、角色定位、专业定位、近期活动、贡献热力图)。
|
||||
省略 `--user` 时默认使用当前认证用户。
|
||||
|
||||
```bash
|
||||
# 开发能力评分 + 语言分布
|
||||
gitlink-cli profile +ability --user zhangsan
|
||||
|
||||
# 角色定位 / 专业(学科)定位
|
||||
gitlink-cli profile +role --user zhangsan
|
||||
gitlink-cli profile +major --user zhangsan
|
||||
|
||||
# 指定时间范围的开发能力(Unix 时间戳)
|
||||
gitlink-cli profile +ability --user zhangsan --start-time 1704067200 --end-time 1735689600
|
||||
|
||||
# 当前用户的近期活动(每日 疑修 / 合并请求 / 提交)
|
||||
gitlink-cli profile +activity
|
||||
|
||||
# 指定年份的贡献热力图
|
||||
gitlink-cli profile +contribution --user zhangsan --year 2025
|
||||
```
|
||||
|
||||
### 数据集
|
||||
|
||||
`dataset` 管理并查询 GitLink 科研数据集(标题、描述、论文内容、许可证、所属项目)。
|
||||
|
||||
```bash
|
||||
# 按数字项目 ID 列出一个或多个项目的数据集
|
||||
gitlink-cli dataset +list --ids 5988
|
||||
|
||||
# 查看仓库的数据集及其附件
|
||||
gitlink-cli dataset +view --owner Gitlink --repo forgeplus
|
||||
|
||||
# 创建 / 更新仓库数据集(先用 --dry-run 预览)
|
||||
gitlink-cli dataset +create --owner me --repo proj -t "我的数据集" -d "..." --license-id 359 --dry-run
|
||||
gitlink-cli dataset +update --owner me --repo proj -t "我的数据集" -d "更新"
|
||||
|
||||
# 删除数据集附件(破坏性:先预览,再用 --yes 确认)
|
||||
gitlink-cli dataset +delete-attachment --owner me --repo proj --uuid <uuid> --dry-run
|
||||
gitlink-cli dataset +delete-attachment --owner me --repo proj --uuid <uuid> --yes
|
||||
```
|
||||
|
||||
> 注意:`dataset +list`(平台数据集查询)已在生产 gitlink.org.cn 验证可用。按仓库的 `+view`/`+create`/`+update` 遵循已发布的 OpenAPI 契约,但生产环境尚未部署(当前返回 404),待平台上线后即可生效。
|
||||
### Raw API
|
||||
|
||||
Shortcuts 未覆盖的接口可通过 Raw API 直接调用:
|
||||
|
||||
```bash
|
||||
# GET 请求
|
||||
gitlink-cli api GET /users/me
|
||||
|
||||
# POST 请求
|
||||
gitlink-cli api POST /Gitlink/forgeplus/issues --body '{"subject":"test","description":"..."}'
|
||||
|
||||
# 从文件读取 JSON body
|
||||
gitlink-cli api POST /Gitlink/forgeplus/issues --body-file issue.json
|
||||
|
||||
# 从 stdin 读取 JSON body
|
||||
Get-Content issue.json | gitlink-cli api POST /Gitlink/forgeplus/issues --body-stdin
|
||||
|
||||
# 带查询参数
|
||||
gitlink-cli api GET /Gitlink/forgeplus/commits --query 'page=1&limit=5'
|
||||
```
|
||||
|
||||
## 全局参数
|
||||
|
||||
| 参数 | 说明 | 示例 |
|
||||
|------|------|------|
|
||||
| `--owner` | 仓库所有者 | `--owner Gitlink` |
|
||||
| `--repo` | 仓库名称 | `--repo forgeplus` |
|
||||
| `--format` | 输出格式(json/table/yaml) | `--format json` |
|
||||
| `--debug` | 启用调试输出 | `--debug` |
|
||||
|
||||
**自动上下文解析**:在 git 仓库目录下,`--owner` 和 `--repo` 会自动从 `git remote origin` 解析。
|
||||
|
||||
## 分支约定
|
||||
|
||||
gitlink-cli 支持 GitHub 和 GitLink 的代码双向同步:
|
||||
|
||||
| 平台 | 主分支 |
|
||||
|------|--------|
|
||||
| GitHub | `main` |
|
||||
| GitLink | `master` |
|
||||
|
||||
**本地 push 到 GitLink**:
|
||||
|
||||
```bash
|
||||
# 方式 1:使用 git 命令
|
||||
git push gitlink main:master
|
||||
|
||||
# 方式 2:配置 git remote
|
||||
git config remote.gitlink.push refs/heads/main:refs/heads/master
|
||||
git push gitlink
|
||||
```
|
||||
|
||||
## AI Agent Skills
|
||||
|
||||
`skills/` 目录含 **37 个 Skill**(官方 11 + 新增 26),分类:核心基础 / 命令封装 / 智能化场景(代码审查·分拣·Release·健康度·合规·新人引导)/ 端到端编排 / 科研辅助。兼容 Claude Code / OpenClaw / Cursor。完整说明见 [skills/README.md](./skills/README.md)。
|
||||
`skills/` 目录包含 Claude Code Agent Skill 文件,支持 AI 自动化操作 GitLink 平台。
|
||||
|
||||
---
|
||||
详见 [skills/README.md](./skills/README.md)
|
||||
|
||||
## 科研智能服务 gitlink-web
|
||||
|
||||
基于 Flask + DeepSeek API + gitlink-cli 的 Web 服务,14 个 Skill(核心 9 + 科研实验室 5),输入研究主题或仓库 → 自动调 gitlink-cli 拉数据 → DeepSeek 生成结构化报告。
|
||||
|
||||
🌐 在线:<http://121.41.212.97/skills/> | 代码:[gitlink-web/](./gitlink-web/README.md) | 验证报告:[作品提交/子任务四](../../作品提交/子任务四/科研场景验证报告.md)
|
||||
|
||||
---
|
||||
|
||||
## 成果落地(竞赛核心)
|
||||
|
||||
向真正上游 `gitlink/gitlink-cli` 提交细粒度 PR(每功能一个特性分支 = 功能代码+测试+文档+变更说明),共 13 个。详见 [docs/UPSTREAM_PRS.md](./docs/UPSTREAM_PRS.md):
|
||||
|
||||
- **子任务一 CLI(5)**:export / pm / notification / alias·browse·status / capability
|
||||
- **子任务二 Skill(2)**:release-notes / compliance-check
|
||||
- **子任务三 编排 Skill(5)**:community-ops / pr-gate / project-bootstrap / multi-repo-ops / contributor-growth
|
||||
- **子任务四 科研服务(1)**:gitlink-web
|
||||
|
||||
---
|
||||
| Skill | 说明 |
|
||||
|-------|------|
|
||||
| `gitlink-shared` | 认证、全局参数、安全规则、API 注意事项 |
|
||||
| `gitlink-repo` | 仓库操作(创建、查看、删除、Fork、洞察数据等) |
|
||||
| `gitlink-issue` | Issue 操作(创建、更新、关闭、批量更新/删除、评论等) |
|
||||
| `gitlink-pr` | Pull Request 操作(创建、合并、Review 等) |
|
||||
| `gitlink-member` | 仓库成员与邀请链接管理 |
|
||||
| `gitlink-release` | 发布管理(创建、编辑、更新、查看、删除等) |
|
||||
| `gitlink-org` | 组织管理(成员、团队等) |
|
||||
| `gitlink-ci` | CI/CD 操作(构建、日志等) |
|
||||
| `gitlink-pipeline` | 流水线工作流操作(运行、日志、启停、删除等) |
|
||||
| `gitlink-search` | 搜索功能(仓库、用户等) |
|
||||
| `gitlink-user` | 用户管理(个人信息等) |
|
||||
| `gitlink-pm` | 项目管理(Sprint、看板、周报等) |
|
||||
| `gitlink-workflow` | AI 自动化工作流(Issue 分类、PR Review、Release Notes 等) |
|
||||
|
||||
## 项目结构
|
||||
|
||||
```
|
||||
gitlink-cli/
|
||||
├── cmd/ # Cobra 命令(root/auth/api/config/alias/browse/status/show/demo/...)
|
||||
├── internal/ # 内部包(auth/client/config/context/output/capability/i18n/demo)
|
||||
├── shortcuts/ # 21 个命令组(repo/issue/pr/...pm/wiki/export/notification/capability/compare/workflow/...)
|
||||
├── skills/ # 37 个 AI Agent Skill(11 官方 + 26 新增)
|
||||
├── gitlink-web/ # 子任务四 科研智能服务(Flask + DeepSeek)
|
||||
├── examples/workflows/ # 子任务三 端到端工作流(community-ops-automation)
|
||||
├── .devops/ # 建木 DevOps 流水线(ci/build/gitlink-web)
|
||||
├── doc/ # 设计文档 + commands/ 命令文档 + changes/ 变更说明
|
||||
├── docs/ # 报告 + 复现指南 + 上游 PR 清单
|
||||
├── REPRODUCE.md # 评委复现手册
|
||||
└── main.go / Makefile / go.mod
|
||||
├── cmd/ # Cobra 命令定义
|
||||
│ ├── root.go # 根命令 + 全局 flags
|
||||
│ ├── auth/ # 认证命令
|
||||
│ ├── api/ # Raw API 命令
|
||||
│ ├── config/ # 配置命令
|
||||
│ └── cmdutil/ # 全局工具
|
||||
├── internal/ # 内部包
|
||||
│ ├── auth/ # 登录、Token 存储、Transport
|
||||
│ ├── client/ # HTTP 客户端 + 分页
|
||||
│ ├── config/ # 配置文件管理
|
||||
│ ├── context/ # git remote 解析
|
||||
│ └── output/ # Envelope + Formatter
|
||||
├── shortcuts/ # Shortcut 实现
|
||||
│ ├── common/ # 框架(types, runner)
|
||||
│ ├── repo/ # 仓库 shortcuts
|
||||
│ ├── issue/ # Issue shortcuts
|
||||
│ ├── pr/ # PR shortcuts
|
||||
│ ├── member/ # 仓库成员 shortcuts
|
||||
│ ├── branch/ # 分支 shortcuts
|
||||
│ ├── release/ # Release shortcuts
|
||||
│ ├── org/ # 组织 shortcuts
|
||||
│ ├── ci/ # CI shortcuts
|
||||
│ ├── pipeline/ # Pipeline shortcuts
|
||||
│ ├── search/ # 搜索 shortcuts
|
||||
│ ├── user/ # 用户 shortcuts
|
||||
│ └── register.go # 注册入口
|
||||
├── skills/ # AI Agent Skills
|
||||
│ ├── README.md # Skills 使用指南
|
||||
│ ├── gitlink-shared/ # 共享规则
|
||||
│ ├── gitlink-repo/ # 仓库 Skill
|
||||
│ ├── gitlink-issue/ # Issue Skill
|
||||
│ ├── gitlink-pr/ # PR Skill
|
||||
│ ├── gitlink-pm/ # 项目管理 Skill
|
||||
│ └── ...
|
||||
├── doc/ # 设计文档
|
||||
│ ├── Design.md
|
||||
│ ├── CODE_SYNC_STRATEGY_FINAL.md
|
||||
│ └── ...
|
||||
├── main.go
|
||||
├── Makefile
|
||||
├── go.mod
|
||||
└── README.md
|
||||
```
|
||||
|
||||
---
|
||||
## 文档
|
||||
|
||||
## 文档导航
|
||||
- [Skills 使用指南](./skills/README.md) — AI Agent Skills 详细说明
|
||||
- [设计文档](doc/design.md) — 架构设计和开发计划
|
||||
|
||||
- 📖 [Skills 指南](./skills/README.md) · 🔧 [设计文档](./doc/design.md) · 📚 [命令帮助](./doc/commands/) · 📝 [变更说明](./doc/changes/)
|
||||
- 🔄 [端到端工作流](./examples/workflows/community-ops-automation/) · 🔬 [科研服务](./gitlink-web/README.md)
|
||||
- 🎓 [课程报告](./docs/reports/README.md)(软件分析建模 / 新需求构思 / 变更影响分析)
|
||||
- ✅ [复现指南](./REPRODUCE.md) · 🏆 [上游 PR 清单](./docs/UPSTREAM_PRS.md)
|
||||
## 常见问题
|
||||
|
||||
---
|
||||
### Q: 如何在脚本中使用 gitlink-cli?
|
||||
|
||||
## FAQ
|
||||
使用 `GITLINK_TOKEN` 环境变量 + `--format json` 获取结构化输出:
|
||||
|
||||
**Q:脚本/CI 中怎么用?** 设 `GITLINK_TOKEN` 环境变量 + `--format json`:
|
||||
```bash
|
||||
export GITLINK_TOKEN="your-token"
|
||||
export GITLINK_TOKEN="your-private-token"
|
||||
gitlink-cli repo +list --format json | jq '.data.projects[] | .name'
|
||||
```
|
||||
|
||||
**Q:owner/repo 自动解析?** 在 git 仓库内运行,自动从 `git remote origin` 解析。
|
||||
### Q: 如何自动解析 owner/repo?
|
||||
|
||||
**Q:某命令在 `--help` 标 ⚠?** 该模块后端暂不支持,`gitlink-cli capability +check` 查看详情。
|
||||
在 git 仓库目录下运行命令,CLI 会自动从 `git remote origin` 解析:
|
||||
|
||||
**Q:完整 API 参考?** 见 [skills/gitlink-shared/REFERENCE.md](./skills/gitlink-shared/REFERENCE.md) 与 [GitLink OpenAPI](https://apifox.com/apidoc/shared-da30afb0-9d2e-429b-a4bc-a83209e06021)。
|
||||
```bash
|
||||
cd ~/my-gitlink-project
|
||||
gitlink-cli issue +list # 自动使用当前仓库
|
||||
```
|
||||
|
||||
---
|
||||
### Q: Token 过期了怎么办?
|
||||
|
||||
## 相关项目
|
||||
重新登录:
|
||||
|
||||
- 上游主仓库:[gitlink/gitlink-cli](https://www.gitlink.org.cn/gitlink/gitlink-cli)
|
||||
- 大赛主页:[www.gitlink.org.cn/competitions/2026](https://www.gitlink.org.cn/competitions/2026)
|
||||
```bash
|
||||
# 用户名密码登录
|
||||
gitlink-cli auth login
|
||||
|
||||
## License
|
||||
# 或使用私人令牌(在 GitLink 网页端 个人设置 → 私人令牌 中生成)
|
||||
gitlink-cli auth login --token
|
||||
```
|
||||
|
||||
[MulanPSL-2.0](https://license.coscl.org.cn/mulanpsl2)
|
||||
### Q: 如何在 CI/CD 或非交互环境(Trae 沙箱等)中使用?
|
||||
|
||||
设置 `GITLINK_TOKEN` 环境变量即可,无需 `auth login`:
|
||||
|
||||
```bash
|
||||
export GITLINK_TOKEN="your-private-token"
|
||||
gitlink-cli repo +list # 直接可用
|
||||
gitlink-cli auth status # 显示 "✓ Logged in via GITLINK_TOKEN environment variable"
|
||||
```
|
||||
|
||||
Token 优先级:`GITLINK_TOKEN` 环境变量 > keyring/文件存储的 token。不设置环境变量时完全兼容原有交互式登录。
|
||||
|
||||
### Q: npm 安装成功但 `gitlink-cli` 提示缺少二进制怎么办?
|
||||
|
||||
先尝试重新安装:
|
||||
|
||||
```bash
|
||||
npm install -g @gitlink-ai/cli
|
||||
```
|
||||
|
||||
如果仍然失败,请检查 Release 页面是否包含当前平台的资产,例如 Windows x64 对应 `gitlink-cli_<version>_windows_amd64.zip`。也可以从 Release 页面手动下载二进制,或使用 `go install .` 从源码构建。
|
||||
|
||||
### Q: Windows 上凭证存储在哪里?
|
||||
|
||||
gitlink-cli 使用 Windows Credential Manager 安全存储 Token。如果 Credential Manager 不可用,会自动降级到文件存储(`~/.config/gitlink-cli/credentials`)。
|
||||
|
||||
### Q: 如何查看完整的 API 参考?
|
||||
|
||||
查看 [skills/gitlink-shared/references/api-reference.md](./skills/gitlink-shared/references/api-reference.md)
|
||||
|
||||
## 许可证
|
||||
|
||||
[MulanPSL-2.0](https://license.coscl.org.cn/MulanPSL2)
|
||||
|
|
|
|||
139
REPRODUCE.md
139
REPRODUCE.md
|
|
@ -1,139 +0,0 @@
|
|||
# 复现指南(REPRODUCE)
|
||||
|
||||
> 面向评委的"照此即可复现全部成果"手册。四个部分相互独立,可按需选做。
|
||||
> 全程约 15–30 分钟(不含等待 Review)。
|
||||
|
||||
## 0. 前置准备
|
||||
|
||||
- GitLink 账号(<https://www.gitlink.org.cn> 注册)
|
||||
- 任一终端:macOS / Linux / Windows
|
||||
- 二选一:Node.js 14+(npm 装 CLI)或 Go 1.26+(源码构建)
|
||||
- GitLink Token:登录后在「个人设置 → 访问令牌」生成,或用 `auth login` 交互登录
|
||||
|
||||
```bash
|
||||
git clone https://www.gitlink.org.cn/jiangtx/gitlink-cli.git
|
||||
cd gitlink-cli
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 1. 复现 CLI 命令(子任务一)
|
||||
|
||||
### 1.1 安装
|
||||
```bash
|
||||
# 方式 A:npm
|
||||
npm install -g @gitlink-ai/cli
|
||||
|
||||
# 方式 B:源码(需 Go 1.26+)
|
||||
go build -o gitlink-cli .
|
||||
```
|
||||
|
||||
### 1.2 认证
|
||||
```bash
|
||||
gitlink-cli auth login # 交互登录;CI 环境用 export GITLINK_TOKEN=xxx
|
||||
gitlink-cli auth status # 确认登录
|
||||
```
|
||||
|
||||
### 1.3 验证新命令(任选)
|
||||
```bash
|
||||
gitlink-cli pm --help # 项目管理(6 命令)
|
||||
gitlink-cli wiki --help # Wiki(5 命令)
|
||||
gitlink-cli export --help # 数据导出(3 命令)
|
||||
gitlink-cli workflow --help # 工作流分析(4 命令)
|
||||
gitlink-cli capability +check # 后端能力探测
|
||||
|
||||
# 跑一条真实命令
|
||||
gitlink-cli export +contributors --owner Gitlink --repo gitlink-cli --format json
|
||||
gitlink-cli workflow +triage --owner Gitlink --repo gitlink-cli --limit 10
|
||||
```
|
||||
|
||||
### 1.4 跑单元测试
|
||||
```bash
|
||||
go test ./... -race # 需 Go 环境;CI 已自动执行
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. 复现 Skill(子任务二)
|
||||
|
||||
### 2.1 加载 Skills 到 Agent 平台
|
||||
把 `skills/` 目录拷贝到 Agent 的 skills 路径,或直接在仓库内调用:
|
||||
|
||||
**Claude Code**:`skills/` 放到 `~/.claude/skills/` 或项目 `.claude/skills/`,重启会话。
|
||||
|
||||
### 2.2 验证已验证 Skill(6 个有运行记录)
|
||||
```bash
|
||||
# 例:技术调研 Skill
|
||||
# 在 Agent 对话:「调研 GitLink 上'大模型'相关的活跃项目」
|
||||
# Agent 会按 skills/gitlink-research-tracker/SKILL.md 执行
|
||||
|
||||
# 或看现成验证记录
|
||||
cat skills/gitlink-research-tracker/EXAMPLES.md # token/耗时/输出
|
||||
cat skills/gitlink-contributor-insight/EXAMPLES.md
|
||||
cat skills/gitlink-ci-health/EXAMPLES.md
|
||||
cat skills/gitlink-issue-triage/EXAMPLES.md
|
||||
cat skills/gitlink-onboarding/EXAMPLES.md
|
||||
cat skills/gitlink-notification-digest/EXAMPLES.md
|
||||
```
|
||||
|
||||
> 待验证 Skill(20 个)含完整 SKILL.md,按同样方式加载即可运行。
|
||||
|
||||
---
|
||||
|
||||
## 3. 复现端到端工作流(子任务三)
|
||||
|
||||
```bash
|
||||
cd examples/workflows/community-ops-automation
|
||||
|
||||
# Windows
|
||||
powershell -File scripts/run_demo.ps1 -Owner Gitlink -Repo gitlink-cli
|
||||
|
||||
# Linux/macOS(需 npm 可用,脚本通过 npm exec 调 @gitlink-ai/cli)
|
||||
node scripts/gitlink_workflow.js --owner Gitlink --repo gitlink-cli
|
||||
```
|
||||
|
||||
输出:`outputs/{owner}_{repo}_{timestamp}_report.md`(社区运营报告)+ `_release_notes.md`。
|
||||
|
||||
验证证据见 `docs/verification.md`(已在 `Gitlink/gitlink-cli`、`puygob236/gitlink-cli` 两真项目跑过,含计数与时间戳)。
|
||||
|
||||
---
|
||||
|
||||
## 4. 复现科研服务(子任务四)
|
||||
|
||||
### 4.1 在线体验(无需自部署)
|
||||
直接访问:<http://121.41.212.97/skills/>
|
||||
- 技术调研:输入「大模型」→ 生成调研报告
|
||||
- 科研实验室:热点追踪 / 项目洞悉 / 合规检查 / 协作匹配 / 进度跟踪
|
||||
|
||||
### 4.2 自部署
|
||||
```bash
|
||||
cd gitlink-web
|
||||
pip install -r requirements.txt
|
||||
export API_KEY="你的 DeepSeek Key"
|
||||
export API_BASE="https://api.deepseek.com/v1"
|
||||
export API_MODEL="deepseek-chat"
|
||||
python app.py --port=5000
|
||||
# 访问 http://localhost:5000/
|
||||
```
|
||||
|
||||
或一键部署到服务器:`python deploy.py`(paramiko SSH,详见 [gitlink-web/README.md](gitlink-web/README.md))。
|
||||
|
||||
---
|
||||
|
||||
## 5. 常见问题
|
||||
|
||||
| 问题 | 解决 |
|
||||
|---|---|
|
||||
| `command not found: go` | 用 npm 方式装 CLI,或装 Go 1.26+ |
|
||||
| 认证失败 / 401 | `gitlink-cli auth login` 重新登录,或检查 `GITLINK_TOKEN` |
|
||||
| 命令标 ⚠ | 该模块后端暂不支持,`gitlink-cli capability +check` 查看详情 |
|
||||
| 工作流只支持 Windows | 用第 3 节的 node 命令在任意平台跑 |
|
||||
| 科研服务报 API_KEY 未设置 | 配 `API_KEY` 环境变量(DeepSeek 或兼容 OpenAI 的接口) |
|
||||
|
||||
## 6. 预期产物清单(复现成功后应得到)
|
||||
|
||||
- [ ] CLI 可运行,`--help` 列出 21 个命令组
|
||||
- [ ] `go test ./...` 全绿(或 CI 绿)
|
||||
- [ ] 至少 1 个 Skill 在 Agent 平台产出结果
|
||||
- [ ] 工作流生成 1 份 `_report.md`
|
||||
- [ ] 科研服务页面可访问并生成 1 份调研报告
|
||||
|
|
@ -1,158 +0,0 @@
|
|||
package alias
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"sort"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"gopkg.in/yaml.v3"
|
||||
|
||||
"github.com/gitlink-org/gitlink-cli/cmd/cmdutil"
|
||||
"github.com/gitlink-org/gitlink-cli/internal/config"
|
||||
"github.com/gitlink-org/gitlink-cli/internal/output"
|
||||
)
|
||||
|
||||
// AliasConfig represents the aliases section of the CLI config.
|
||||
type AliasConfig struct {
|
||||
Aliases map[string]string `yaml:"aliases,omitempty"`
|
||||
}
|
||||
|
||||
// NewAliasCmd creates the alias command with subcommands.
|
||||
func NewAliasCmd() *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "alias",
|
||||
Short: "管理命令别名(把长命令变短)",
|
||||
Long: `管理 gitlink-cli 的命令别名。
|
||||
|
||||
别名允许你为常用命令创建简短的名称,例如:
|
||||
gitlink-cli alias +set rl "repo +list"
|
||||
之后可以使用: gitlink-cli rl
|
||||
|
||||
别名存储在 ~/.config/gitlink-cli/aliases.yaml 中。`,
|
||||
Example: ` gitlink-cli alias +list
|
||||
gitlink-cli alias +set rl "repo +list"
|
||||
gitlink-cli alias +set ri "repo +info --owner Gitlink --repo gitlink-cli"
|
||||
gitlink-cli alias +delete rl`,
|
||||
}
|
||||
|
||||
cmd.AddCommand(
|
||||
&cobra.Command{
|
||||
Use: "+list",
|
||||
Short: "列出所有已定义的别名",
|
||||
Long: "列出所有已定义的命令别名。如果没有任何别名,会给出创建提示。",
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
aliases, _ := loadAliases()
|
||||
// Structured formats (json/yaml/table) route through output.Print
|
||||
// so alias +list integrates with scripts and AI Agents.
|
||||
if cmdutil.Format == "json" || cmdutil.Format == "yaml" || cmdutil.Format == "table" {
|
||||
rows := make([]map[string]string, 0, len(aliases))
|
||||
names := make([]string, 0, len(aliases))
|
||||
for k := range aliases {
|
||||
names = append(names, k)
|
||||
}
|
||||
sort.Strings(names)
|
||||
for _, k := range names {
|
||||
rows = append(rows, map[string]string{"name": k, "command": aliases[k]})
|
||||
}
|
||||
return output.Print(output.SuccessEnvelope(rows, nil), cmdutil.Format)
|
||||
}
|
||||
if len(aliases) == 0 {
|
||||
fmt.Println("(未定义任何别名)")
|
||||
fmt.Println("使用 alias +set <名称> <命令> 来创建别名")
|
||||
return nil
|
||||
}
|
||||
names := make([]string, 0, len(aliases))
|
||||
for k := range aliases {
|
||||
names = append(names, k)
|
||||
}
|
||||
sort.Strings(names)
|
||||
for _, k := range names {
|
||||
fmt.Printf(" %-15s → %s\n", k, aliases[k])
|
||||
}
|
||||
return nil
|
||||
},
|
||||
},
|
||||
&cobra.Command{
|
||||
Use: "+set <name> <command>",
|
||||
Short: "设置别名",
|
||||
Long: "为一条命令设置别名。如果别名已存在,会被覆盖。",
|
||||
Args: cobra.ExactArgs(2),
|
||||
Example: ` gitlink-cli alias +set rl "repo +list"
|
||||
gitlink-cli alias +set ri "repo +info"`,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
aliases, _ := loadAliases()
|
||||
aliases[args[0]] = args[1]
|
||||
if err := saveAliases(aliases); err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Printf("别名已设置: %s → %s\n", args[0], args[1])
|
||||
return nil
|
||||
},
|
||||
},
|
||||
&cobra.Command{
|
||||
Use: "+delete <name>",
|
||||
Short: "删除别名",
|
||||
Long: "删除一个已定义的命令别名。",
|
||||
Args: cobra.ExactArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
aliases, _ := loadAliases()
|
||||
if _, ok := aliases[args[0]]; !ok {
|
||||
return fmt.Errorf("别名 %s 不存在", args[0])
|
||||
}
|
||||
delete(aliases, args[0])
|
||||
if err := saveAliases(aliases); err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Printf("别名已删除: %s\n", args[0])
|
||||
return nil
|
||||
},
|
||||
},
|
||||
&cobra.Command{
|
||||
Use: "+expand <name>",
|
||||
Short: "展开别名查看原命令",
|
||||
Long: "查看一个别名对应的原始命令。如果别名不存在则报错。",
|
||||
Args: cobra.ExactArgs(1),
|
||||
Example: ` gitlink-cli alias +expand rl
|
||||
输出: rl → repo +list`,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
aliases, _ := loadAliases()
|
||||
expanded, ok := aliases[args[0]]
|
||||
if !ok {
|
||||
return fmt.Errorf("别名 %s 不存在", args[0])
|
||||
}
|
||||
fmt.Printf("%s → %s\n", args[0], expanded)
|
||||
return nil
|
||||
},
|
||||
},
|
||||
)
|
||||
return cmd
|
||||
}
|
||||
|
||||
func aliasesPath() string {
|
||||
return config.ConfigDir() + "/aliases.yaml"
|
||||
}
|
||||
|
||||
func loadAliases() (map[string]string, error) {
|
||||
data, err := os.ReadFile(aliasesPath())
|
||||
if err != nil {
|
||||
return make(map[string]string), nil
|
||||
}
|
||||
var ac AliasConfig
|
||||
if err := yaml.Unmarshal(data, &ac); err != nil {
|
||||
return make(map[string]string), nil
|
||||
}
|
||||
if ac.Aliases == nil {
|
||||
ac.Aliases = make(map[string]string)
|
||||
}
|
||||
return ac.Aliases, nil
|
||||
}
|
||||
|
||||
func saveAliases(a map[string]string) error {
|
||||
data, err := yaml.Marshal(AliasConfig{Aliases: a})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
os.MkdirAll(config.ConfigDir(), 0700)
|
||||
return os.WriteFile(aliasesPath(), data, 0600)
|
||||
}
|
||||
|
|
@ -1,224 +0,0 @@
|
|||
package alias
|
||||
|
||||
import (
|
||||
"io"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/gitlink-org/gitlink-cli/cmd/cmdutil"
|
||||
)
|
||||
|
||||
func TestLoadAliasesEmpty(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
t.Setenv("GITLINK_CONFIG_DIR", tmpDir)
|
||||
|
||||
aliases, err := loadAliases()
|
||||
if err != nil {
|
||||
t.Fatalf("loadAliases failed: %v", err)
|
||||
}
|
||||
if len(aliases) != 0 {
|
||||
t.Fatalf("expected empty aliases, got %d", len(aliases))
|
||||
}
|
||||
}
|
||||
|
||||
func TestSaveAndLoadAliases(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
t.Setenv("GITLINK_CONFIG_DIR", tmpDir)
|
||||
|
||||
original := map[string]string{
|
||||
"rl": "repo +list",
|
||||
"ri": "repo +info",
|
||||
}
|
||||
if err := saveAliases(original); err != nil {
|
||||
t.Fatalf("saveAliases failed: %v", err)
|
||||
}
|
||||
|
||||
loaded, err := loadAliases()
|
||||
if err != nil {
|
||||
t.Fatalf("loadAliases failed: %v", err)
|
||||
}
|
||||
if len(loaded) != 2 {
|
||||
t.Fatalf("expected 2 aliases, got %d", len(loaded))
|
||||
}
|
||||
if loaded["rl"] != "repo +list" {
|
||||
t.Errorf("expected rl -> repo +list, got %s", loaded["rl"])
|
||||
}
|
||||
if loaded["ri"] != "repo +info" {
|
||||
t.Errorf("expected ri -> repo +info, got %s", loaded["ri"])
|
||||
}
|
||||
}
|
||||
|
||||
func TestSaveAliasesOverwrite(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
t.Setenv("GITLINK_CONFIG_DIR", tmpDir)
|
||||
|
||||
saveAliases(map[string]string{"rl": "repo +list"})
|
||||
saveAliases(map[string]string{"rl": "repo +list --owner Gitlink"})
|
||||
|
||||
loaded, _ := loadAliases()
|
||||
if loaded["rl"] != "repo +list --owner Gitlink" {
|
||||
t.Errorf("alias should be overwritten, got %s", loaded["rl"])
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadAliasesInvalidYAML(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
t.Setenv("GITLINK_CONFIG_DIR", tmpDir)
|
||||
|
||||
os.WriteFile(tmpDir+"/aliases.yaml", []byte("{{invalid yaml}}"), 0600)
|
||||
|
||||
aliases, err := loadAliases()
|
||||
if err != nil {
|
||||
t.Fatalf("should not error on invalid YAML, got: %v", err)
|
||||
}
|
||||
if len(aliases) != 0 {
|
||||
t.Fatalf("should return empty map on invalid YAML, got %d", len(aliases))
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewAliasCmdStructure(t *testing.T) {
|
||||
cmd := NewAliasCmd()
|
||||
if cmd.Use != "alias" {
|
||||
t.Errorf("expected Use 'alias', got %s", cmd.Use)
|
||||
}
|
||||
if !cmd.HasSubCommands() {
|
||||
t.Error("alias command should have subcommands")
|
||||
}
|
||||
|
||||
subcmds := cmd.Commands()
|
||||
if len(subcmds) != 4 {
|
||||
t.Fatalf("expected 4 subcommands, got %d", len(subcmds))
|
||||
}
|
||||
|
||||
expectedUses := map[string]bool{"+list": false, "+set <name> <command>": false, "+delete <name>": false, "+expand <name>": false}
|
||||
for _, sub := range subcmds {
|
||||
if _, ok := expectedUses[sub.Use]; ok {
|
||||
expectedUses[sub.Use] = true
|
||||
}
|
||||
}
|
||||
for use, found := range expectedUses {
|
||||
if !found {
|
||||
t.Errorf("subcommand %q not found", use)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestAliasSetAndDeleteFlow(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
t.Setenv("GITLINK_CONFIG_DIR", tmpDir)
|
||||
|
||||
// 模拟 +set 操作:直接调用 saveAliases
|
||||
aliases := make(map[string]string)
|
||||
aliases["rl"] = "repo +list"
|
||||
aliases["ri"] = "repo +info"
|
||||
if err := saveAliases(aliases); err != nil {
|
||||
t.Fatalf("saveAliases failed: %v", err)
|
||||
}
|
||||
|
||||
// 验证保存成功
|
||||
loaded, _ := loadAliases()
|
||||
if loaded["rl"] != "repo +list" {
|
||||
t.Fatalf("alias not saved correctly: %v", loaded)
|
||||
}
|
||||
if loaded["ri"] != "repo +info" {
|
||||
t.Fatalf("alias not saved correctly: %v", loaded)
|
||||
}
|
||||
|
||||
// 模拟 +delete 操作:删除别名后保存
|
||||
delete(loaded, "rl")
|
||||
if err := saveAliases(loaded); err != nil {
|
||||
t.Fatalf("saveAliases after delete failed: %v", err)
|
||||
}
|
||||
|
||||
// 验证删除成功
|
||||
final, _ := loadAliases()
|
||||
if _, ok := final["rl"]; ok {
|
||||
t.Fatal("alias 'rl' should have been deleted")
|
||||
}
|
||||
if final["ri"] != "repo +info" {
|
||||
t.Fatal("alias 'ri' should still exist")
|
||||
}
|
||||
}
|
||||
|
||||
func TestAliasDeleteNonExistent(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
t.Setenv("GITLINK_CONFIG_DIR", tmpDir)
|
||||
|
||||
// 空别名列表,删除不存在的别名
|
||||
aliases, _ := loadAliases()
|
||||
if _, ok := aliases["nonexistent"]; ok {
|
||||
t.Fatal("nonexistent alias should not exist")
|
||||
}
|
||||
// 验证逻辑:别名不存在时不应执行删除
|
||||
// 这对应 alias.go 中 if _, ok := aliases[args[0]]; !ok 的检查
|
||||
}
|
||||
|
||||
func TestAliasesFilePath(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
t.Setenv("GITLINK_CONFIG_DIR", tmpDir)
|
||||
|
||||
expected := tmpDir + "/aliases.yaml"
|
||||
got := aliasesPath()
|
||||
if got != expected {
|
||||
t.Errorf("expected path %s, got %s", expected, got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAliasExpandExisting(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
t.Setenv("GITLINK_CONFIG_DIR", tmpDir)
|
||||
|
||||
saveAliases(map[string]string{
|
||||
"rl": "repo +list",
|
||||
"ri": "repo +info",
|
||||
})
|
||||
|
||||
aliases, _ := loadAliases()
|
||||
if expanded, ok := aliases["rl"]; !ok || expanded != "repo +list" {
|
||||
t.Fatalf("expected rl → repo +list, got %s", expanded)
|
||||
}
|
||||
if expanded, ok := aliases["ri"]; !ok || expanded != "repo +info" {
|
||||
t.Fatalf("expected ri → repo +info, got %s", expanded)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAliasExpandNonExistent(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
t.Setenv("GITLINK_CONFIG_DIR", tmpDir)
|
||||
|
||||
aliases, _ := loadAliases()
|
||||
if _, ok := aliases["nonexistent"]; ok {
|
||||
t.Fatal("nonexistent alias should not be found")
|
||||
}
|
||||
}
|
||||
|
||||
func TestAliasListJSONFormat(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
t.Setenv("GITLINK_CONFIG_DIR", tmpDir)
|
||||
if err := saveAliases(map[string]string{"rl": "repo +list", "ri": "repo +info"}); err != nil {
|
||||
t.Fatalf("save: %v", err)
|
||||
}
|
||||
cmdutil.Format = "json"
|
||||
defer func() { cmdutil.Format = "" }()
|
||||
|
||||
old := os.Stdout
|
||||
r, w, _ := os.Pipe()
|
||||
os.Stdout = w
|
||||
root := NewAliasCmd()
|
||||
root.SetArgs([]string{"+list"})
|
||||
execErr := root.Execute()
|
||||
w.Close()
|
||||
os.Stdout = old
|
||||
if execErr != nil {
|
||||
t.Fatalf("execute: %v", execErr)
|
||||
}
|
||||
var buf strings.Builder
|
||||
io.Copy(&buf, r)
|
||||
out := buf.String()
|
||||
for _, want := range []string{`"ok": true`, `"name"`, `"rl"`, `"repo +list"`} {
|
||||
if !strings.Contains(out, want) {
|
||||
t.Errorf("JSON output missing %q: %s", want, out)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -23,14 +23,16 @@ func NewAPICmd(translators ...*i18n.Translator) *cobra.Command {
|
|||
tr = translators[0]
|
||||
}
|
||||
apiCmd := &cobra.Command{
|
||||
Use: "api <METHOD> <PATH>",
|
||||
Use: "api (<METHOD> <PATH> | --batch-file <FILE>)",
|
||||
Short: tr.T("cmd.api.short"),
|
||||
Long: tr.T("cmd.api.long"),
|
||||
Example: ` gitlink-cli api GET /users/me
|
||||
gitlink-cli api GET /projects --query 'page=1&limit=10'
|
||||
gitlink-cli api POST /:owner/:repo/issues --body '{"subject":"Bug","description":"..."}'
|
||||
gitlink-cli api POST /:owner/:repo/issues --body-file issue.json`,
|
||||
Args: cobra.ExactArgs(2),
|
||||
gitlink-cli api POST /:owner/:repo/issues --body-file issue.json
|
||||
gitlink-cli api --batch-file plan.json --dry-run
|
||||
gitlink-cli api --batch-file plan.json --var owner=Gitlink --var repo=gitlink-cli`,
|
||||
Args: validateAPIArgs,
|
||||
RunE: runAPI,
|
||||
}
|
||||
|
||||
|
|
@ -39,11 +41,31 @@ func NewAPICmd(translators ...*i18n.Translator) *cobra.Command {
|
|||
apiCmd.Flags().Bool("body-stdin", false, tr.T("flag.api.body_stdin"))
|
||||
apiCmd.Flags().String("query", "", tr.T("flag.api.query"))
|
||||
apiCmd.Flags().StringSlice("header", nil, tr.T("flag.api.header"))
|
||||
apiCmd.Flags().String("batch-file", "", tr.T("flag.api.batch_file"))
|
||||
apiCmd.Flags().Bool("dry-run", false, tr.T("flag.api.batch_dry_run"))
|
||||
apiCmd.Flags().Bool("continue-on-error", false, tr.T("flag.api.batch_continue_on_error"))
|
||||
apiCmd.Flags().StringArray("var", nil, tr.T("flag.api.batch_var"))
|
||||
|
||||
return apiCmd
|
||||
}
|
||||
|
||||
func validateAPIArgs(c *cobra.Command, args []string) error {
|
||||
batchFile, _ := c.Flags().GetString("batch-file")
|
||||
if batchFile != "" {
|
||||
if len(args) != 0 {
|
||||
return fmt.Errorf("api batch mode does not accept METHOD or PATH arguments")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
return cobra.ExactArgs(2)(c, args)
|
||||
}
|
||||
|
||||
func runAPI(c *cobra.Command, args []string) error {
|
||||
batchFile, _ := c.Flags().GetString("batch-file")
|
||||
if batchFile != "" {
|
||||
return runAPIBatch(c, batchFile)
|
||||
}
|
||||
|
||||
method := strings.ToUpper(args[0])
|
||||
path := args[1]
|
||||
|
||||
|
|
|
|||
|
|
@ -34,7 +34,7 @@ func TestResolveFormat(t *testing.T) {
|
|||
|
||||
func TestNewAPICmd(t *testing.T) {
|
||||
cmd := NewAPICmd()
|
||||
if cmd.Use != "api <METHOD> <PATH>" {
|
||||
if cmd.Use != "api (<METHOD> <PATH> | --batch-file <FILE>)" {
|
||||
t.Fatalf("Use = %q", cmd.Use)
|
||||
}
|
||||
if cmd.Short == "" {
|
||||
|
|
@ -42,7 +42,7 @@ func TestNewAPICmd(t *testing.T) {
|
|||
}
|
||||
|
||||
// Verify flags exist
|
||||
flags := []string{"body", "query", "header"}
|
||||
flags := []string{"body", "query", "header", "batch-file", "dry-run", "continue-on-error", "var"}
|
||||
for _, f := range flags {
|
||||
if cmd.Flags().Lookup(f) == nil {
|
||||
t.Fatalf("flag %q not found", f)
|
||||
|
|
@ -201,3 +201,204 @@ func TestRunAPINoPrefix(t *testing.T) {
|
|||
t.Fatalf("runAPI no-prefix error: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRenderBatchRequestsTemplateVars(t *testing.T) {
|
||||
requests, err := renderBatchRequests([]batchRequest{
|
||||
{
|
||||
Name: "comment-{{number}}",
|
||||
Method: "post",
|
||||
Path: "v1/{{owner}}/{{repo}}/issues/{{number}}/journals",
|
||||
Query: map[string]interface{}{
|
||||
"label": []interface{}{"{{label}}", "triage"},
|
||||
"page": float64(1),
|
||||
},
|
||||
Body: map[string]interface{}{
|
||||
"notes": "handled by {{actor}}",
|
||||
"meta": map[string]interface{}{"repo": "{{repo}}"},
|
||||
},
|
||||
},
|
||||
}, map[string]string{
|
||||
"owner": "Gitlink",
|
||||
"repo": "gitlink-cli",
|
||||
"number": "42",
|
||||
"label": "bug",
|
||||
"actor": "bot",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("renderBatchRequests error: %v", err)
|
||||
}
|
||||
if len(requests) != 1 {
|
||||
t.Fatalf("len = %d, want 1", len(requests))
|
||||
}
|
||||
req := requests[0]
|
||||
if req.Name != "comment-42" {
|
||||
t.Fatalf("Name = %q", req.Name)
|
||||
}
|
||||
if req.Method != "POST" {
|
||||
t.Fatalf("Method = %q", req.Method)
|
||||
}
|
||||
if req.Path != "/v1/Gitlink/gitlink-cli/issues/42/journals" {
|
||||
t.Fatalf("Path = %q", req.Path)
|
||||
}
|
||||
if got := req.Query["label"]; len(got) != 2 || got[0] != "bug" || got[1] != "triage" {
|
||||
t.Fatalf("label query = %#v", got)
|
||||
}
|
||||
body := req.Body.(map[string]interface{})
|
||||
if body["notes"] != "handled by bot" {
|
||||
t.Fatalf("notes = %#v", body["notes"])
|
||||
}
|
||||
}
|
||||
|
||||
func TestRenderBatchRequestsMissingVar(t *testing.T) {
|
||||
_, err := renderBatchRequests([]batchRequest{{Method: "GET", Path: "/{{missing}}"}}, nil)
|
||||
if err == nil {
|
||||
t.Fatal("expected missing variable error")
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunAPIBatchDryRunDoesNotReachServer(t *testing.T) {
|
||||
setupAPITest(t, func(w http.ResponseWriter, r *http.Request) {
|
||||
t.Fatal("dry-run should not reach server")
|
||||
})
|
||||
cmdutil.Format = "json"
|
||||
|
||||
plan := writeBatchPlan(t, map[string]interface{}{
|
||||
"vars": map[string]string{"owner": "Gitlink"},
|
||||
"requests": []map[string]interface{}{
|
||||
{"name": "me", "method": "GET", "path": "/users/me"},
|
||||
{"name": "repo", "method": "GET", "path": "/{{owner}}/gitlink-cli"},
|
||||
},
|
||||
})
|
||||
|
||||
cmd := NewAPICmd()
|
||||
cmd.SetArgs([]string{"--batch-file", plan, "--dry-run"})
|
||||
if err := cmd.Execute(); err != nil {
|
||||
t.Fatalf("dry-run batch error: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunAPIBatchExecutesRequestsWithOverrides(t *testing.T) {
|
||||
var seen []string
|
||||
var gotBody map[string]interface{}
|
||||
setupAPITest(t, func(w http.ResponseWriter, r *http.Request) {
|
||||
seen = append(seen, r.Method+" "+r.URL.String())
|
||||
switch r.URL.Path {
|
||||
case "/v1/Mengz/gitlink-cli/issues.json":
|
||||
if r.URL.Query().Get("state") != "open" {
|
||||
t.Fatalf("state query = %q", r.URL.Query().Get("state"))
|
||||
}
|
||||
json.NewEncoder(w).Encode(map[string]interface{}{"issues": []interface{}{}})
|
||||
case "/v1/Mengz/gitlink-cli/issues/7/journals.json":
|
||||
if err := json.NewDecoder(r.Body).Decode(&gotBody); err != nil {
|
||||
t.Fatalf("decode body: %v", err)
|
||||
}
|
||||
json.NewEncoder(w).Encode(map[string]interface{}{"id": 99})
|
||||
default:
|
||||
t.Fatalf("unexpected path: %s", r.URL.Path)
|
||||
}
|
||||
})
|
||||
cmdutil.Format = "json"
|
||||
|
||||
plan := writeBatchPlan(t, map[string]interface{}{
|
||||
"vars": map[string]string{"owner": "Gitlink", "repo": "gitlink-cli", "issue": "7"},
|
||||
"requests": []map[string]interface{}{
|
||||
{
|
||||
"name": "list",
|
||||
"method": "GET",
|
||||
"path": "/v1/{{owner}}/{{repo}}/issues",
|
||||
"query": map[string]interface{}{"state": "open"},
|
||||
},
|
||||
{
|
||||
"name": "comment",
|
||||
"method": "POST",
|
||||
"path": "/v1/{{owner}}/{{repo}}/issues/{{issue}}/journals",
|
||||
"body": map[string]interface{}{"notes": "hello {{repo}}"},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
cmd := NewAPICmd()
|
||||
cmd.SetArgs([]string{"--batch-file", plan, "--var", "owner=Mengz"})
|
||||
if err := cmd.Execute(); err != nil {
|
||||
t.Fatalf("batch execute error: %v", err)
|
||||
}
|
||||
if len(seen) != 2 {
|
||||
t.Fatalf("requests = %d, want 2 (%v)", len(seen), seen)
|
||||
}
|
||||
if gotBody["notes"] != "hello gitlink-cli" {
|
||||
t.Fatalf("body notes = %#v", gotBody["notes"])
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunAPIBatchStopsOnErrorByDefault(t *testing.T) {
|
||||
var seen []string
|
||||
setupAPITest(t, func(w http.ResponseWriter, r *http.Request) {
|
||||
seen = append(seen, r.URL.Path)
|
||||
if r.URL.Path == "/fail.json" {
|
||||
http.Error(w, "boom", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
json.NewEncoder(w).Encode(map[string]interface{}{"ok": true})
|
||||
})
|
||||
cmdutil.Format = "json"
|
||||
|
||||
plan := writeBatchPlan(t, map[string]interface{}{
|
||||
"requests": []map[string]interface{}{
|
||||
{"method": "GET", "path": "/ok"},
|
||||
{"method": "GET", "path": "/fail"},
|
||||
{"method": "GET", "path": "/never"},
|
||||
},
|
||||
})
|
||||
|
||||
cmd := NewAPICmd()
|
||||
cmd.SetArgs([]string{"--batch-file", plan})
|
||||
if err := cmd.Execute(); err == nil {
|
||||
t.Fatal("expected batch error")
|
||||
}
|
||||
if len(seen) != 2 {
|
||||
t.Fatalf("requests = %d, want 2 (%v)", len(seen), seen)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunAPIBatchContinueOnError(t *testing.T) {
|
||||
var seen []string
|
||||
setupAPITest(t, func(w http.ResponseWriter, r *http.Request) {
|
||||
seen = append(seen, r.URL.Path)
|
||||
if r.URL.Path == "/fail.json" {
|
||||
http.Error(w, "boom", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
json.NewEncoder(w).Encode(map[string]interface{}{"ok": true})
|
||||
})
|
||||
cmdutil.Format = "json"
|
||||
|
||||
plan := writeBatchPlan(t, map[string]interface{}{
|
||||
"requests": []map[string]interface{}{
|
||||
{"method": "GET", "path": "/ok"},
|
||||
{"method": "GET", "path": "/fail"},
|
||||
{"method": "GET", "path": "/after"},
|
||||
},
|
||||
})
|
||||
|
||||
cmd := NewAPICmd()
|
||||
cmd.SetArgs([]string{"--batch-file", plan, "--continue-on-error"})
|
||||
if err := cmd.Execute(); err != nil {
|
||||
t.Fatalf("batch should continue: %v", err)
|
||||
}
|
||||
if len(seen) != 3 {
|
||||
t.Fatalf("requests = %d, want 3 (%v)", len(seen), seen)
|
||||
}
|
||||
}
|
||||
|
||||
func writeBatchPlan(t *testing.T, payload interface{}) string {
|
||||
t.Helper()
|
||||
data, err := json.Marshal(payload)
|
||||
if err != nil {
|
||||
t.Fatalf("marshal plan: %v", err)
|
||||
}
|
||||
path := filepath.Join(t.TempDir(), "plan.json")
|
||||
if err := os.WriteFile(path, data, 0600); err != nil {
|
||||
t.Fatalf("write plan: %v", err)
|
||||
}
|
||||
return path
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,351 @@
|
|||
package api
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"os"
|
||||
"regexp"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/gitlink-org/gitlink-cli/cmd/cmdutil"
|
||||
"github.com/gitlink-org/gitlink-cli/internal/client"
|
||||
"github.com/gitlink-org/gitlink-cli/internal/output"
|
||||
)
|
||||
|
||||
type batchPlan struct {
|
||||
Vars map[string]string `json:"vars"`
|
||||
Requests []batchRequest `json:"requests"`
|
||||
}
|
||||
|
||||
type batchRequest struct {
|
||||
Name string `json:"name"`
|
||||
Method string `json:"method"`
|
||||
Path string `json:"path"`
|
||||
Query map[string]interface{} `json:"query"`
|
||||
Body interface{} `json:"body"`
|
||||
}
|
||||
|
||||
type renderedBatchRequest struct {
|
||||
Index int `json:"index" yaml:"index"`
|
||||
Name string `json:"name,omitempty" yaml:"name,omitempty"`
|
||||
Method string `json:"method" yaml:"method"`
|
||||
Path string `json:"path" yaml:"path"`
|
||||
Query url.Values `json:"query,omitempty" yaml:"query,omitempty"`
|
||||
Body interface{} `json:"body,omitempty" yaml:"body,omitempty"`
|
||||
}
|
||||
|
||||
type batchResult struct {
|
||||
Index int `json:"index" yaml:"index"`
|
||||
Name string `json:"name,omitempty" yaml:"name,omitempty"`
|
||||
Method string `json:"method" yaml:"method"`
|
||||
Path string `json:"path" yaml:"path"`
|
||||
OK bool `json:"ok" yaml:"ok"`
|
||||
Error string `json:"error,omitempty" yaml:"error,omitempty"`
|
||||
Data interface{} `json:"data,omitempty" yaml:"data,omitempty"`
|
||||
}
|
||||
|
||||
type batchSummary struct {
|
||||
DryRun bool `json:"dry_run" yaml:"dry_run"`
|
||||
ContinueOnError bool `json:"continue_on_error" yaml:"continue_on_error"`
|
||||
Total int `json:"total" yaml:"total"`
|
||||
Succeeded int `json:"succeeded" yaml:"succeeded"`
|
||||
Failed int `json:"failed" yaml:"failed"`
|
||||
Variables map[string]string `json:"variables,omitempty" yaml:"variables,omitempty"`
|
||||
Requests []renderedBatchRequest `json:"requests,omitempty" yaml:"requests,omitempty"`
|
||||
Results []batchResult `json:"results,omitempty" yaml:"results,omitempty"`
|
||||
}
|
||||
|
||||
var templatePattern = regexp.MustCompile(`\{\{\s*([A-Za-z0-9_.-]+)\s*\}\}`)
|
||||
|
||||
func runAPIBatch(c *cobra.Command, batchFile string) error {
|
||||
if hasSingleRequestInput(c) {
|
||||
return fmt.Errorf("use batch flags separately from --body, --body-file, --body-stdin, --query, or --header")
|
||||
}
|
||||
|
||||
dryRun, _ := c.Flags().GetBool("dry-run")
|
||||
continueOnError, _ := c.Flags().GetBool("continue-on-error")
|
||||
overrides, err := parseBatchVars(c)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
plan, err := readBatchPlan(batchFile)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
vars := mergeBatchVars(plan.Vars, overrides)
|
||||
requests, err := renderBatchRequests(plan.Requests, vars)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if dryRun {
|
||||
return output.Print(output.SuccessEnvelope(batchSummary{
|
||||
DryRun: true,
|
||||
ContinueOnError: continueOnError,
|
||||
Total: len(requests),
|
||||
Variables: sortedVars(vars),
|
||||
Requests: requests,
|
||||
}, nil), resolveFormat())
|
||||
}
|
||||
|
||||
cli, err := client.New()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
cli.Debug = cmdutil.Debug
|
||||
|
||||
summary := batchSummary{
|
||||
DryRun: false,
|
||||
ContinueOnError: continueOnError,
|
||||
Total: len(requests),
|
||||
Variables: sortedVars(vars),
|
||||
Results: make([]batchResult, 0, len(requests)),
|
||||
}
|
||||
for _, req := range requests {
|
||||
result := batchResult{
|
||||
Index: req.Index,
|
||||
Name: req.Name,
|
||||
Method: req.Method,
|
||||
Path: req.Path,
|
||||
}
|
||||
env, callErr := cli.Do(req.Method, req.Path, req.Body, req.Query)
|
||||
if callErr != nil {
|
||||
summary.Failed++
|
||||
result.OK = false
|
||||
result.Error = apiBatchErrorMessage(callErr)
|
||||
summary.Results = append(summary.Results, result)
|
||||
if !continueOnError {
|
||||
_ = output.Print(output.SuccessEnvelope(summary, nil), resolveFormat())
|
||||
return callErr
|
||||
}
|
||||
continue
|
||||
}
|
||||
summary.Succeeded++
|
||||
result.OK = true
|
||||
if env != nil {
|
||||
result.Data = env.Data
|
||||
}
|
||||
summary.Results = append(summary.Results, result)
|
||||
}
|
||||
|
||||
return output.Print(output.SuccessEnvelope(summary, nil), resolveFormat())
|
||||
}
|
||||
|
||||
func hasSingleRequestInput(c *cobra.Command) bool {
|
||||
body, _ := c.Flags().GetString("body")
|
||||
bodyFile, _ := c.Flags().GetString("body-file")
|
||||
bodyStdin, _ := c.Flags().GetBool("body-stdin")
|
||||
query, _ := c.Flags().GetString("query")
|
||||
headers, _ := c.Flags().GetStringSlice("header")
|
||||
return body != "" || bodyFile != "" || bodyStdin || query != "" || len(headers) > 0
|
||||
}
|
||||
|
||||
func parseBatchVars(c *cobra.Command) (map[string]string, error) {
|
||||
raw, _ := c.Flags().GetStringArray("var")
|
||||
vars := make(map[string]string, len(raw))
|
||||
for _, item := range raw {
|
||||
key, value, ok := strings.Cut(item, "=")
|
||||
key = strings.TrimSpace(key)
|
||||
if !ok || key == "" {
|
||||
return nil, fmt.Errorf("invalid --var %q, want key=value", item)
|
||||
}
|
||||
vars[key] = value
|
||||
}
|
||||
return vars, nil
|
||||
}
|
||||
|
||||
func readBatchPlan(path string) (*batchPlan, error) {
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("read batch file: %w", err)
|
||||
}
|
||||
var plan batchPlan
|
||||
if err := json.Unmarshal(data, &plan); err != nil {
|
||||
return nil, fmt.Errorf("invalid batch file JSON: %w", err)
|
||||
}
|
||||
if len(plan.Requests) == 0 {
|
||||
return nil, fmt.Errorf("batch file must contain at least one request")
|
||||
}
|
||||
return &plan, nil
|
||||
}
|
||||
|
||||
func mergeBatchVars(planVars, overrides map[string]string) map[string]string {
|
||||
vars := make(map[string]string, len(planVars)+len(overrides))
|
||||
for key, value := range planVars {
|
||||
vars[key] = value
|
||||
}
|
||||
for key, value := range overrides {
|
||||
vars[key] = value
|
||||
}
|
||||
return vars
|
||||
}
|
||||
|
||||
func renderBatchRequests(requests []batchRequest, vars map[string]string) ([]renderedBatchRequest, error) {
|
||||
rendered := make([]renderedBatchRequest, 0, len(requests))
|
||||
for i, req := range requests {
|
||||
method := strings.ToUpper(strings.TrimSpace(req.Method))
|
||||
if method == "" {
|
||||
return nil, fmt.Errorf("request %d method is required", i+1)
|
||||
}
|
||||
path, err := renderTemplate(req.Path, vars)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("request %d path: %w", i+1, err)
|
||||
}
|
||||
path = strings.TrimSpace(path)
|
||||
if path == "" {
|
||||
return nil, fmt.Errorf("request %d path is required", i+1)
|
||||
}
|
||||
if !strings.HasPrefix(path, "/") {
|
||||
path = "/" + path
|
||||
}
|
||||
query, err := renderBatchQuery(req.Query, vars)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("request %d query: %w", i+1, err)
|
||||
}
|
||||
body, err := renderBatchValue(req.Body, vars)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("request %d body: %w", i+1, err)
|
||||
}
|
||||
name, err := renderTemplate(req.Name, vars)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("request %d name: %w", i+1, err)
|
||||
}
|
||||
rendered = append(rendered, renderedBatchRequest{
|
||||
Index: i + 1,
|
||||
Name: name,
|
||||
Method: method,
|
||||
Path: path,
|
||||
Query: query,
|
||||
Body: body,
|
||||
})
|
||||
}
|
||||
return rendered, nil
|
||||
}
|
||||
|
||||
func renderBatchQuery(raw map[string]interface{}, vars map[string]string) (url.Values, error) {
|
||||
if len(raw) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
query := url.Values{}
|
||||
keys := make([]string, 0, len(raw))
|
||||
for key := range raw {
|
||||
keys = append(keys, key)
|
||||
}
|
||||
sort.Strings(keys)
|
||||
for _, key := range keys {
|
||||
renderedKey, err := renderTemplate(key, vars)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
values, err := renderQueryValues(raw[key], vars)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%s: %w", key, err)
|
||||
}
|
||||
for _, value := range values {
|
||||
query.Add(renderedKey, value)
|
||||
}
|
||||
}
|
||||
return query, nil
|
||||
}
|
||||
|
||||
func renderQueryValues(raw interface{}, vars map[string]string) ([]string, error) {
|
||||
switch value := raw.(type) {
|
||||
case nil:
|
||||
return []string{""}, nil
|
||||
case string:
|
||||
rendered, err := renderTemplate(value, vars)
|
||||
return []string{rendered}, err
|
||||
case []interface{}:
|
||||
values := make([]string, 0, len(value))
|
||||
for _, item := range value {
|
||||
itemValues, err := renderQueryValues(item, vars)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
values = append(values, itemValues...)
|
||||
}
|
||||
return values, nil
|
||||
default:
|
||||
return []string{fmt.Sprint(value)}, nil
|
||||
}
|
||||
}
|
||||
|
||||
func renderBatchValue(raw interface{}, vars map[string]string) (interface{}, error) {
|
||||
switch value := raw.(type) {
|
||||
case nil:
|
||||
return nil, nil
|
||||
case string:
|
||||
return renderTemplate(value, vars)
|
||||
case []interface{}:
|
||||
items := make([]interface{}, 0, len(value))
|
||||
for _, item := range value {
|
||||
rendered, err := renderBatchValue(item, vars)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, rendered)
|
||||
}
|
||||
return items, nil
|
||||
case map[string]interface{}:
|
||||
obj := make(map[string]interface{}, len(value))
|
||||
for key, item := range value {
|
||||
renderedKey, err := renderTemplate(key, vars)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
rendered, err := renderBatchValue(item, vars)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
obj[renderedKey] = rendered
|
||||
}
|
||||
return obj, nil
|
||||
default:
|
||||
return raw, nil
|
||||
}
|
||||
}
|
||||
|
||||
func renderTemplate(value string, vars map[string]string) (string, error) {
|
||||
var missing []string
|
||||
rendered := templatePattern.ReplaceAllStringFunc(value, func(match string) string {
|
||||
parts := templatePattern.FindStringSubmatch(match)
|
||||
if len(parts) != 2 {
|
||||
return match
|
||||
}
|
||||
replacement, ok := vars[parts[1]]
|
||||
if !ok {
|
||||
missing = append(missing, parts[1])
|
||||
return match
|
||||
}
|
||||
return replacement
|
||||
})
|
||||
if len(missing) > 0 {
|
||||
sort.Strings(missing)
|
||||
return "", fmt.Errorf("missing template variable(s): %s", strings.Join(missing, ", "))
|
||||
}
|
||||
return rendered, nil
|
||||
}
|
||||
|
||||
func sortedVars(vars map[string]string) map[string]string {
|
||||
if len(vars) == 0 {
|
||||
return nil
|
||||
}
|
||||
copyVars := make(map[string]string, len(vars))
|
||||
for key, value := range vars {
|
||||
copyVars[key] = value
|
||||
}
|
||||
return copyVars
|
||||
}
|
||||
|
||||
func apiBatchErrorMessage(err error) string {
|
||||
var apiErr *client.APIError
|
||||
if errors.As(err, &apiErr) {
|
||||
return apiErr.Message
|
||||
}
|
||||
return err.Error()
|
||||
}
|
||||
|
|
@ -12,13 +12,10 @@ import (
|
|||
internalAuth "github.com/gitlink-org/gitlink-cli/internal/auth"
|
||||
)
|
||||
|
||||
// setupAuthTest sets up a temporary config directory for auth tests.
|
||||
// Returns the path to the config dir (.config/gitlink-cli).
|
||||
func setupAuthTest(t *testing.T) string {
|
||||
func tempConfigDir(t *testing.T) string {
|
||||
t.Helper()
|
||||
dir := filepath.Join(t.TempDir(), ".config", "gitlink-cli")
|
||||
dir := t.TempDir()
|
||||
t.Setenv("GITLINK_CONFIG_DIR", dir)
|
||||
t.Setenv("HOME", filepath.Dir(filepath.Dir(dir))) // for backward compat
|
||||
return dir
|
||||
}
|
||||
|
||||
|
|
@ -72,7 +69,7 @@ func TestLoginTokenFlag(t *testing.T) {
|
|||
|
||||
func TestStatusCmdNotLoggedIn(t *testing.T) {
|
||||
keyring.MockInitWithError(errors.New("keychain unavailable"))
|
||||
setupAuthTest(t)
|
||||
tempConfigDir(t)
|
||||
t.Setenv("GITLINK_TOKEN", "")
|
||||
_ = internalAuth.DeleteToken()
|
||||
|
||||
|
|
@ -87,7 +84,7 @@ func TestStatusCmdNotLoggedIn(t *testing.T) {
|
|||
|
||||
func TestStatusCmdEnvToken(t *testing.T) {
|
||||
keyring.MockInitWithError(errors.New("keychain unavailable"))
|
||||
setupAuthTest(t)
|
||||
tempConfigDir(t)
|
||||
t.Setenv("GITLINK_TOKEN", "env-token-123")
|
||||
_ = internalAuth.DeleteToken()
|
||||
|
||||
|
|
@ -97,7 +94,7 @@ func TestStatusCmdEnvToken(t *testing.T) {
|
|||
|
||||
func TestStatusCmdStoredToken(t *testing.T) {
|
||||
keyring.MockInitWithError(errors.New("keychain unavailable"))
|
||||
dir := setupAuthTest(t)
|
||||
dir := tempConfigDir(t)
|
||||
t.Setenv("GITLINK_TOKEN", "")
|
||||
|
||||
os.MkdirAll(dir, 0700)
|
||||
|
|
@ -109,7 +106,7 @@ func TestStatusCmdStoredToken(t *testing.T) {
|
|||
|
||||
func TestStatusCmdEnvAndStoredToken(t *testing.T) {
|
||||
keyring.MockInitWithError(errors.New("keychain unavailable"))
|
||||
dir := setupAuthTest(t)
|
||||
dir := tempConfigDir(t)
|
||||
t.Setenv("GITLINK_TOKEN", "env-token")
|
||||
|
||||
os.MkdirAll(dir, 0700)
|
||||
|
|
@ -123,9 +120,9 @@ func TestStatusCmdEnvAndStoredToken(t *testing.T) {
|
|||
|
||||
func TestStatusCmdStoredTokenButLoadFails(t *testing.T) {
|
||||
keyring.MockInitWithError(errors.New("keychain unavailable"))
|
||||
setupAuthTest(t)
|
||||
tempConfigDir(t)
|
||||
t.Setenv("GITLINK_TOKEN", "")
|
||||
// Don't create credentials file — LoadToken returns empty
|
||||
// Do not create credentials; LoadToken should return empty.
|
||||
|
||||
cmd := findSub(NewAuthCmd(), "status")
|
||||
if err := cmd.RunE(cmd, nil); err != nil {
|
||||
|
|
@ -133,22 +130,21 @@ func TestStatusCmdStoredTokenButLoadFails(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestLogoutCmdError(t *testing.T) {
|
||||
func TestLogoutCmdNoStoredToken(t *testing.T) {
|
||||
keyring.MockInitWithError(errors.New("keychain unavailable"))
|
||||
setupAuthTest(t)
|
||||
tempConfigDir(t)
|
||||
t.Setenv("GITLINK_TOKEN", "")
|
||||
// Don't create credentials dir — DeleteToken will fail
|
||||
// Do not create credentials; logout should be idempotent.
|
||||
|
||||
cmd := findSub(NewAuthCmd(), "logout")
|
||||
err := cmd.RunE(cmd, nil)
|
||||
if err == nil {
|
||||
t.Fatal("expected error when DeleteToken fails")
|
||||
if err := cmd.RunE(cmd, nil); err != nil {
|
||||
t.Fatalf("logout should succeed without stored token: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLogoutCmd(t *testing.T) {
|
||||
keyring.MockInitWithError(errors.New("keychain unavailable"))
|
||||
dir := setupAuthTest(t)
|
||||
dir := tempConfigDir(t)
|
||||
t.Setenv("GITLINK_TOKEN", "")
|
||||
|
||||
// Store a token first so DeleteToken has something to delete
|
||||
|
|
@ -166,7 +162,7 @@ func TestLogoutCmd(t *testing.T) {
|
|||
|
||||
func TestLoginWithToken(t *testing.T) {
|
||||
keyring.MockInitWithError(errors.New("keychain unavailable"))
|
||||
dir := setupAuthTest(t)
|
||||
dir := tempConfigDir(t)
|
||||
t.Setenv("GITLINK_TOKEN", "")
|
||||
|
||||
// Mock stdin
|
||||
|
|
@ -202,7 +198,7 @@ func TestLoginWithToken(t *testing.T) {
|
|||
|
||||
func TestLoginWithTokenEmpty(t *testing.T) {
|
||||
keyring.MockInitWithError(errors.New("keychain unavailable"))
|
||||
setupAuthTest(t)
|
||||
tempConfigDir(t)
|
||||
t.Setenv("GITLINK_TOKEN", "")
|
||||
|
||||
oldStdin := os.Stdin
|
||||
|
|
@ -225,7 +221,7 @@ func TestLoginWithTokenEmpty(t *testing.T) {
|
|||
|
||||
func TestLoginWithPasswordNoTerminal(t *testing.T) {
|
||||
keyring.MockInitWithError(errors.New("keychain unavailable"))
|
||||
setupAuthTest(t)
|
||||
tempConfigDir(t)
|
||||
t.Setenv("GITLINK_TOKEN", "")
|
||||
|
||||
// term.ReadPassword will fail because test has no terminal
|
||||
|
|
|
|||
|
|
@ -1,165 +0,0 @@
|
|||
package browse
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/gitlink-org/gitlink-cli/cmd/cmdutil"
|
||||
"github.com/gitlink-org/gitlink-cli/internal/context"
|
||||
"github.com/gitlink-org/gitlink-cli/internal/output"
|
||||
"github.com/gitlink-org/gitlink-cli/internal/web"
|
||||
)
|
||||
|
||||
// stdout is the browse command's output target (so tests can redirect).
|
||||
var stdout io.Writer = os.Stdout
|
||||
|
||||
// browsableKinds maps the first path segment of `browse <kind>[/id>` to a URL
|
||||
// builder. The "default" entry is used as a fallback that appends the raw arg
|
||||
// to the repo URL, preserving the original passthrough behaviour.
|
||||
var browsableKinds = []struct {
|
||||
kind string
|
||||
desc string
|
||||
}{
|
||||
{"issues", "Issue 列表 / 详情 (issues/42)"},
|
||||
{"pulls", "PR 列表 / 详情 (pulls/128)"},
|
||||
{"wiki", "Wiki 首页 / 页面 (wiki 或 wiki/API指南)"},
|
||||
{"actions", "CI/Actions 页面"},
|
||||
{"commits", "提交列表 / 详情 (commits/abc123)"},
|
||||
{"branches", "分支列表"},
|
||||
{"releases", "Release 列表 / 详情 (releases/v2.0)"},
|
||||
{"milestones", "里程碑页面"},
|
||||
{"labels", "标签管理页"},
|
||||
{"settings/hooks", "Webhook 设置页"},
|
||||
{"settings/collaboration", "成员管理页"},
|
||||
{"projects", "项目看板页"},
|
||||
}
|
||||
|
||||
// NewBrowseCmd creates the browse command for opening GitLink pages in a browser.
|
||||
func NewBrowseCmd() *cobra.Command {
|
||||
var listFlag, noOpen bool
|
||||
cmd := &cobra.Command{
|
||||
Use: "browse [resource]",
|
||||
Short: "在浏览器中打开 GitLink 页面",
|
||||
Long: `打开当前仓库(或指定资源)的 GitLink 页面。
|
||||
|
||||
资源格式: issues/42, pulls/42, wiki, wiki/页面名, commits/abc123, ...
|
||||
不带参数则打开仓库主页。owner/repo 自动从 git remote 推断或用 --owner/--repo 指定。
|
||||
|
||||
示例:
|
||||
gitlink-cli browse
|
||||
gitlink-cli browse issues/42
|
||||
gitlink-cli browse pulls/128
|
||||
gitlink-cli browse wiki
|
||||
gitlink-cli browse --list # 列出所有可浏览页面
|
||||
gitlink-cli browse --no-open # 只打印 URL,不打开浏览器`,
|
||||
Example: ` gitlink-cli browse
|
||||
gitlink-cli browse issues/42
|
||||
gitlink-cli browse pulls/128
|
||||
gitlink-cli browse wiki
|
||||
gitlink-cli browse --list
|
||||
gitlink-cli browse --no-open`,
|
||||
Args: cobra.MaximumNArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
owner, repo, err := context.ResolveOwnerRepo(cmdutil.Owner, cmdutil.Repo)
|
||||
if err != nil {
|
||||
return fmt.Errorf("无法推断仓库信息: %w", err)
|
||||
}
|
||||
|
||||
if listFlag {
|
||||
listBrowsables(owner, repo)
|
||||
return nil
|
||||
}
|
||||
|
||||
rurl := resolveBrowseURL(web.NewBuilder(), owner, repo, args)
|
||||
emitBrowse(rurl)
|
||||
if !noOpen {
|
||||
if err := web.OpenBrowser(rurl.URL); err != nil {
|
||||
// 打开失败仅告警,URL 已经打印供手动复制
|
||||
fmt.Fprintf(stdout, "(浏览器未自动打开: %v;请手动复制上方 URL)\n", err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
},
|
||||
}
|
||||
cmd.Flags().BoolVar(&listFlag, "list", false, "列出当前仓库所有可浏览的页面")
|
||||
cmd.Flags().BoolVar(&noOpen, "no-open", false, "只打印 URL,不打开浏览器")
|
||||
return cmd
|
||||
}
|
||||
|
||||
// emitBrowse prints the URL — friendly single line by default, structured
|
||||
// envelope when --format is set.
|
||||
func emitBrowse(r *web.ResourceURL) {
|
||||
if cmdutil.Format == "" {
|
||||
fmt.Fprintf(stdout, "🔗 %s\n", r.URL)
|
||||
return
|
||||
}
|
||||
_ = output.PrintTo(stdout, output.SuccessEnvelope(r, nil), cmdutil.Format)
|
||||
}
|
||||
|
||||
// listBrowsables prints the catalog of pages `browse` understands.
|
||||
func listBrowsables(owner, repo string) {
|
||||
fmt.Fprintf(stdout, "可浏览的 GitLink 页面 (%s/%s):\n", owner, repo)
|
||||
for _, k := range browsableKinds {
|
||||
fmt.Fprintf(stdout, " %-28s %s\n", k.kind, k.desc)
|
||||
}
|
||||
fmt.Fprintf(stdout, "\n用法: gitlink-cli browse <资源>\n")
|
||||
}
|
||||
|
||||
// resolveBrowseURL maps `browse <arg>` to a web URL. With no arg → repo home.
|
||||
func resolveBrowseURL(b *web.Builder, owner, repo string, args []string) *web.ResourceURL {
|
||||
if len(args) == 0 || args[0] == "" {
|
||||
return b.RepoURL(owner, repo)
|
||||
}
|
||||
arg := strings.TrimPrefix(args[0], "/")
|
||||
// Split into kind and (optional) rest after the first "/".
|
||||
kind, rest, _ := strings.Cut(arg, "/")
|
||||
rest = strings.Trim(rest, "/")
|
||||
|
||||
switch {
|
||||
case kind == "issues" || kind == "issue":
|
||||
return b.IssueURL(owner, repo, atoiOrZero(rest))
|
||||
case kind == "pulls" || kind == "pr" || kind == "pull":
|
||||
return b.PRURL(owner, repo, atoiOrZero(rest))
|
||||
case kind == "wiki":
|
||||
return b.WikiURL(owner, repo, rest)
|
||||
case kind == "actions" || kind == "ci":
|
||||
return b.CIURL(owner, repo)
|
||||
case kind == "commits":
|
||||
return b.CommitURL(owner, repo, rest)
|
||||
case kind == "branches":
|
||||
return b.BranchURL(owner, repo, rest)
|
||||
case kind == "releases":
|
||||
return b.ReleaseURL(owner, repo, rest)
|
||||
case kind == "milestones":
|
||||
return b.MilestoneURL(owner, repo)
|
||||
case kind == "labels":
|
||||
return b.LabelURL(owner, repo)
|
||||
case arg == "settings/hooks":
|
||||
return b.WebhookURL(owner, repo)
|
||||
case arg == "settings/collaboration":
|
||||
return b.MemberURL(owner, repo)
|
||||
case kind == "settings":
|
||||
return b.RepoURL(owner, repo) // settings landing falls back to repo home
|
||||
default:
|
||||
// Unknown resource: append the raw arg as a path segment so behaviour
|
||||
// stays predictable for callers that already know their URL shape.
|
||||
return &web.ResourceURL{
|
||||
URL: b.RepoURL(owner, repo).URL + "/" + arg,
|
||||
Resource: "custom",
|
||||
Identifier: arg,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func atoiOrZero(s string) int {
|
||||
n, err := strconv.Atoi(s)
|
||||
if err != nil {
|
||||
return 0
|
||||
}
|
||||
return n
|
||||
}
|
||||
|
|
@ -1,176 +0,0 @@
|
|||
package browse
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/gitlink-org/gitlink-cli/cmd/cmdutil"
|
||||
"github.com/gitlink-org/gitlink-cli/internal/web"
|
||||
)
|
||||
|
||||
func TestNewBrowseCmd(t *testing.T) {
|
||||
cmd := NewBrowseCmd()
|
||||
if cmd.Use != "browse [resource]" {
|
||||
t.Errorf("expected Use 'browse [resource]', got %s", cmd.Use)
|
||||
}
|
||||
if cmd.Short == "" {
|
||||
t.Error("Short description should not be empty")
|
||||
}
|
||||
if cmd.Long == "" {
|
||||
t.Error("Long description should not be empty")
|
||||
}
|
||||
}
|
||||
|
||||
func TestBrowseCmdHasCorrectArgs(t *testing.T) {
|
||||
cmd := NewBrowseCmd()
|
||||
if err := cmd.Args(cmd, []string{}); err != nil {
|
||||
t.Errorf("should accept 0 args: %v", err)
|
||||
}
|
||||
if err := cmd.Args(cmd, []string{"issues/42"}); err != nil {
|
||||
t.Errorf("should accept 1 arg: %v", err)
|
||||
}
|
||||
if err := cmd.Args(cmd, []string{"a", "b"}); err == nil {
|
||||
t.Error("should reject more than 1 arg")
|
||||
}
|
||||
}
|
||||
|
||||
func TestBrowseCmdNoSubcommands(t *testing.T) {
|
||||
cmd := NewBrowseCmd()
|
||||
if cmd.HasSubCommands() {
|
||||
t.Error("browse should not have subcommands")
|
||||
}
|
||||
}
|
||||
|
||||
func TestBrowseCmdExample(t *testing.T) {
|
||||
cmd := NewBrowseCmd()
|
||||
if cmd.Example == "" {
|
||||
t.Error("Example should not be empty")
|
||||
}
|
||||
if !strings.Contains(cmd.Example, "browse") {
|
||||
t.Error("Example should contain 'browse'")
|
||||
}
|
||||
}
|
||||
|
||||
func TestBrowseCmdHasListAndNoOpenFlags(t *testing.T) {
|
||||
cmd := NewBrowseCmd()
|
||||
if cmd.Flags().Lookup("list") == nil {
|
||||
t.Error("missing --list flag")
|
||||
}
|
||||
if cmd.Flags().Lookup("no-open") == nil {
|
||||
t.Error("missing --no-open flag")
|
||||
}
|
||||
}
|
||||
|
||||
func TestResolveBrowseURL(t *testing.T) {
|
||||
b := web.NewBuilder()
|
||||
cases := []struct {
|
||||
name string
|
||||
args []string
|
||||
wantSub string
|
||||
}{
|
||||
{"no args → repo", nil, "/o/r"},
|
||||
{"issue detail", []string{"issues/42"}, "/issues/42"},
|
||||
{"issue alias", []string{"issue/7"}, "/issues/7"},
|
||||
{"pr detail", []string{"pulls/128"}, "/pulls/128"},
|
||||
{"pr alias", []string{"pr/9"}, "/pulls/9"},
|
||||
{"wiki index", []string{"wiki"}, "/wiki"},
|
||||
{"wiki page", []string{"wiki/Guide"}, "/wiki/Guide"},
|
||||
{"ci", []string{"actions"}, "/actions"},
|
||||
{"ci alias", []string{"ci"}, "/actions"},
|
||||
{"commit", []string{"commits/abc123"}, "/commits/abc123"},
|
||||
{"release", []string{"releases/v2.0"}, "/releases/v2.0"},
|
||||
{"milestones", []string{"milestones"}, "/milestones"},
|
||||
{"labels", []string{"labels"}, "/issues/labels"},
|
||||
{"webhook settings", []string{"settings/hooks"}, "/settings/hooks"},
|
||||
{"collaboration", []string{"settings/collaboration"}, "/settings/collaboration"},
|
||||
{"unknown passthrough", []string{"custom/seg"}, "/custom/seg"},
|
||||
}
|
||||
for _, c := range cases {
|
||||
t.Run(c.name, func(t *testing.T) {
|
||||
r := resolveBrowseURL(b, "o", "r", c.args)
|
||||
if !strings.Contains(r.URL, c.wantSub) {
|
||||
t.Errorf("URL %q missing %q", r.URL, c.wantSub)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestBrowseListOutputsCatalog(t *testing.T) {
|
||||
out := runBrowse(t, "--owner", "o", "--repo", "r", "--list")
|
||||
for _, want := range []string{"issues", "pulls", "wiki", "actions"} {
|
||||
if !strings.Contains(out, want) {
|
||||
t.Errorf("list missing %q: %q", want, out)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestBrowseJSONFormat(t *testing.T) {
|
||||
// --format json must route emitBrowse through the output envelope.
|
||||
out := runBrowseFmt(t, "json", "issues/42")
|
||||
if !strings.Contains(out, `"html_url"`) {
|
||||
t.Errorf("JSON browse missing html_url: %q", out)
|
||||
}
|
||||
}
|
||||
|
||||
func TestResolveBrowseURLIssueNonNumeric(t *testing.T) {
|
||||
// atoiOrZero("abc") should fall back to 0 (issue list page).
|
||||
r := resolveBrowseURL(web.NewBuilder(), "o", "r", []string{"issues/abc"})
|
||||
if !strings.HasSuffix(r.URL, "/issues") {
|
||||
t.Errorf("expected /issues fallback, got %q", r.URL)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBrowseNoOpenDoesNotLaunchBrowser(t *testing.T) {
|
||||
// --no-open must print the URL but never invoke a browser. We can't easily
|
||||
// stub web.OpenBrowser across packages, so we assert the URL is printed
|
||||
// and that the "browser did not open" warning (printed only when
|
||||
// OpenBrowser returns an error) is absent.
|
||||
out := runBrowseNoOpen(t, "issues/42", true)
|
||||
if !strings.Contains(out, "/issues/42") {
|
||||
t.Errorf("expected /issues/42 in output: %q", out)
|
||||
}
|
||||
if strings.Contains(out, "浏览器未自动打开") {
|
||||
t.Errorf("--no-open should not print open-failure warning: %q", out)
|
||||
}
|
||||
}
|
||||
|
||||
// runBrowse runs `browse <args>` with captured stdout.
|
||||
func runBrowse(t *testing.T, args ...string) string {
|
||||
t.Helper()
|
||||
old := stdout
|
||||
oldOwner, oldRepo, oldFmt := cmdutil.Owner, cmdutil.Repo, cmdutil.Format
|
||||
buf := &bytes.Buffer{}
|
||||
stdout = buf
|
||||
defer func() {
|
||||
stdout = old
|
||||
cmdutil.Owner, cmdutil.Repo, cmdutil.Format = oldOwner, oldRepo, oldFmt
|
||||
}()
|
||||
cmd := NewBrowseCmd()
|
||||
cmd.PersistentFlags().StringVar(&cmdutil.Owner, "owner", "", "")
|
||||
cmd.PersistentFlags().StringVar(&cmdutil.Repo, "repo", "", "")
|
||||
cmd.PersistentFlags().StringVar(&cmdutil.Format, "format", "", "")
|
||||
cmd.SetArgs(args)
|
||||
if err := cmd.Execute(); err != nil {
|
||||
t.Fatalf("browse %v: %v", args, err)
|
||||
}
|
||||
return buf.String()
|
||||
}
|
||||
|
||||
// runBrowseFmt runs `browse <resource>` with a specific --format value.
|
||||
func runBrowseFmt(t *testing.T, format, resource string) string {
|
||||
t.Helper()
|
||||
return runBrowse(t, "--owner", "o", "--repo", "r", "--format", format, "--no-open", resource)
|
||||
}
|
||||
|
||||
func runBrowseNoOpen(t *testing.T, resource string, noOpen bool) string {
|
||||
t.Helper()
|
||||
args := []string{"--owner", "o", "--repo", "r"}
|
||||
if resource != "" {
|
||||
args = append(args, resource)
|
||||
}
|
||||
if noOpen {
|
||||
args = append(args, "--no-open")
|
||||
}
|
||||
return runBrowse(t, args...)
|
||||
}
|
||||
|
|
@ -57,7 +57,7 @@ func TestRootCmdHasSubcommands(t *testing.T) {
|
|||
for _, sub := range root.Commands() {
|
||||
names[sub.Use] = true
|
||||
}
|
||||
for _, want := range []string{"auth", "config", "version"} {
|
||||
for _, want := range []string{"auth", "config", "doctor", "version"} {
|
||||
if !names[want] {
|
||||
t.Fatalf("missing subcommand: %s", want)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,10 +7,4 @@ var (
|
|||
Format string
|
||||
Debug bool
|
||||
Lang string
|
||||
// Web, when true, asks shortcut commands to print and open the GitLink web
|
||||
// URL corresponding to the resource they just operated on.
|
||||
Web bool
|
||||
// Demo, when true (or GITLINK_DEMO=1), routes every API call through the
|
||||
// in-process mock transport so the CLI runs with no network and no token.
|
||||
Demo bool
|
||||
)
|
||||
|
|
|
|||
121
cmd/demo/demo.go
121
cmd/demo/demo.go
|
|
@ -1,121 +0,0 @@
|
|||
// Package demo implements `gitlink-cli demo +run <showcase>`: a pre-baked,
|
||||
// projector-friendly runner that walks through the subtask-1 demo script with
|
||||
// GITLINK_DEMO=1 forced on, so the whole showcase runs offline.
|
||||
package demo
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strings"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
// stdout is the runner's output target (so tests can redirect).
|
||||
var stdout io.Writer = os.Stdout
|
||||
|
||||
// NewDemoCmd builds the `demo` command tree.
|
||||
func NewDemoCmd() *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "demo",
|
||||
Short: "演示模式 — 无需 Token 即可展示全部功能",
|
||||
Long: `运行预编排的演示脚本,自动开启 GITLINK_DEMO=1,所有命令走内置 Mock 数据,
|
||||
无需网络和 Token,适合课堂投影演示。
|
||||
|
||||
可用 showcase:
|
||||
showcase 全功能展示(覆盖五大类别,~20 min)
|
||||
quick-tour 5 分钟快速导览`,
|
||||
}
|
||||
cmd.AddCommand(newRunCmd())
|
||||
return cmd
|
||||
}
|
||||
|
||||
func newRunCmd() *cobra.Command {
|
||||
var listOnly bool
|
||||
cmd := &cobra.Command{
|
||||
Use: "+run [showcase]",
|
||||
Short: "运行(或列出)预编排演示脚本",
|
||||
Args: cobra.MaximumNArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
if listOnly {
|
||||
for _, s := range showcases {
|
||||
fmt.Fprintf(stdout, "%-12s %s\n", s.Name, s.Description)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
name := "showcase"
|
||||
if len(args) > 0 {
|
||||
name = args[0]
|
||||
}
|
||||
sc, ok := findShowcase(name)
|
||||
if !ok {
|
||||
return fmt.Errorf("未找到 showcase %q,运行 `demo +run --list` 查看可用项", name)
|
||||
}
|
||||
return runShowcase(sc)
|
||||
},
|
||||
}
|
||||
cmd.Flags().BoolVar(&listOnly, "list", false, "列出所有可用 showcase")
|
||||
return cmd
|
||||
}
|
||||
|
||||
// runStep is the per-step executor. It is a package-level variable so tests
|
||||
// can swap it for a no-op instead of recursing into the test binary (which
|
||||
// os.Args[0] points at during go test).
|
||||
var runStep = func(bin string, argv []string, env []string, out io.Writer) error {
|
||||
c := exec.Command(bin, argv...)
|
||||
c.Env = env
|
||||
c.Stdout = out
|
||||
c.Stderr = out
|
||||
return c.Run()
|
||||
}
|
||||
|
||||
// runShowcase prints + executes each step. Each step invokes the gitlink-cli
|
||||
// binary (os.Args[0]) with GITLINK_DEMO=1 so commands return mock data even on
|
||||
// a machine without network/token. Step failures never abort the showcase.
|
||||
func runShowcase(sc Showcase) error {
|
||||
fmt.Fprintf(stdout, "╔════ %s ════╗\n", sc.Name)
|
||||
fmt.Fprintf(stdout, " %s\n 步数: %d\n\n", sc.Description, len(sc.Steps))
|
||||
bin := os.Args[0]
|
||||
env := append(os.Environ(), "GITLINK_DEMO=1")
|
||||
for i, step := range sc.Steps {
|
||||
fmt.Fprintf(stdout, "\n━━━ [%d/%d] %s ━━━\n", i+1, len(sc.Steps), step.Desc)
|
||||
fmt.Fprintf(stdout, "▶ gitlink-cli %s\n", step.Cmd)
|
||||
argv := splitArgs(step.Cmd)
|
||||
if len(argv) == 0 {
|
||||
continue
|
||||
}
|
||||
if err := runStep(bin, argv, env, stdout); err != nil {
|
||||
// Demo never aborts on a step failure — just narrate it.
|
||||
fmt.Fprintf(stdout, " (本步返回错误: %v)\n", err)
|
||||
}
|
||||
}
|
||||
fmt.Fprintf(stdout, "\n╚════ 演示结束(共 %d 步) ════╝\n", len(sc.Steps))
|
||||
return nil
|
||||
}
|
||||
|
||||
// splitArgs tokenises a command line. It honours double-quoted segments so
|
||||
// flags like --title "a b c" survive intact.
|
||||
func splitArgs(line string) []string {
|
||||
var out []string
|
||||
var cur strings.Builder
|
||||
inQ := false
|
||||
for _, r := range line {
|
||||
switch {
|
||||
case r == '"':
|
||||
inQ = !inQ
|
||||
case (r == ' ' || r == '\t') && !inQ:
|
||||
if cur.Len() > 0 {
|
||||
out = append(out, cur.String())
|
||||
cur.Reset()
|
||||
}
|
||||
default:
|
||||
cur.WriteRune(r)
|
||||
}
|
||||
}
|
||||
if cur.Len() > 0 {
|
||||
out = append(out, cur.String())
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
|
@ -1,156 +0,0 @@
|
|||
package demo
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestNewDemoCmd(t *testing.T) {
|
||||
cmd := NewDemoCmd()
|
||||
if cmd.Use != "demo" {
|
||||
t.Errorf("Use = %q, want demo", cmd.Use)
|
||||
}
|
||||
if !cmd.HasSubCommands() {
|
||||
t.Error("demo should have +run subcommand")
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunList(t *testing.T) {
|
||||
out := runRun(t, "--list")
|
||||
if !strings.Contains(out, "showcase") {
|
||||
t.Errorf("list missing showcase: %q", out)
|
||||
}
|
||||
if !strings.Contains(out, "quick-tour") {
|
||||
t.Errorf("list missing quick-tour: %q", out)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunUnknownShowcaseErrors(t *testing.T) {
|
||||
root := NewDemoCmd()
|
||||
root.SetArgs([]string{"+run", "does-not-exist"})
|
||||
err := root.Execute()
|
||||
if err == nil {
|
||||
t.Fatal("expected error for unknown showcase")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "does-not-exist") {
|
||||
t.Errorf("error should name the missing showcase: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFindShowcase(t *testing.T) {
|
||||
if _, ok := findShowcase("showcase"); !ok {
|
||||
t.Error("showcase should exist")
|
||||
}
|
||||
if _, ok := findShowcase("quick-tour"); !ok {
|
||||
t.Error("quick-tour should exist")
|
||||
}
|
||||
if _, ok := findShowcase("missing"); ok {
|
||||
t.Error("missing should not exist")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSplitArgs(t *testing.T) {
|
||||
cases := []struct {
|
||||
in string
|
||||
want []string
|
||||
}{
|
||||
{"", nil},
|
||||
{"version", []string{"version"}},
|
||||
{"issue +list --owner o --repo r", []string{"issue", "+list", "--owner", "o", "--repo", "r"}},
|
||||
{`--title "a b c" --x`, []string{"--title", "a b c", "--x"}},
|
||||
{" multiple spaces ", []string{"multiple", "spaces"}},
|
||||
}
|
||||
for _, c := range cases {
|
||||
got := splitArgs(c.in)
|
||||
if len(got) != len(c.want) {
|
||||
t.Errorf("splitArgs(%q) = %v, want %v", c.in, got, c.want)
|
||||
continue
|
||||
}
|
||||
for i := range got {
|
||||
if got[i] != c.want[i] {
|
||||
t.Errorf("splitArgs(%q)[%d] = %q, want %q", c.in, i, got[i], c.want[i])
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunShowcasePrintsHeaderAndFooter(t *testing.T) {
|
||||
// Stub the per-step executor so we don't recurse into the test binary.
|
||||
oldStep := runStep
|
||||
runStep = func(string, []string, []string, io.Writer) error { return nil }
|
||||
defer func() { runStep = oldStep }()
|
||||
|
||||
old := stdout
|
||||
buf := &bytes.Buffer{}
|
||||
stdout = buf
|
||||
defer func() { stdout = old }()
|
||||
|
||||
sc := Showcase{Name: "t", Description: "test", Steps: []Step{{Desc: "s1", Cmd: "version"}}}
|
||||
if err := runShowcase(sc); err != nil {
|
||||
t.Fatalf("runShowcase: %v", err)
|
||||
}
|
||||
out := buf.String()
|
||||
if !strings.Contains(out, "test") {
|
||||
t.Errorf("missing description: %q", out)
|
||||
}
|
||||
if !strings.Contains(out, "演示结束") {
|
||||
t.Errorf("missing footer: %q", out)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunShowcaseNarratesStepFailure(t *testing.T) {
|
||||
oldStep := runStep
|
||||
runStep = func(string, []string, []string, io.Writer) error { return fmt.Errorf("boom") }
|
||||
defer func() { runStep = oldStep }()
|
||||
|
||||
old := stdout
|
||||
buf := &bytes.Buffer{}
|
||||
stdout = buf
|
||||
defer func() { stdout = old }()
|
||||
|
||||
sc := Showcase{Name: "t", Description: "x", Steps: []Step{{Desc: "s", Cmd: "version"}}}
|
||||
_ = runShowcase(sc)
|
||||
if !strings.Contains(buf.String(), "本步返回错误") {
|
||||
t.Errorf("expected step-failure narration: %q", buf.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunShowcaseSkipsEmptyCmd(t *testing.T) {
|
||||
called := false
|
||||
oldStep := runStep
|
||||
runStep = func(string, []string, []string, io.Writer) error { called = true; return nil }
|
||||
defer func() { runStep = oldStep }()
|
||||
old := stdout
|
||||
stdout = &bytes.Buffer{}
|
||||
defer func() { stdout = old }()
|
||||
|
||||
_ = runShowcase(Showcase{Name: "n", Description: "d", Steps: []Step{{Desc: "empty", Cmd: ""}}})
|
||||
if called {
|
||||
t.Error("runStep should not be called for empty Cmd")
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunListFlagWired(t *testing.T) {
|
||||
cmd := newRunCmd()
|
||||
if cmd.Flags().Lookup("list") == nil {
|
||||
t.Error("missing --list flag on +run")
|
||||
}
|
||||
}
|
||||
|
||||
// runRun executes `demo +run <args>` with captured stdout.
|
||||
func runRun(t *testing.T, args ...string) string {
|
||||
t.Helper()
|
||||
old := stdout
|
||||
buf := &bytes.Buffer{}
|
||||
stdout = buf
|
||||
defer func() { stdout = old }()
|
||||
root := NewDemoCmd()
|
||||
root.SetArgs(append([]string{"+run"}, args...))
|
||||
if err := root.Execute(); err != nil {
|
||||
t.Fatalf("demo +run %v: %v", args, err)
|
||||
}
|
||||
return buf.String()
|
||||
}
|
||||
|
|
@ -1,69 +0,0 @@
|
|||
package demo
|
||||
|
||||
// Showcase is a pre-baked, projector-friendly demo script: a titled sequence of
|
||||
// gitlink-cli invocations. Each step runs with GITLINK_DEMO=1 so the whole
|
||||
// showcase is offline and token-free.
|
||||
type Showcase struct {
|
||||
Name string
|
||||
Description string
|
||||
Steps []Step
|
||||
}
|
||||
|
||||
// Step is one CLI invocation inside a showcase.
|
||||
type Step struct {
|
||||
Desc string // human-readable narration for the slide
|
||||
Cmd string // the gitlink-cli args (without the binary name)
|
||||
}
|
||||
|
||||
// showcases is the catalog. Add new entries here; +run --list reads from it.
|
||||
var showcases = []Showcase{
|
||||
{
|
||||
Name: "showcase",
|
||||
Description: "GitLink-CLI 全功能展示(覆盖子任务一五大类别,~20 min)",
|
||||
Steps: []Step{
|
||||
{"版本与命令全景", "version"},
|
||||
{"自动上下文 + 健康检查", "status"},
|
||||
{"能力探测", "capability +check"},
|
||||
{"终端↔网页联动(仓库主页)", "show repo --owner jiangtx --repo gitlink-cli-demo"},
|
||||
{"Issue 详情页 URL", "show issue --owner jiangtx --repo gitlink-cli-demo --number 42"},
|
||||
|
||||
{"第一类·Webhook 列表", "webhook +list --owner jiangtx --repo gitlink-cli-demo"},
|
||||
{"第一类·Wiki 页面", "wiki +pages --owner jiangtx --repo gitlink-cli-demo"},
|
||||
{"第一类·标签列表", "label +list --owner jiangtx --repo gitlink-cli-demo"},
|
||||
{"第一类·成员列表", "member +list --owner jiangtx --repo gitlink-cli-demo"},
|
||||
{"第一类·里程碑", "milestone +list --owner jiangtx --repo gitlink-cli-demo"},
|
||||
|
||||
{"第二类·三种输出格式(JSON)", "repo +list --format json"},
|
||||
{"第二类·Table 格式", "repo +list --format table"},
|
||||
{"第二类·调试模式", "issue +list --owner jiangtx --repo gitlink-cli-demo --debug"},
|
||||
|
||||
{"第三类·批量关闭 dry-run", "issue +batch-close --owner jiangtx --repo gitlink-cli-demo --numbers 1,2,3 --dry-run"},
|
||||
{"第三类·批量加成员 dry-run", "member +batch-add --owner jiangtx --repo gitlink-cli-demo --user-ids 101,102 --dry-run"},
|
||||
{"第三类·导出 Issue", "export +issues --owner jiangtx --repo gitlink-cli-demo --format csv --output demo_issues.csv"},
|
||||
|
||||
{"第五类·语言占比", "repo +languages --owner jiangtx --repo gitlink-cli-demo"},
|
||||
{"第五类·贡献者", "repo +contributors --owner jiangtx --repo gitlink-cli-demo"},
|
||||
{"第五类·用户热力图", "user +heatmap --login jiangtx"},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "quick-tour",
|
||||
Description: "5 分钟快速导览(仅核心命令)",
|
||||
Steps: []Step{
|
||||
{"版本", "version"},
|
||||
{"仓库主页 URL", "show repo --owner jiangtx --repo gitlink-cli-demo"},
|
||||
{"Issue 列表", "issue +list --owner jiangtx --repo gitlink-cli-demo"},
|
||||
{"Webhook 列表", "webhook +list --owner jiangtx --repo gitlink-cli-demo"},
|
||||
{"能力探测", "capability +check"},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
func findShowcase(name string) (Showcase, bool) {
|
||||
for _, s := range showcases {
|
||||
if s.Name == name {
|
||||
return s, true
|
||||
}
|
||||
}
|
||||
return Showcase{}, false
|
||||
}
|
||||
|
|
@ -0,0 +1,319 @@
|
|||
package doctor
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/url"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/gitlink-org/gitlink-cli/cmd/cmdutil"
|
||||
internalAuth "github.com/gitlink-org/gitlink-cli/internal/auth"
|
||||
internalConfig "github.com/gitlink-org/gitlink-cli/internal/config"
|
||||
repoContext "github.com/gitlink-org/gitlink-cli/internal/context"
|
||||
"github.com/gitlink-org/gitlink-cli/internal/i18n"
|
||||
"github.com/gitlink-org/gitlink-cli/internal/output"
|
||||
)
|
||||
|
||||
const (
|
||||
statusOK = "ok"
|
||||
statusWarning = "warning"
|
||||
statusError = "error"
|
||||
statusSkipped = "skipped"
|
||||
)
|
||||
|
||||
var (
|
||||
loadConfig = internalConfig.Load
|
||||
loadToken = internalAuth.LoadToken
|
||||
getCurrentUser = internalAuth.GetCurrentUser
|
||||
resolveOwnerRepo = repoContext.ResolveOwnerRepo
|
||||
statFile = os.Stat
|
||||
lookupEnv = os.LookupEnv
|
||||
)
|
||||
|
||||
type Report struct {
|
||||
OK bool `json:"ok"`
|
||||
Summary Summary `json:"summary"`
|
||||
Checks []Check `json:"checks"`
|
||||
Actions []string `json:"actions,omitempty"`
|
||||
}
|
||||
|
||||
type Summary struct {
|
||||
OK int `json:"ok"`
|
||||
Warning int `json:"warning"`
|
||||
Error int `json:"error"`
|
||||
Skipped int `json:"skipped"`
|
||||
Total int `json:"total"`
|
||||
}
|
||||
|
||||
type Check struct {
|
||||
Name string `json:"name"`
|
||||
Status string `json:"status"`
|
||||
Message string `json:"message"`
|
||||
Suggestion string `json:"suggestion,omitempty"`
|
||||
Details map[string]interface{} `json:"details,omitempty"`
|
||||
}
|
||||
|
||||
func NewDoctorCmd(translators ...*i18n.Translator) *cobra.Command {
|
||||
tr := i18n.Default()
|
||||
if len(translators) > 0 && translators[0] != nil {
|
||||
tr = translators[0]
|
||||
}
|
||||
|
||||
var skipNetwork bool
|
||||
cmd := &cobra.Command{
|
||||
Use: "doctor",
|
||||
Short: tr.T("cmd.doctor.short"),
|
||||
Long: tr.T("cmd.doctor.long"),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
report := Run(skipNetwork, tr)
|
||||
return output.PrintTo(cmd.OutOrStdout(), output.SuccessEnvelope(report, nil), resolveFormat())
|
||||
},
|
||||
}
|
||||
cmd.Flags().BoolVar(&skipNetwork, "skip-network", false, tr.T("flag.doctor.skip_network"))
|
||||
return cmd
|
||||
}
|
||||
|
||||
func Run(skipNetwork bool, tr *i18n.Translator) Report {
|
||||
if tr == nil {
|
||||
tr = i18n.Default()
|
||||
}
|
||||
|
||||
checks := make([]Check, 0, 5)
|
||||
cfg, cfgErr := loadConfig()
|
||||
checks = append(checks, checkConfigFile(tr, cfgErr))
|
||||
checks = append(checks, checkConfigValues(tr, cfg, cfgErr))
|
||||
checks = append(checks, checkAuthToken(tr))
|
||||
checks = append(checks, checkRepoContext(tr))
|
||||
checks = append(checks, checkAuthenticatedUser(tr, skipNetwork, cfgErr))
|
||||
|
||||
report := Report{OK: true, Checks: checks}
|
||||
seenActions := map[string]bool{}
|
||||
for _, check := range checks {
|
||||
report.Summary.Total++
|
||||
switch check.Status {
|
||||
case statusOK:
|
||||
report.Summary.OK++
|
||||
case statusWarning:
|
||||
report.Summary.Warning++
|
||||
case statusError:
|
||||
report.OK = false
|
||||
report.Summary.Error++
|
||||
case statusSkipped:
|
||||
report.Summary.Skipped++
|
||||
}
|
||||
if check.Suggestion != "" && !seenActions[check.Suggestion] {
|
||||
report.Actions = append(report.Actions, check.Suggestion)
|
||||
seenActions[check.Suggestion] = true
|
||||
}
|
||||
}
|
||||
return report
|
||||
}
|
||||
|
||||
func checkConfigFile(tr *i18n.Translator, cfgErr error) Check {
|
||||
path := internalConfig.ConfigPath()
|
||||
info, err := statFile(path)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return Check{
|
||||
Name: "config_file",
|
||||
Status: statusWarning,
|
||||
Message: tr.T("output.doctor.config_file.missing"),
|
||||
Suggestion: "gitlink-cli config init",
|
||||
Details: map[string]interface{}{"path": path},
|
||||
}
|
||||
}
|
||||
return Check{
|
||||
Name: "config_file",
|
||||
Status: statusError,
|
||||
Message: tr.Tf("output.doctor.config_file.unreadable", i18n.Args{"message": err.Error()}),
|
||||
Suggestion: tr.T("output.doctor.suggestion.check_config_permissions"),
|
||||
Details: map[string]interface{}{"path": path},
|
||||
}
|
||||
}
|
||||
if cfgErr != nil {
|
||||
return Check{
|
||||
Name: "config_file",
|
||||
Status: statusError,
|
||||
Message: tr.Tf("output.doctor.config_file.invalid", i18n.Args{"message": cfgErr.Error()}),
|
||||
Suggestion: tr.T("output.doctor.suggestion.fix_config_yaml"),
|
||||
Details: map[string]interface{}{"path": path},
|
||||
}
|
||||
}
|
||||
return Check{
|
||||
Name: "config_file",
|
||||
Status: statusOK,
|
||||
Message: tr.T("output.doctor.config_file.ok"),
|
||||
Details: map[string]interface{}{
|
||||
"path": path,
|
||||
"size": info.Size(),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func checkConfigValues(tr *i18n.Translator, cfg *internalConfig.Config, cfgErr error) Check {
|
||||
if cfgErr != nil || cfg == nil {
|
||||
return Check{
|
||||
Name: "config_values",
|
||||
Status: statusSkipped,
|
||||
Message: tr.T("output.doctor.config_values.skipped"),
|
||||
Suggestion: tr.T("output.doctor.suggestion.fix_config_yaml"),
|
||||
}
|
||||
}
|
||||
|
||||
details := map[string]interface{}{
|
||||
"base_url": cfg.BaseURL,
|
||||
"default_format": cfg.Format,
|
||||
}
|
||||
if err := validateBaseURL(cfg.BaseURL); err != nil {
|
||||
return Check{
|
||||
Name: "config_values",
|
||||
Status: statusError,
|
||||
Message: tr.Tf("output.doctor.config_values.bad_base_url", i18n.Args{"message": err.Error()}),
|
||||
Suggestion: "gitlink-cli config set base_url https://www.gitlink.org.cn/api",
|
||||
Details: details,
|
||||
}
|
||||
}
|
||||
if !validFormat(cfg.Format) {
|
||||
return Check{
|
||||
Name: "config_values",
|
||||
Status: statusWarning,
|
||||
Message: tr.Tf("output.doctor.config_values.bad_format", i18n.Args{"format": cfg.Format}),
|
||||
Suggestion: "gitlink-cli config set default_format table",
|
||||
Details: details,
|
||||
}
|
||||
}
|
||||
return Check{
|
||||
Name: "config_values",
|
||||
Status: statusOK,
|
||||
Message: tr.T("output.doctor.config_values.ok"),
|
||||
Details: details,
|
||||
}
|
||||
}
|
||||
|
||||
func checkAuthToken(tr *i18n.Translator) Check {
|
||||
if token, ok := lookupEnv("GITLINK_TOKEN"); ok && strings.TrimSpace(token) != "" {
|
||||
return Check{
|
||||
Name: "auth_token",
|
||||
Status: statusOK,
|
||||
Message: tr.T("output.doctor.auth_token.env"),
|
||||
Details: map[string]interface{}{"source": "env"},
|
||||
}
|
||||
}
|
||||
token, err := loadToken()
|
||||
if err != nil || strings.TrimSpace(token) == "" {
|
||||
return Check{
|
||||
Name: "auth_token",
|
||||
Status: statusWarning,
|
||||
Message: tr.T("output.doctor.auth_token.missing"),
|
||||
Suggestion: "gitlink-cli auth login",
|
||||
}
|
||||
}
|
||||
source := "token"
|
||||
if strings.HasPrefix(token, "cookie:") {
|
||||
source = "cookie"
|
||||
}
|
||||
return Check{
|
||||
Name: "auth_token",
|
||||
Status: statusOK,
|
||||
Message: tr.T("output.doctor.auth_token.stored"),
|
||||
Details: map[string]interface{}{"source": source},
|
||||
}
|
||||
}
|
||||
|
||||
func checkRepoContext(tr *i18n.Translator) Check {
|
||||
owner, repo, err := resolveOwnerRepo(cmdutil.Owner, cmdutil.Repo)
|
||||
if err != nil {
|
||||
return Check{
|
||||
Name: "repo_context",
|
||||
Status: statusWarning,
|
||||
Message: tr.Tf("output.doctor.repo_context.missing", i18n.Args{"message": err.Error()}),
|
||||
Suggestion: tr.T("output.doctor.suggestion.pass_owner_repo"),
|
||||
}
|
||||
}
|
||||
return Check{
|
||||
Name: "repo_context",
|
||||
Status: statusOK,
|
||||
Message: tr.Tf("output.doctor.repo_context.ok", i18n.Args{"owner": owner, "repo": repo}),
|
||||
Details: map[string]interface{}{
|
||||
"owner": owner,
|
||||
"repo": repo,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func checkAuthenticatedUser(tr *i18n.Translator, skipNetwork bool, cfgErr error) Check {
|
||||
if skipNetwork {
|
||||
return Check{
|
||||
Name: "api_auth",
|
||||
Status: statusSkipped,
|
||||
Message: tr.T("output.doctor.api_auth.skipped"),
|
||||
}
|
||||
}
|
||||
if cfgErr != nil {
|
||||
return Check{
|
||||
Name: "api_auth",
|
||||
Status: statusSkipped,
|
||||
Message: tr.T("output.doctor.api_auth.config_skipped"),
|
||||
Suggestion: tr.T("output.doctor.suggestion.fix_config_yaml"),
|
||||
}
|
||||
}
|
||||
|
||||
user, err := getCurrentUser()
|
||||
if err != nil {
|
||||
return Check{
|
||||
Name: "api_auth",
|
||||
Status: statusError,
|
||||
Message: tr.Tf("output.doctor.api_auth.failed", i18n.Args{"message": err.Error()}),
|
||||
Suggestion: "gitlink-cli auth login",
|
||||
}
|
||||
}
|
||||
login, _ := user["login"].(string)
|
||||
if login == "" {
|
||||
return Check{
|
||||
Name: "api_auth",
|
||||
Status: statusWarning,
|
||||
Message: tr.T("output.doctor.api_auth.no_login"),
|
||||
Suggestion: tr.T("output.doctor.suggestion.check_token"),
|
||||
}
|
||||
}
|
||||
return Check{
|
||||
Name: "api_auth",
|
||||
Status: statusOK,
|
||||
Message: tr.Tf("output.doctor.api_auth.ok", i18n.Args{"login": login}),
|
||||
Details: map[string]interface{}{
|
||||
"login": login,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func validateBaseURL(value string) error {
|
||||
u, err := url.Parse(value)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if u.Scheme != "http" && u.Scheme != "https" {
|
||||
return fmt.Errorf("scheme must be http or https")
|
||||
}
|
||||
if u.Host == "" {
|
||||
return fmt.Errorf("host is required")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func validFormat(value string) bool {
|
||||
switch value {
|
||||
case "json", "table", "yaml":
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func resolveFormat() string {
|
||||
if cmdutil.Format != "" {
|
||||
return cmdutil.Format
|
||||
}
|
||||
return "json"
|
||||
}
|
||||
|
|
@ -0,0 +1,200 @@
|
|||
package doctor
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/gitlink-org/gitlink-cli/cmd/cmdutil"
|
||||
"github.com/gitlink-org/gitlink-cli/internal/i18n"
|
||||
)
|
||||
|
||||
func TestDoctorSkipNetworkReportsLocalChecks(t *testing.T) {
|
||||
withDoctorTestState(t)
|
||||
writeConfig(t, "base_url: https://www.gitlink.org.cn/api\ndefault_format: json\n")
|
||||
t.Setenv("GITLINK_TOKEN", "secret-token")
|
||||
resolveOwnerRepo = func(owner, repo string) (string, string, error) {
|
||||
return "Gitlink", "gitlink-cli", nil
|
||||
}
|
||||
|
||||
report := Run(true, i18n.Default())
|
||||
if !report.OK {
|
||||
t.Fatalf("expected report OK, got %+v", report)
|
||||
}
|
||||
assertCheck(t, report, "config_file", statusOK)
|
||||
assertCheck(t, report, "config_values", statusOK)
|
||||
assertCheck(t, report, "auth_token", statusOK)
|
||||
assertCheck(t, report, "repo_context", statusOK)
|
||||
assertCheck(t, report, "api_auth", statusSkipped)
|
||||
if report.Summary.Total != 5 {
|
||||
t.Fatalf("summary total = %d, want 5", report.Summary.Total)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDoctorInvalidConfigDoesNotPanic(t *testing.T) {
|
||||
withDoctorTestState(t)
|
||||
writeConfig(t, "base_url: [broken\n")
|
||||
resolveOwnerRepo = func(owner, repo string) (string, string, error) {
|
||||
return "Gitlink", "gitlink-cli", nil
|
||||
}
|
||||
|
||||
report := Run(true, i18n.Default())
|
||||
if report.OK {
|
||||
t.Fatalf("expected report not OK, got %+v", report)
|
||||
}
|
||||
assertCheck(t, report, "config_file", statusError)
|
||||
assertCheck(t, report, "config_values", statusSkipped)
|
||||
}
|
||||
|
||||
func TestDoctorInvalidBaseURL(t *testing.T) {
|
||||
withDoctorTestState(t)
|
||||
writeConfig(t, "base_url: gitlink.local/api\ndefault_format: table\n")
|
||||
resolveOwnerRepo = func(owner, repo string) (string, string, error) {
|
||||
return "Gitlink", "gitlink-cli", nil
|
||||
}
|
||||
|
||||
report := Run(true, i18n.Default())
|
||||
if report.OK {
|
||||
t.Fatalf("expected invalid base_url to mark report not OK")
|
||||
}
|
||||
check := assertCheck(t, report, "config_values", statusError)
|
||||
if check.Suggestion == "" {
|
||||
t.Fatalf("expected config_values suggestion")
|
||||
}
|
||||
}
|
||||
|
||||
func TestDoctorMissingRepoContextIsWarning(t *testing.T) {
|
||||
withDoctorTestState(t)
|
||||
writeConfig(t, "base_url: https://www.gitlink.org.cn/api\ndefault_format: table\n")
|
||||
resolveOwnerRepo = func(owner, repo string) (string, string, error) {
|
||||
return "", "", errors.New("no origin remote")
|
||||
}
|
||||
|
||||
report := Run(true, i18n.Default())
|
||||
assertCheck(t, report, "auth_token", statusWarning)
|
||||
check := assertCheck(t, report, "repo_context", statusWarning)
|
||||
if check.Suggestion == "" {
|
||||
t.Fatalf("expected repo_context suggestion")
|
||||
}
|
||||
if !report.OK {
|
||||
t.Fatalf("warnings should not make report fail: %+v", report)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDoctorNetworkCheckCanSucceed(t *testing.T) {
|
||||
withDoctorTestState(t)
|
||||
writeConfig(t, "base_url: https://www.gitlink.org.cn/api\ndefault_format: table\n")
|
||||
resolveOwnerRepo = func(owner, repo string) (string, string, error) {
|
||||
return "Gitlink", "gitlink-cli", nil
|
||||
}
|
||||
getCurrentUser = func() (map[string]interface{}, error) {
|
||||
return map[string]interface{}{"login": "Mengz"}, nil
|
||||
}
|
||||
|
||||
report := Run(false, i18n.Default())
|
||||
assertCheck(t, report, "api_auth", statusOK)
|
||||
if !report.OK {
|
||||
t.Fatalf("expected report OK, got %+v", report)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDoctorCommandPrintsJSONEnvelope(t *testing.T) {
|
||||
withDoctorTestState(t)
|
||||
writeConfig(t, "base_url: https://www.gitlink.org.cn/api\ndefault_format: json\n")
|
||||
resolveOwnerRepo = func(owner, repo string) (string, string, error) {
|
||||
return "Gitlink", "gitlink-cli", nil
|
||||
}
|
||||
|
||||
cmd := NewDoctorCmd(i18n.Default())
|
||||
cmd.SetArgs([]string{"--skip-network"})
|
||||
var out bytes.Buffer
|
||||
cmd.SetOut(&out)
|
||||
if err := cmd.Execute(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
var env struct {
|
||||
OK bool `json:"ok"`
|
||||
Data json.RawMessage `json:"data"`
|
||||
}
|
||||
if err := json.Unmarshal(out.Bytes(), &env); err != nil {
|
||||
t.Fatalf("invalid JSON output: %v\n%s", err, out.String())
|
||||
}
|
||||
if !env.OK || len(env.Data) == 0 {
|
||||
t.Fatalf("unexpected envelope: %+v", env)
|
||||
}
|
||||
}
|
||||
|
||||
func withDoctorTestState(t *testing.T) {
|
||||
t.Helper()
|
||||
|
||||
oldLoadConfig := loadConfig
|
||||
oldLoadToken := loadToken
|
||||
oldGetCurrentUser := getCurrentUser
|
||||
oldResolveOwnerRepo := resolveOwnerRepo
|
||||
oldStatFile := statFile
|
||||
oldLookupEnv := lookupEnv
|
||||
oldFormat := cmdutil.Format
|
||||
oldOwner := cmdutil.Owner
|
||||
oldRepo := cmdutil.Repo
|
||||
|
||||
t.Setenv("GITLINK_CONFIG_DIR", t.TempDir())
|
||||
t.Setenv("GITLINK_TOKEN", "")
|
||||
cmdutil.Format = "json"
|
||||
cmdutil.Owner = ""
|
||||
cmdutil.Repo = ""
|
||||
loadConfig = oldLoadConfig
|
||||
loadToken = func() (string, error) { return "", os.ErrNotExist }
|
||||
getCurrentUser = func() (map[string]interface{}, error) {
|
||||
return nil, errors.New("unexpected network call")
|
||||
}
|
||||
resolveOwnerRepo = oldResolveOwnerRepo
|
||||
statFile = oldStatFile
|
||||
lookupEnv = func(key string) (string, bool) {
|
||||
if key == "GITLINK_TOKEN" {
|
||||
value := os.Getenv(key)
|
||||
return value, value != ""
|
||||
}
|
||||
return os.LookupEnv(key)
|
||||
}
|
||||
|
||||
t.Cleanup(func() {
|
||||
loadConfig = oldLoadConfig
|
||||
loadToken = oldLoadToken
|
||||
getCurrentUser = oldGetCurrentUser
|
||||
resolveOwnerRepo = oldResolveOwnerRepo
|
||||
statFile = oldStatFile
|
||||
lookupEnv = oldLookupEnv
|
||||
cmdutil.Format = oldFormat
|
||||
cmdutil.Owner = oldOwner
|
||||
cmdutil.Repo = oldRepo
|
||||
})
|
||||
}
|
||||
|
||||
func writeConfig(t *testing.T, content string) {
|
||||
t.Helper()
|
||||
dir := os.Getenv("GITLINK_CONFIG_DIR")
|
||||
if err := os.MkdirAll(dir, 0700); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.WriteFile(filepath.Join(dir, "config.yaml"), []byte(content), 0600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func assertCheck(t *testing.T, report Report, name, status string) Check {
|
||||
t.Helper()
|
||||
for _, check := range report.Checks {
|
||||
if check.Name == name {
|
||||
if check.Status != status {
|
||||
t.Fatalf("%s status = %s, want %s; check=%+v", name, check.Status, status, check)
|
||||
}
|
||||
return check
|
||||
}
|
||||
}
|
||||
t.Fatalf("missing check %q in %+v", name, report.Checks)
|
||||
return Check{}
|
||||
}
|
||||
15
cmd/root.go
15
cmd/root.go
|
|
@ -7,17 +7,12 @@ import (
|
|||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
aliasCmd "github.com/gitlink-org/gitlink-cli/cmd/alias"
|
||||
apiCmd "github.com/gitlink-org/gitlink-cli/cmd/api"
|
||||
authCmd "github.com/gitlink-org/gitlink-cli/cmd/auth"
|
||||
browseCmd "github.com/gitlink-org/gitlink-cli/cmd/browse"
|
||||
"github.com/gitlink-org/gitlink-cli/cmd/cmdutil"
|
||||
configCmd "github.com/gitlink-org/gitlink-cli/cmd/config"
|
||||
demoCmd "github.com/gitlink-org/gitlink-cli/cmd/demo"
|
||||
showCmd "github.com/gitlink-org/gitlink-cli/cmd/show"
|
||||
statusCmd "github.com/gitlink-org/gitlink-cli/cmd/status"
|
||||
doctorCmd "github.com/gitlink-org/gitlink-cli/cmd/doctor"
|
||||
internalConfig "github.com/gitlink-org/gitlink-cli/internal/config"
|
||||
"github.com/gitlink-org/gitlink-cli/internal/demo"
|
||||
"github.com/gitlink-org/gitlink-cli/internal/i18n"
|
||||
"github.com/gitlink-org/gitlink-cli/shortcuts"
|
||||
)
|
||||
|
|
@ -57,19 +52,13 @@ func NewRootCmd(opts RootOptions, tr *i18n.Translator) (*cobra.Command, error) {
|
|||
rootCmd.PersistentFlags().StringVar(&cmdutil.Repo, "repo", "", tr.T("flag.repo"))
|
||||
rootCmd.PersistentFlags().StringVar(&cmdutil.Format, "format", "", tr.T("flag.format"))
|
||||
rootCmd.PersistentFlags().BoolVar(&cmdutil.Debug, "debug", false, tr.T("flag.debug"))
|
||||
rootCmd.PersistentFlags().BoolVar(&cmdutil.Web, "web", false, "执行后打印并打开对应 GitLink 网页 URL")
|
||||
rootCmd.PersistentFlags().BoolVar(&demo.DemoFlag, "demo", false, "演示模式:使用内置 Mock 数据,无需网络和 Token")
|
||||
rootCmd.PersistentFlags().StringVar(&cmdutil.Lang, "lang", "", tr.T("flag.lang"))
|
||||
|
||||
rootCmd.AddCommand(authCmd.NewAuthCmd(tr))
|
||||
rootCmd.AddCommand(apiCmd.NewAPICmd(tr))
|
||||
rootCmd.AddCommand(configCmd.NewConfigCmd(tr))
|
||||
rootCmd.AddCommand(doctorCmd.NewDoctorCmd(tr))
|
||||
rootCmd.AddCommand(newVersionCmd(version, tr))
|
||||
rootCmd.AddCommand(aliasCmd.NewAliasCmd())
|
||||
rootCmd.AddCommand(browseCmd.NewBrowseCmd())
|
||||
rootCmd.AddCommand(statusCmd.NewStatusCmd())
|
||||
rootCmd.AddCommand(showCmd.NewShowCmd())
|
||||
rootCmd.AddCommand(demoCmd.NewDemoCmd())
|
||||
|
||||
shortcuts.RegisterAll(rootCmd, tr)
|
||||
|
||||
|
|
|
|||
292
cmd/show/show.go
292
cmd/show/show.go
|
|
@ -1,292 +0,0 @@
|
|||
// Package show implements `gitlink-cli show <resource>`: print the GitLink web
|
||||
// URL for a resource without opening a browser. It is the projector-friendly
|
||||
// twin of `browse` — exactly the same URL table (via internal/web.Builder),
|
||||
// but it prints instead of launching.
|
||||
//
|
||||
// Output convention:
|
||||
// - default (no --format): a single human-readable line "🔗 <URL>"
|
||||
// - --format json/table/yaml: a standard output envelope
|
||||
//
|
||||
// This dual mode lets the demo script `gitlink-cli show repo` render a tidy
|
||||
// URL on slides while `show repo --format json` stays machine-parsable for AI
|
||||
// Agents and verify.sh.
|
||||
package show
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/gitlink-org/gitlink-cli/cmd/cmdutil"
|
||||
"github.com/gitlink-org/gitlink-cli/internal/context"
|
||||
"github.com/gitlink-org/gitlink-cli/internal/output"
|
||||
"github.com/gitlink-org/gitlink-cli/internal/web"
|
||||
)
|
||||
|
||||
// stdout is the emit target. Package-level so tests can redirect it.
|
||||
var stdout io.Writer = os.Stdout
|
||||
|
||||
// NewShowCmd builds the `show` command tree.
|
||||
func NewShowCmd() *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "show",
|
||||
Short: "打印 GitLink 网页 URL(不打开浏览器)",
|
||||
Long: `打印资源对应的 GitLink 网页 URL,用于投影演示。
|
||||
|
||||
默认输出一行 🔗 URL;加 --format json 输出结构化 envelope(供 AI Agent / 脚本)。
|
||||
owner/repo 自动从 git remote 推断,或用 --owner/--repo 指定。
|
||||
|
||||
示例:
|
||||
gitlink-cli show repo
|
||||
gitlink-cli show issue --number 42
|
||||
gitlink-cli show pr --number 128
|
||||
gitlink-cli show wiki --page "API 指南"
|
||||
gitlink-cli show webhook
|
||||
gitlink-cli show user --login jiangtx`,
|
||||
}
|
||||
cmd.AddCommand(
|
||||
newRepoCmd(),
|
||||
newIssueCmd(),
|
||||
newPRCmd(),
|
||||
newWikiCmd(),
|
||||
newMemberCmd(),
|
||||
newWebhookCmd(),
|
||||
newLabelCmd(),
|
||||
newMilestoneCmd(),
|
||||
newBranchCmd(),
|
||||
newReleaseCmd(),
|
||||
newCommitCmd(),
|
||||
newCICmd(),
|
||||
newCompareCmd(),
|
||||
newOrgCmd(),
|
||||
newUserCmd(),
|
||||
newNotificationCmd(),
|
||||
)
|
||||
return cmd
|
||||
}
|
||||
|
||||
// emit prints r in the configured format. Empty format → friendly single line.
|
||||
func emit(r *web.ResourceURL) error {
|
||||
if cmdutil.Format == "" {
|
||||
fmt.Fprintf(stdout, "🔗 %s\n", r.URL)
|
||||
return nil
|
||||
}
|
||||
return output.PrintTo(stdout, output.SuccessEnvelope(r, nil), cmdutil.Format)
|
||||
}
|
||||
|
||||
// resolveOwnerRepo reads global --owner/--repo and falls back to git remote.
|
||||
func resolveOwnerRepo() (string, string, error) {
|
||||
return context.ResolveOwnerRepo(cmdutil.Owner, cmdutil.Repo)
|
||||
}
|
||||
|
||||
// ownerRepoCmd builds a subcommand that only needs owner/repo.
|
||||
func ownerRepoCmd(use, short string, build func(b *web.Builder, owner, repo string) *web.ResourceURL) *cobra.Command {
|
||||
return &cobra.Command{
|
||||
Use: use,
|
||||
Short: short,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
owner, repo, err := resolveOwnerRepo()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return emit(build(web.NewBuilder(), owner, repo))
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func newRepoCmd() *cobra.Command {
|
||||
return ownerRepoCmd("repo", "仓库主页 URL",
|
||||
func(b *web.Builder, o, r string) *web.ResourceURL { return b.RepoURL(o, r) })
|
||||
}
|
||||
|
||||
func newIssueCmd() *cobra.Command {
|
||||
var number int
|
||||
cmd := &cobra.Command{
|
||||
Use: "issue",
|
||||
Short: "Issue 网页 URL",
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
owner, repo, err := resolveOwnerRepo()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return emit(web.NewBuilder().IssueURL(owner, repo, number))
|
||||
},
|
||||
}
|
||||
cmd.Flags().IntVarP(&number, "number", "n", 0, "Issue 编号(省略则输出 Issue 列表页)")
|
||||
return cmd
|
||||
}
|
||||
|
||||
func newPRCmd() *cobra.Command {
|
||||
var number int
|
||||
cmd := &cobra.Command{
|
||||
Use: "pr",
|
||||
Short: "Pull Request 网页 URL",
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
owner, repo, err := resolveOwnerRepo()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return emit(web.NewBuilder().PRURL(owner, repo, number))
|
||||
},
|
||||
}
|
||||
cmd.Flags().IntVarP(&number, "number", "n", 0, "PR 编号(省略则输出 PR 列表页)")
|
||||
return cmd
|
||||
}
|
||||
|
||||
func newWikiCmd() *cobra.Command {
|
||||
var page string
|
||||
cmd := &cobra.Command{
|
||||
Use: "wiki",
|
||||
Short: "Wiki 网页 URL",
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
owner, repo, err := resolveOwnerRepo()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return emit(web.NewBuilder().WikiURL(owner, repo, page))
|
||||
},
|
||||
}
|
||||
cmd.Flags().StringVarP(&page, "page", "p", "", "Wiki 页面名(省略则输出 Wiki 首页)")
|
||||
return cmd
|
||||
}
|
||||
|
||||
func newMemberCmd() *cobra.Command {
|
||||
return ownerRepoCmd("member", "成员管理页 URL",
|
||||
func(b *web.Builder, o, r string) *web.ResourceURL { return b.MemberURL(o, r) })
|
||||
}
|
||||
|
||||
func newWebhookCmd() *cobra.Command {
|
||||
return ownerRepoCmd("webhook", "Webhook 设置页 URL",
|
||||
func(b *web.Builder, o, r string) *web.ResourceURL { return b.WebhookURL(o, r) })
|
||||
}
|
||||
|
||||
func newLabelCmd() *cobra.Command {
|
||||
return ownerRepoCmd("label", "标签管理页 URL",
|
||||
func(b *web.Builder, o, r string) *web.ResourceURL { return b.LabelURL(o, r) })
|
||||
}
|
||||
|
||||
func newMilestoneCmd() *cobra.Command {
|
||||
return ownerRepoCmd("milestone", "里程碑页 URL",
|
||||
func(b *web.Builder, o, r string) *web.ResourceURL { return b.MilestoneURL(o, r) })
|
||||
}
|
||||
|
||||
func newBranchCmd() *cobra.Command {
|
||||
var branch string
|
||||
cmd := &cobra.Command{
|
||||
Use: "branch",
|
||||
Short: "分支页 URL",
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
owner, repo, err := resolveOwnerRepo()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return emit(web.NewBuilder().BranchURL(owner, repo, branch))
|
||||
},
|
||||
}
|
||||
cmd.Flags().StringVarP(&branch, "branch", "b", "", "分支名(省略则输出分支列表页)")
|
||||
return cmd
|
||||
}
|
||||
|
||||
func newReleaseCmd() *cobra.Command {
|
||||
var tag string
|
||||
cmd := &cobra.Command{
|
||||
Use: "release",
|
||||
Short: "Release 页 URL",
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
owner, repo, err := resolveOwnerRepo()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return emit(web.NewBuilder().ReleaseURL(owner, repo, tag))
|
||||
},
|
||||
}
|
||||
cmd.Flags().StringVarP(&tag, "tag", "t", "", "Release tag(省略则输出 Release 列表页)")
|
||||
return cmd
|
||||
}
|
||||
|
||||
func newCommitCmd() *cobra.Command {
|
||||
var sha string
|
||||
cmd := &cobra.Command{
|
||||
Use: "commit",
|
||||
Short: "提交详情页 URL",
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
owner, repo, err := resolveOwnerRepo()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return emit(web.NewBuilder().CommitURL(owner, repo, sha))
|
||||
},
|
||||
}
|
||||
cmd.Flags().StringVarP(&sha, "sha", "s", "", "提交 SHA(省略则输出提交列表页)")
|
||||
return cmd
|
||||
}
|
||||
|
||||
func newCICmd() *cobra.Command {
|
||||
return ownerRepoCmd("ci", "CI/Actions 页 URL",
|
||||
func(b *web.Builder, o, r string) *web.ResourceURL { return b.CIURL(o, r) })
|
||||
}
|
||||
|
||||
func newCompareCmd() *cobra.Command {
|
||||
var base, head string
|
||||
cmd := &cobra.Command{
|
||||
Use: "compare",
|
||||
Short: "分支对比页 URL",
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
if base == "" || head == "" {
|
||||
return fmt.Errorf("--base 和 --head 均为必填")
|
||||
}
|
||||
owner, repo, err := resolveOwnerRepo()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return emit(web.NewBuilder().CompareURL(owner, repo, base, head))
|
||||
},
|
||||
}
|
||||
cmd.Flags().StringVar(&base, "base", "", "基准分支/tag")
|
||||
cmd.Flags().StringVar(&head, "head", "", "目标分支/tag")
|
||||
return cmd
|
||||
}
|
||||
|
||||
func newOrgCmd() *cobra.Command {
|
||||
var name string
|
||||
cmd := &cobra.Command{
|
||||
Use: "org",
|
||||
Short: "组织页 URL",
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
if name == "" {
|
||||
return fmt.Errorf("--name 为必填")
|
||||
}
|
||||
return emit(web.NewBuilder().OrgURL(name))
|
||||
},
|
||||
}
|
||||
cmd.Flags().StringVar(&name, "name", "", "组织名")
|
||||
return cmd
|
||||
}
|
||||
|
||||
func newUserCmd() *cobra.Command {
|
||||
var login string
|
||||
cmd := &cobra.Command{
|
||||
Use: "user",
|
||||
Short: "用户主页 URL",
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
if login == "" {
|
||||
return fmt.Errorf("--login 为必填")
|
||||
}
|
||||
return emit(web.NewBuilder().UserURL(login))
|
||||
},
|
||||
}
|
||||
cmd.Flags().StringVar(&login, "login", "", "用户登录名")
|
||||
return cmd
|
||||
}
|
||||
|
||||
func newNotificationCmd() *cobra.Command {
|
||||
return &cobra.Command{
|
||||
Use: "notification",
|
||||
Short: "通知中心 URL",
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
return emit(web.NewBuilder().NotificationURL())
|
||||
},
|
||||
}
|
||||
}
|
||||
|
|
@ -1,214 +0,0 @@
|
|||
package show
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/gitlink-org/gitlink-cli/cmd/cmdutil"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
// newShowWithFlags builds a show tree with owner/repo/format persistent flags
|
||||
// that normally come from the parent rootCmd, so subcommands can parse them in
|
||||
// isolation during tests.
|
||||
func newShowWithFlags() *cobra.Command {
|
||||
cmdutil.Owner = ""
|
||||
cmdutil.Repo = ""
|
||||
cmdutil.Format = ""
|
||||
r := NewShowCmd()
|
||||
r.PersistentFlags().StringVar(&cmdutil.Owner, "owner", "", "")
|
||||
r.PersistentFlags().StringVar(&cmdutil.Repo, "repo", "", "")
|
||||
r.PersistentFlags().StringVar(&cmdutil.Format, "format", "", "")
|
||||
return r
|
||||
}
|
||||
|
||||
// runShow executes `show <args>` with captured stdout and returns the output.
|
||||
func runShow(t *testing.T, args ...string) string {
|
||||
t.Helper()
|
||||
oldOut := stdout
|
||||
oldFmt := cmdutil.Format
|
||||
buf := &bytes.Buffer{}
|
||||
stdout = buf
|
||||
cmdutil.Format = ""
|
||||
defer func() {
|
||||
stdout = oldOut
|
||||
cmdutil.Format = oldFmt
|
||||
}()
|
||||
root := newShowWithFlags()
|
||||
root.SetArgs(args)
|
||||
if err := root.Execute(); err != nil {
|
||||
t.Fatalf("show %v: %v", args, err)
|
||||
}
|
||||
return buf.String()
|
||||
}
|
||||
|
||||
func TestShowSubcommandsRegistered(t *testing.T) {
|
||||
root := NewShowCmd()
|
||||
want := []string{
|
||||
"repo", "issue", "pr", "wiki", "member", "webhook", "label",
|
||||
"milestone", "branch", "release", "commit", "ci", "compare",
|
||||
"org", "user", "notification",
|
||||
}
|
||||
got := map[string]bool{}
|
||||
for _, c := range root.Commands() {
|
||||
got[c.Name()] = true
|
||||
}
|
||||
for _, w := range want {
|
||||
if !got[w] {
|
||||
t.Errorf("subcommand %q not registered", w)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestShowRepoDefault(t *testing.T) {
|
||||
out := runShow(t, "repo", "--owner", "jiangtx", "--repo", "gitlink-cli-demo")
|
||||
if !strings.Contains(out, "🔗") {
|
||||
t.Errorf("missing 🔗 marker: %q", out)
|
||||
}
|
||||
if !strings.Contains(out, "https://gitlink.org.cn/jiangtx/gitlink-cli-demo") {
|
||||
t.Errorf("wrong URL: %q", out)
|
||||
}
|
||||
}
|
||||
|
||||
func TestShowIssueWithNumber(t *testing.T) {
|
||||
out := runShow(t, "issue", "--owner", "jiangtx", "--repo", "demo", "--number", "42")
|
||||
if !strings.Contains(out, "/issues/42") {
|
||||
t.Errorf("expected /issues/42 in: %q", out)
|
||||
}
|
||||
}
|
||||
|
||||
func TestShowIssueListWhenNoNumber(t *testing.T) {
|
||||
out := runShow(t, "issue", "--owner", "jiangtx", "--repo", "demo")
|
||||
if !strings.HasSuffix(strings.TrimSpace(out), "/issues") {
|
||||
t.Errorf("expected /issues suffix: %q", out)
|
||||
}
|
||||
}
|
||||
|
||||
func TestShowPR(t *testing.T) {
|
||||
out := runShow(t, "pr", "--owner", "o", "--repo", "r", "--number", "128")
|
||||
if !strings.Contains(out, "/pulls/128") {
|
||||
t.Errorf("expected /pulls/128: %q", out)
|
||||
}
|
||||
}
|
||||
|
||||
func TestShowWiki(t *testing.T) {
|
||||
out := runShow(t, "wiki", "--owner", "o", "--repo", "r", "--page", "API 指南")
|
||||
if !strings.Contains(out, "/wiki/") {
|
||||
t.Errorf("expected /wiki/ segment: %q", out)
|
||||
}
|
||||
}
|
||||
|
||||
func TestShowWebhook(t *testing.T) {
|
||||
out := runShow(t, "webhook", "--owner", "o", "--repo", "r")
|
||||
if !strings.Contains(out, "/settings/hooks") {
|
||||
t.Errorf("expected /settings/hooks: %q", out)
|
||||
}
|
||||
}
|
||||
|
||||
func TestShowMember(t *testing.T) {
|
||||
out := runShow(t, "member", "--owner", "o", "--repo", "r")
|
||||
if !strings.Contains(out, "/settings/collaboration") {
|
||||
t.Errorf("expected collaboration URL: %q", out)
|
||||
}
|
||||
}
|
||||
|
||||
func TestShowLabel(t *testing.T) {
|
||||
out := runShow(t, "label", "--owner", "o", "--repo", "r")
|
||||
if !strings.Contains(out, "/issues/labels") {
|
||||
t.Errorf("expected labels URL: %q", out)
|
||||
}
|
||||
}
|
||||
|
||||
func TestShowMilestone(t *testing.T) {
|
||||
out := runShow(t, "milestone", "--owner", "o", "--repo", "r")
|
||||
if !strings.Contains(out, "/milestones") {
|
||||
t.Errorf("expected milestones URL: %q", out)
|
||||
}
|
||||
}
|
||||
|
||||
func TestShowBranch(t *testing.T) {
|
||||
out := runShow(t, "branch", "--owner", "o", "--repo", "r", "--branch", "feat/x")
|
||||
if !strings.Contains(out, "/branches/") {
|
||||
t.Errorf("expected /branches/ segment: %q", out)
|
||||
}
|
||||
}
|
||||
|
||||
func TestShowRelease(t *testing.T) {
|
||||
out := runShow(t, "release", "--owner", "o", "--repo", "r", "--tag", "v2.0")
|
||||
if !strings.Contains(out, "/releases/v2.0") {
|
||||
t.Errorf("expected /releases/v2.0: %q", out)
|
||||
}
|
||||
}
|
||||
|
||||
func TestShowCommit(t *testing.T) {
|
||||
out := runShow(t, "commit", "--owner", "o", "--repo", "r", "--sha", "abc123")
|
||||
if !strings.Contains(out, "/commits/abc123") {
|
||||
t.Errorf("expected /commits/abc123: %q", out)
|
||||
}
|
||||
}
|
||||
|
||||
func TestShowCI(t *testing.T) {
|
||||
out := runShow(t, "ci", "--owner", "o", "--repo", "r")
|
||||
if !strings.Contains(out, "/actions") {
|
||||
t.Errorf("expected /actions: %q", out)
|
||||
}
|
||||
}
|
||||
|
||||
func TestShowCompare(t *testing.T) {
|
||||
out := runShow(t, "compare", "--owner", "o", "--repo", "r", "--base", "master", "--head", "dev")
|
||||
if !strings.Contains(out, "/compare/master...dev") {
|
||||
t.Errorf("expected compare URL: %q", out)
|
||||
}
|
||||
}
|
||||
|
||||
func TestShowOrg(t *testing.T) {
|
||||
out := runShow(t, "org", "--name", "ccf")
|
||||
if !strings.HasSuffix(strings.TrimSpace(out), "/ccf") {
|
||||
t.Errorf("expected /ccf suffix: %q", out)
|
||||
}
|
||||
}
|
||||
|
||||
func TestShowUser(t *testing.T) {
|
||||
out := runShow(t, "user", "--login", "jiangtx")
|
||||
if !strings.HasSuffix(strings.TrimSpace(out), "/jiangtx") {
|
||||
t.Errorf("expected /jiangtx suffix: %q", out)
|
||||
}
|
||||
}
|
||||
|
||||
func TestShowNotification(t *testing.T) {
|
||||
out := runShow(t, "notification")
|
||||
if !strings.Contains(out, "/notifications") {
|
||||
t.Errorf("expected /notifications: %q", out)
|
||||
}
|
||||
}
|
||||
|
||||
func TestShowJSONFormat(t *testing.T) {
|
||||
// JSON mode must produce a parseable envelope containing html_url.
|
||||
oldOut := stdout
|
||||
buf := &bytes.Buffer{}
|
||||
stdout = buf
|
||||
defer func() { stdout = oldOut }()
|
||||
|
||||
root := newShowWithFlags()
|
||||
cmdutil.Format = "json"
|
||||
root.SetArgs([]string{"repo", "--owner", "o", "--repo", "r"})
|
||||
if err := root.Execute(); err != nil {
|
||||
t.Fatalf("execute: %v", err)
|
||||
}
|
||||
if !strings.Contains(buf.String(), `"html_url"`) {
|
||||
t.Errorf("JSON missing html_url: %q", buf.String())
|
||||
}
|
||||
if !strings.Contains(buf.String(), `"ok": true`) {
|
||||
t.Errorf("JSON missing ok:true: %q", buf.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestShowCompareRequiresBothFlags(t *testing.T) {
|
||||
root := newShowWithFlags()
|
||||
root.SetArgs([]string{"compare", "--owner", "o", "--repo", "r", "--base", "master"})
|
||||
err := root.Execute()
|
||||
if err == nil {
|
||||
t.Fatal("expected error when --head missing")
|
||||
}
|
||||
}
|
||||
|
|
@ -1,68 +0,0 @@
|
|||
package status
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/gitlink-org/gitlink-cli/internal/auth"
|
||||
"github.com/gitlink-org/gitlink-cli/internal/config"
|
||||
"github.com/gitlink-org/gitlink-cli/internal/context"
|
||||
)
|
||||
|
||||
// NewStatusCmd creates the status command that displays login state and context.
|
||||
func NewStatusCmd() *cobra.Command {
|
||||
return &cobra.Command{
|
||||
Use: "status",
|
||||
Short: "显示当前登录状态和上下文信息",
|
||||
Long: `显示 gitlink-cli 的当前状态,包括:
|
||||
- 认证状态(是否已登录、Token 来源)
|
||||
- API 地址
|
||||
- 当前目录
|
||||
- 自动推断的仓库信息`,
|
||||
Example: ` gitlink-cli status`,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
cfg, _ := config.Load()
|
||||
token, _ := auth.LoadToken()
|
||||
if token == "" {
|
||||
token = os.Getenv("GITLINK_TOKEN")
|
||||
}
|
||||
cwd, _ := os.Getwd()
|
||||
|
||||
fmt.Println("GitLink CLI 状态")
|
||||
fmt.Println("───────────────")
|
||||
|
||||
// 认证状态
|
||||
if token != "" {
|
||||
fmt.Println(" 认证状态: 已登录")
|
||||
fmt.Printf(" Token 来源: %s\n", tokenSource(token))
|
||||
} else {
|
||||
fmt.Println(" 认证状态: 未登录(运行 gitlink-cli auth login)")
|
||||
}
|
||||
|
||||
// API 地址
|
||||
fmt.Printf(" API 地址: %s\n", cfg.BaseURL)
|
||||
|
||||
// 当前目录
|
||||
fmt.Printf(" 当前目录: %s\n", cwd)
|
||||
|
||||
// 推断的仓库
|
||||
owner, repo, err := context.ResolveOwnerRepo("", "")
|
||||
if err == nil {
|
||||
fmt.Printf(" 推断仓库: %s/%s\n", owner, repo)
|
||||
} else {
|
||||
fmt.Println(" 推断仓库: (不在 Git 仓库中)")
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func tokenSource(token string) string {
|
||||
if token == os.Getenv("GITLINK_TOKEN") {
|
||||
return "环境变量 GITLINK_TOKEN"
|
||||
}
|
||||
return "keyring / 配置文件"
|
||||
}
|
||||
|
|
@ -1,58 +0,0 @@
|
|||
package status
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestNewStatusCmd(t *testing.T) {
|
||||
cmd := NewStatusCmd()
|
||||
if cmd.Use != "status" {
|
||||
t.Errorf("expected Use 'status', got %s", cmd.Use)
|
||||
}
|
||||
if cmd.Short == "" {
|
||||
t.Error("Short description should not be empty")
|
||||
}
|
||||
if cmd.Long == "" {
|
||||
t.Error("Long description should not be empty")
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewStatusCmdExample(t *testing.T) {
|
||||
cmd := NewStatusCmd()
|
||||
if !strings.Contains(cmd.Example, "status") {
|
||||
t.Errorf("Example should contain 'status', got: %s", cmd.Example)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewStatusCmdHasNoSubcommands(t *testing.T) {
|
||||
cmd := NewStatusCmd()
|
||||
if cmd.HasSubCommands() {
|
||||
t.Error("status should not have subcommands")
|
||||
}
|
||||
}
|
||||
|
||||
func TestTokenSourceFromEnv(t *testing.T) {
|
||||
t.Setenv("GITLINK_TOKEN", "test-token-123")
|
||||
result := tokenSource("test-token-123")
|
||||
if result != "环境变量 GITLINK_TOKEN" {
|
||||
t.Errorf("expected env source, got: %s", result)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTokenSourceFromKeyring(t *testing.T) {
|
||||
// 不设置环境变量,或用不同的值
|
||||
t.Setenv("GITLINK_TOKEN", "")
|
||||
result := tokenSource("some-stored-token")
|
||||
if result != "keyring / 配置文件" {
|
||||
t.Errorf("expected keyring source, got: %s", result)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTokenSourceMismatch(t *testing.T) {
|
||||
t.Setenv("GITLINK_TOKEN", "env-token")
|
||||
result := tokenSource("different-token")
|
||||
if result != "keyring / 配置文件" {
|
||||
t.Errorf("should fallback to keyring when token differs from env, got: %s", result)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,2 +0,0 @@
|
|||
id,login,contributions
|
||||
<nil>,<nil>,<nil>
|
||||
|
|
|
@ -1,387 +0,0 @@
|
|||
# 子任务一:终端↔网页联动演示方案
|
||||
|
||||
> 对齐 `课程实践任务及要求 - 0613.pdf` 子任务一的五大类别
|
||||
>
|
||||
> 核心思路:**终端执行命令 → `show` 输出网页 URL → 投影仪打开网页 → 听众直观验证**
|
||||
|
||||
---
|
||||
|
||||
## 演示路线图(~20 min)
|
||||
|
||||
```
|
||||
开场 (2 min) 安装 + 认证 + 全景概览
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
第一类 (4 min) 新增 Shortcut 命令
|
||||
Wiki/Webhook/PM/Pipeline/Label/Member/Milestone
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
第二类 (3 min) 优化现有命令
|
||||
多格式输出/自动上下文/--debug/中文帮助
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
第三类 (3 min) 批量操作能力
|
||||
batch-close/batch-add/export/dry-run
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
第四类 (3 min) 跨平台兼容 + 安装体验
|
||||
5种安装/npm postinstall/keyring多平台
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
第五类 (3 min) 补全 Raw API 封装
|
||||
HTML检测/端点→Shortcut映射/Skills修复
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
收尾 (2 min) Capability检测 + 覆盖率 + 验收清单
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 第一类:新增 Shortcut 命令(4 min)
|
||||
|
||||
> **覆盖**: Wiki 管理 · Webhook 配置 · 项目看板 · 流水线 · 标签 · 成员 · 里程碑
|
||||
>
|
||||
> **话术**: "扩展前这些模块完全不存在。扩展后每个都是完整的 CRUD + 列表查询。"
|
||||
|
||||
### A. Webhook 管理 — 从无到有的完整 CRUD
|
||||
|
||||
```bash
|
||||
# ── 终端 Step 1: 创建 Webhook ──
|
||||
gitlink-cli webhook +create --owner jiangtx --repo gitlink-cli-demo \
|
||||
--url https://my-ci.gitlink.org.cn/hook --events push,create
|
||||
# → ✓ Webhook 创建成功 (id: 51348)
|
||||
|
||||
# ── 终端 Step 2: 列出所有 Webhook ──
|
||||
gitlink-cli webhook +list --owner jiangtx --repo gitlink-cli-demo
|
||||
# → { "ok": true, "data": { "total_count": 1, "webhooks": [...] } }
|
||||
|
||||
# ── 终端 Step 3: 获取网页链接 ──
|
||||
gitlink-cli show webhook
|
||||
# → 🔗 https://gitlink.org.cn/jiangtx/gitlink-cli-demo/settings/hooks
|
||||
|
||||
# 【投影仪】打开 Webhook 设置页面,展示刚才创建的 Webhook
|
||||
```
|
||||
|
||||
```
|
||||
终端输出 网页对应
|
||||
──────── ────────
|
||||
webhook +create 成功 → settings/hooks 页面显示新增条目
|
||||
webhook +list (JSON) → settings/hooks 列表视图
|
||||
webhook +test --id 51348 → settings/hooks/51348 测试结果
|
||||
```
|
||||
|
||||
### B. Wiki 管理 — 文档的 CLI 操作
|
||||
|
||||
```bash
|
||||
gitlink-cli wiki +pages --owner jiangtx --repo gitlink-cli-demo
|
||||
# → 列出所有 Wiki 页面
|
||||
|
||||
gitlink-cli wiki +create --owner jiangtx --repo gitlink-cli-demo \
|
||||
--title "API 使用指南" --content "# 快速开始\n\n## 安装\n..."
|
||||
# → ✓ Wiki 页面创建成功
|
||||
|
||||
gitlink-cli show wiki --page "API 使用指南"
|
||||
# → 🔗 https://gitlink.org.cn/jiangtx/gitlink-cli-demo/wiki/API%20使用指南
|
||||
# 【投影仪】打开 Wiki 页面,展示刚创建的内容
|
||||
```
|
||||
|
||||
### C. 项目管理(PM) + Pipeline + Label + Member + Milestone
|
||||
|
||||
```bash
|
||||
# PM — 看板
|
||||
gitlink-cli pm +dashboards --project 1 → show repo --tab projects
|
||||
# Pipeline — 流水线
|
||||
gitlink-cli pipeline +runs --owner jiangtx --repo gitlink-cli-demo --ref master --workflow build.yml
|
||||
gitlink-cli show ci → .../actions (流水线页面)
|
||||
# Label — 标签
|
||||
gitlink-cli label +create -n "P0" -c "#FF0000" --owner jiangtx --repo gitlink-cli-demo
|
||||
gitlink-cli show label → .../issues/labels
|
||||
# Member — 成员
|
||||
gitlink-cli member +list --owner jiangtx --repo gitlink-cli-demo
|
||||
gitlink-cli show member → .../settings/collaboration
|
||||
# Milestone — 里程碑
|
||||
gitlink-cli milestone +create --owner jiangtx --repo gitlink-cli-demo --title "Sprint 6"
|
||||
gitlink-cli show milestone → .../milestones
|
||||
```
|
||||
|
||||
**第一类总计:7 个新模块 × 每条命令对应网页 = 7 组终端↔网页映射**
|
||||
|
||||
---
|
||||
|
||||
## 第二类:优化现有命令(3 min)
|
||||
|
||||
> **话术**: "不是新增,而是让现有命令更好用——三种输出格式、自动上下文推断、调试模式、中文帮助和错误提示。"
|
||||
|
||||
### A. 三种输出格式 — 同一命令,不同场景
|
||||
|
||||
```bash
|
||||
# ── 终端: JSON 格式(给脚本和 AI Agent) ──
|
||||
gitlink-cli repo +list --format json
|
||||
gitlink-cli show repo → 🔗 https://gitlink.org.cn/jiangtx/gitlink-cli-demo
|
||||
|
||||
# ── 终端: Table 格式(给人看) ──
|
||||
gitlink-cli repo +list --format table
|
||||
|
||||
# ── 终端: YAML 格式(给配置文件) ──
|
||||
gitlink-cli repo +list --format yaml
|
||||
# 【投影仪】GitLink 仓库列表页面,展示了相同的数据
|
||||
```
|
||||
|
||||
### B. 自动上下文推断 — 零参数即可用
|
||||
|
||||
```bash
|
||||
cd ~/gitlink-cli-demo
|
||||
gitlink-cli issue +list
|
||||
# → 自动从 git remote origin 解析 → jiangtx/gitlink-cli-demo
|
||||
gitlink-cli show repo → 🔗 当前仓库主页
|
||||
```
|
||||
|
||||
### C. 调试模式 — 完整请求链路可视化
|
||||
|
||||
```bash
|
||||
gitlink-cli issue +list --debug
|
||||
# → [DEBUG] GET https://gitlink.org.cn/api/v1/jiangtx/gitlink-cli-demo/issues.json?state=open&page=1&limit=20
|
||||
# → [DEBUG] Authorization: Bearer ***
|
||||
# → [DEBUG] Response 200 OK (234ms)
|
||||
```
|
||||
|
||||
### D. 中文帮助 — 每个命令都有 API 端点 + 使用示例
|
||||
|
||||
```bash
|
||||
gitlink-cli repo +languages --help
|
||||
# → 中文描述 + API 端点: GET /:owner/:repo/languages + 使用示例
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 第三类:批量操作能力(3 min)
|
||||
|
||||
> **话术**: "扩展前只能一个一个操作。扩展后支持批量、CSV文件导入、dry-run 安全预览。"
|
||||
|
||||
### A. 批量 Issue 操作
|
||||
|
||||
```bash
|
||||
# ── 终端: dry-run 安全预览 ──
|
||||
gitlink-cli issue +batch-close --owner jiangtx --repo gitlink-cli-demo \
|
||||
--numbers 1,2,3,4,5 --dry-run
|
||||
# → [DRY RUN] 将关闭以下 Issue: #1, #2, #3, #4, #5
|
||||
|
||||
# ── 终端: 确认执行 ──
|
||||
gitlink-cli issue +batch-close --owner jiangtx --repo gitlink-cli-demo \
|
||||
--numbers 1,2,3,4,5
|
||||
# → ✓ 已关闭 5 个 Issue
|
||||
|
||||
# ── 终端: 查看网页验证 ──
|
||||
gitlink-cli show repo --tab issues?state=closed
|
||||
# → 🔗 https://gitlink.org.cn/jiangtx/gitlink-cli-demo/issues?state=closed
|
||||
|
||||
# 【投影仪】浏览器显示已关闭的 Issue 列表,5个Issue 全部状态为 Closed
|
||||
```
|
||||
|
||||
### B. 批量成员管理
|
||||
|
||||
```bash
|
||||
# ── 终端: CSV 批量导入 ──
|
||||
cat > members.csv << 'EOF'
|
||||
user_id,role
|
||||
101325,Developer
|
||||
126177,Developer
|
||||
92560,Reporter
|
||||
EOF
|
||||
gitlink-cli member +batch-add --owner jiangtx --repo gitlink-cli-demo \
|
||||
--from members.csv --dry-run
|
||||
# → [DRY RUN] 将添加 3 位成员
|
||||
|
||||
gitlink-cli member +batch-add --owner jiangtx --repo gitlink-cli-demo \
|
||||
--from members.csv
|
||||
|
||||
# ── 终端: 网页验证 ──
|
||||
gitlink-cli show member
|
||||
# → 🔗 https://gitlink.org.cn/jiangtx/gitlink-cli-demo/settings/collaboration
|
||||
# 【投影仪】成员管理页面,3个新成员已出现
|
||||
```
|
||||
|
||||
### C. 数据导出 — 支撑科研分析
|
||||
|
||||
```bash
|
||||
# ── 终端: 导出数据 ──
|
||||
gitlink-cli export +issues --owner jiangtx --repo gitlink-cli-demo \
|
||||
--format csv --output gitlink_cli_issues.csv
|
||||
gitlink-cli export +prs --owner jiangtx --repo gitlink-cli-demo \
|
||||
--format csv --output gitlink_cli_prs.csv
|
||||
gitlink-cli export +contributors --owner jiangtx --repo gitlink-cli-demo \
|
||||
--format json --output gitlink_cli_contributors.json
|
||||
|
||||
# ── 终端: 数据文件可直接导入 Python/R ──
|
||||
head gitlink_cli_issues.csv
|
||||
# → number,title,state,created_at,...
|
||||
# 【投影仪】展示 CSV 文件在 Excel 中打开的效果
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 第四类:跨平台兼容 + 安装体验(3 min)
|
||||
|
||||
> **话术**: "扩展前仅 go install。扩展后 5 种安装方式,覆盖全平台全场景。"
|
||||
|
||||
### A. 五种安装方式
|
||||
|
||||
```bash
|
||||
# 方式 1: npm(全平台通用,推荐)
|
||||
npm install -g @gitlink-ai/cli
|
||||
# → npm postinstall 自动下载对应平台二进制
|
||||
|
||||
# 方式 2: winget(Windows 原生)
|
||||
winget install gitlink-cli
|
||||
|
||||
# 方式 3: brew(macOS/Linux 原生)
|
||||
brew install gitlink-cli
|
||||
|
||||
# 方式 4: go install(开发者)
|
||||
go install github.com/gitlink-org/gitlink-cli@latest
|
||||
|
||||
# 方式 5: 一键脚本
|
||||
curl -fsSL https://gitlink.org.cn/install.sh | bash # Linux/macOS
|
||||
iwr -useb https://gitlink.org.cn/install.ps1 | iex # Windows
|
||||
```
|
||||
|
||||
### B. 跨平台 keyring 适配
|
||||
|
||||
```bash
|
||||
gitlink-cli auth login
|
||||
# → Windows: Token → Credential Manager
|
||||
# → macOS: Token → Keychain
|
||||
# → Linux: Token → Secret Service / dbus
|
||||
# → CI/CD: export GITLINK_TOKEN="..." (环境变量 fallback)
|
||||
|
||||
gitlink-cli version
|
||||
# → gitlink-cli v3.0.0 (windows/amd64)
|
||||
```
|
||||
|
||||
```bash
|
||||
# ── 网页对应: npm 包页面 ──
|
||||
gitlink-cli show npm
|
||||
# → 🔗 https://www.npmjs.com/package/@gitlink-ai/cli
|
||||
# 【投影仪】展示 npm 包页面(下载量/版本号/README)
|
||||
```
|
||||
|
||||
### C. 安装体验对比表
|
||||
|
||||
| 维度 | 扩展前 | 扩展后 |
|
||||
|------|--------|--------|
|
||||
| 安装方式 | 仅 go install | npm / winget / brew / go install / 脚本 |
|
||||
| Token存储 | 仅环境变量 | Credential Manager / Keychain / dbus / 文件 |
|
||||
| 二进制下载 | 手动 | npm postinstall 自动 |
|
||||
|
||||
---
|
||||
|
||||
## 第五类:补全 Raw API 封装(3 min)
|
||||
|
||||
> **话术**: "影响面最广的修复——扩展前 31 个 GET + 10 个 POST/PUT/DELETE 端点全部返回 HTML 乱码。"
|
||||
|
||||
### A. HTML 检测 — 从乱码到中文指引
|
||||
|
||||
```bash
|
||||
# ── 演示 1: 扩展前(Token 未注入时) ──
|
||||
GITLINK_TOKEN="" gitlink-cli api GET /users/me 2>&1
|
||||
# → {"data":"<!doctype html>...<script>...</script></html>"}
|
||||
# → 几千字符的 HTML 源码,完全无法理解
|
||||
|
||||
# ── 演示 2: 扩展后(HTML 检测) ──
|
||||
# (演示已修复的效果 — 说明 detectHTMLResponse() 的原理)
|
||||
# 仅 ~30 行新增代码,覆盖 41 个端点
|
||||
# → ❌ 服务器返回了 HTML 页面而非 JSON 数据
|
||||
# → 可能原因:
|
||||
# → 1. 未登录或 Token 已过期 → gitlink-cli auth login
|
||||
# → 2. Token 权限不足 → 在 GitLink 平台重新生成
|
||||
# → 3. API 端点不存在 → 检查路径
|
||||
# → 4. 使用 Shortcut 命令替代 Raw API
|
||||
```
|
||||
|
||||
### B. 端点 → Shortcut 映射 — 现场对比
|
||||
|
||||
```bash
|
||||
# ── 终端: 扩展前(不可用,只能 Raw API + 大概率 HTML) ──
|
||||
# GITLINK_TOKEN="" gitlink-cli api GET /jiangtx/gitlink-cli-demo/languages → HTML
|
||||
# GITLINK_TOKEN="" gitlink-cli api GET /jiangtx/gitlink-cli-demo/contributors → HTML
|
||||
# GITLINK_TOKEN="" gitlink-cli api GET /jiangtx/gitlink-cli-demo/commits → HTML
|
||||
# GITLINK_TOKEN="" gitlink-cli api GET /users/jiangtx/headmaps → HTML
|
||||
|
||||
# ── 终端: 扩展后(全部有对应 Shortcut) ──
|
||||
gitlink-cli repo +languages --owner jiangtx --repo gitlink-cli-demo
|
||||
gitlink-cli repo +contributors --owner jiangtx --repo gitlink-cli-demo
|
||||
gitlink-cli repo +commits --owner jiangtx --repo gitlink-cli-demo
|
||||
gitlink-cli user +heatmap --login jiangtx
|
||||
# → 全部正常返回 JSON
|
||||
|
||||
# ── 网页对应 ──
|
||||
gitlink-cli show repo → .../jiangtx/gitlink-cli-demo (仓库主页,含语言占比)
|
||||
gitlink-cli show user --login jiangtx → .../jiangtx (用户主页,含热力图)
|
||||
```
|
||||
|
||||
### C. 端点映射总表(演示用简表)
|
||||
|
||||
```text
|
||||
未封装 Raw API 端点 → 新 Shortcut 命令
|
||||
GET /:owner/:repo/languages → repo +languages
|
||||
GET /:owner/:repo/contributors → repo +contributors
|
||||
GET /:owner/:repo/commits → repo +commits
|
||||
GET /users/:login/headmaps → user +heatmap
|
||||
POST /:owner/:repo/create_file → repo +create-file
|
||||
GET /organizations/:id/teams → org +teams
|
||||
... 共 56 条新命令,覆盖 ~40 个未封装端点
|
||||
```
|
||||
|
||||
### D. Skills 修复 — Agent 技能从不可用到全部恢复
|
||||
|
||||
```text
|
||||
修复前: skills/gitlink-repo/SKILL.md 引用
|
||||
gitlink-cli api GET /:owner/:repo/languages → 返回 HTML → Agent 无法使用
|
||||
|
||||
修复后: skills/gitlink-repo/SKILL.md 引用
|
||||
gitlink-cli repo +languages --owner <> --repo <> → 正常 JSON → Agent 可用
|
||||
|
||||
修复范围: 18 个 Skill 子目录,~29 个文件,~90 处引用
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 收尾:能力验证 + 验收清单(2 min)
|
||||
|
||||
### A. 后端 API 能力探测
|
||||
|
||||
```bash
|
||||
gitlink-cli capability +check
|
||||
# → 显示每个模块的 API 可用状态(✓ / ⚠)
|
||||
gitlink-cli show api-docs
|
||||
# → 🔗 https://s.apifox.cn/da30afb0-9d2e-429b-a4bc-a83209e06021
|
||||
# 【投影仪】GitLink OpenAPI 文档页面
|
||||
```
|
||||
|
||||
### B. 验收清单
|
||||
|
||||
```text
|
||||
✅ 新增 Shortcut 命令 webhook/wiki/pm/pipeline/label/member/milestone — 全部可执行
|
||||
✅ 优化现有命令 --format json/table/yaml + --debug + 中文帮助
|
||||
✅ 批量操作能力 batch-close/batch-add/export — dry-run 安全预览
|
||||
✅ 跨平台兼容 5 种安装方式 + keyring 多平台 + 环境变量 fallback
|
||||
✅ 补全 Raw API 封装 56 条新命令覆盖 ~40 个端点 + HTML 检测
|
||||
✅ 测试覆盖率 ≥ 80% 80.4% — CI 门禁通过
|
||||
✅ Skills ~90 处修复 Agent 技能全部修复
|
||||
```
|
||||
|
||||
### C. 架构速览
|
||||
|
||||
```text
|
||||
L1: Shortcuts ← 56 条新命令(人性化参数 + 智能默认值)
|
||||
L2: Domain Cmds ← 新增 7+ 个域组 (webhook/member/milestone/pipeline/label/pm/wiki/...)
|
||||
L3: Raw API ← HTML 检测 + Skills 修复
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 演示前置条件检查
|
||||
|
||||
| # | 条件 | 状态 |
|
||||
|---|------|:---:|
|
||||
| 1 | `gitlink-cli.exe` 可执行 | ✅ |
|
||||
| 2 | `GITLINK_TOKEN` 已设置 | 需配置 |
|
||||
| 3 | GitLink 服务可访问 | 需网络 |
|
||||
| 4 | 备用方案:`--demo` Mock 模式 | ✅ 无网可演示 |
|
||||
| 5 | 投影仪可打开浏览器 | 需确认 |
|
||||
|
||||
**备用方案**:如果 Token 或网络不可用,使用 `export GITLINK_DEMO=1` 进入 Mock 模式,所有命令返回预置数据,演示不受影响。
|
||||
|
|
@ -1,326 +0,0 @@
|
|||
# GitLink-CLI 数据分析与可视化管线(analyze 模块)
|
||||
|
||||
## 一、设计目标
|
||||
|
||||
在现有 `export` 模块基础上,建立**端到端数据分析管线**,使 CLI 不仅是数据导出工具,
|
||||
更是**科研级数据分析平台**。所有分析结果支持多格式输出,直接可用于论文。
|
||||
|
||||
## 二、为什么需要 analyze 模块?
|
||||
|
||||
### 2.1 export 只做"搬数据",不做"看数据"
|
||||
|
||||
```text
|
||||
现状: export +issues → CSV 文件 → 用户需手动用 Python/R/Excel 分析
|
||||
期望: analyze +health → 一键生成健康报告 → 直接可用于论文
|
||||
```
|
||||
|
||||
### 2.2 实质性与表面性的区别
|
||||
|
||||
| 表面扩展 | 实质性扩展 |
|
||||
|---------|-----------|
|
||||
| 多一个 `export +wiki` 导出 | `analyze +velocity` 用 Burndown 算法做交付速度分析 |
|
||||
| 多一个格式化选项 | `analyze +network` 构建贡献者协作图(Graphviz) |
|
||||
| 多一个 CSV 列 | `analyze +hotspots` 用变更频率+复杂度找代码热点 |
|
||||
|
||||
---
|
||||
|
||||
## 三、命令设计
|
||||
|
||||
### 3.1 命令全景
|
||||
|
||||
```text
|
||||
gitlink-cli analyze
|
||||
├── +health 仓库综合健康度分析
|
||||
├── +velocity 团队交付速度(Burndown / Velocity)
|
||||
├── +hotspots 代码变更热点分析
|
||||
├── +network 贡献者协作网络
|
||||
├── +trends 多维度趋势分析
|
||||
├── +compare 跨仓库对比分析
|
||||
└── +profile 贡献者画像
|
||||
```
|
||||
|
||||
### 3.2 各命令详解
|
||||
|
||||
#### A. analyze +health — 仓库健康度分析
|
||||
|
||||
```bash
|
||||
# 基础用法
|
||||
gitlink-cli analyze +health --owner jiangtx --repo gitlink-cli
|
||||
|
||||
# 输出
|
||||
gitlink-cli analyze +health --owner jiangtx --repo gitlink-cli --format markdown
|
||||
```
|
||||
|
||||
```markdown
|
||||
## 📊 仓库健康报告: jiangtx/gitlink-cli
|
||||
**生成时间**: 2026-06-08 15:30 CST
|
||||
|
||||
### 活跃度指标
|
||||
| 指标 | 数值 | 评级 |
|
||||
|------|------|------|
|
||||
| 近30天提交数 | 127 | 🟢 活跃 |
|
||||
| 近30天 Issue 创建 | 23 | 🟢 正常 |
|
||||
| 近30天 PR 合并 | 18 | 🟢 正常 |
|
||||
| Issue 平均关闭时间 | 3.2 天 | 🟢 健康 |
|
||||
| PR 平均审查时间 | 1.5 天 | 🟢 健康 |
|
||||
|
||||
### 社区健康
|
||||
| 指标 | 数值 | 评级 |
|
||||
|------|------|------|
|
||||
| 贡献者数量 | 11 | 🟢 健康 |
|
||||
| 总线因子 | 3 | 🟡 关注 |
|
||||
| 新贡献者比例 | 27% | 🟢 增长 |
|
||||
| 平均响应时间 | 4.2 小时 | 🟢 快速 |
|
||||
|
||||
### 风险信号
|
||||
- ⚠ 总线因子偏低(3/11),2 个主要贡献者贡献了 61% 的代码
|
||||
|
||||
### 建议
|
||||
1. 鼓励代码评审轮换,降低总线因子
|
||||
2. 为新贡献者添加更多 `good first issue` 标签
|
||||
|
||||
---
|
||||
*数据来源: GitLink API. 方法: 加权移动平均.*
|
||||
```
|
||||
|
||||
#### B. analyze +velocity — 团队交付速度
|
||||
|
||||
```bash
|
||||
# Sprint 燃尽图数据(JSON 格式,可导入绘图工具)
|
||||
gitlink-cli analyze +velocity --owner jiangtx --repo gitlink-cli \
|
||||
--milestone "Sprint 5" --format json
|
||||
|
||||
# 输出: Burndown 数据点
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"ok": true,
|
||||
"data": {
|
||||
"milestone": "Sprint 5",
|
||||
"start_date": "2026-05-25",
|
||||
"end_date": "2026-06-08",
|
||||
"total_points": 120,
|
||||
"burndown": [
|
||||
{"date": "2026-05-25", "remaining": 120, "ideal": 120},
|
||||
{"date": "2026-05-27", "remaining": 105, "ideal": 104},
|
||||
{"date": "2026-05-29", "remaining": 88, "ideal": 88},
|
||||
{"date": "2026-06-01", "remaining": 62, "ideal": 64},
|
||||
{"date": "2026-06-03", "remaining": 45, "ideal": 48},
|
||||
{"date": "2026-06-05", "remaining": 28, "ideal": 32},
|
||||
{"date": "2026-06-08", "remaining": 5, "ideal": 8}
|
||||
],
|
||||
"velocity": {
|
||||
"average": 18.4,
|
||||
"trend": "stable",
|
||||
"completion_rate": 95.8
|
||||
},
|
||||
"forecast": {
|
||||
"estimated_completion": "2026-06-09",
|
||||
"confidence": 0.92
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### C. analyze +hotspots — 代码变更热点
|
||||
|
||||
```bash
|
||||
# 找出变更最频繁的文件(技术债务指标)
|
||||
gitlink-cli analyze +hotspots --owner jiangtx --repo gitlink-cli \
|
||||
--since 2026-01-01 --top 10 --format table
|
||||
```
|
||||
|
||||
```
|
||||
┌──────────────────────────────────────┬────────┬──────────┬────────┐
|
||||
│ 文件 │ 变更数 │ 作者数 │ 热点 │
|
||||
├──────────────────────────────────────┼────────┼──────────┼────────┤
|
||||
│ internal/client/client.go │ 47 │ 5 │ 🔴 高 │
|
||||
│ shortcuts/repo/repo.go │ 38 │ 4 │ 🔴 高 │
|
||||
│ shortcuts/issue/issue.go │ 31 │ 3 │ 🟡 中 │
|
||||
│ cmd/root.go │ 25 │ 3 │ 🟡 中 │
|
||||
│ internal/output/envelope.go │ 18 │ 2 │ 🟢 低 │
|
||||
│ shortcuts/pr/pr.go │ 16 │ 3 │ 🟢 低 │
|
||||
│ internal/auth/token_store.go │ 15 │ 2 │ 🟢 低 │
|
||||
│ shortcuts/common/runner.go │ 12 │ 2 │ 🟢 低 │
|
||||
│ internal/config/config.go │ 10 │ 1 │ 🟢 低 │
|
||||
│ shortcuts/member/member.go │ 9 │ 2 │ 🟢 低 │
|
||||
└──────────────────────────────────────┴────────┴──────────┴────────┘
|
||||
```
|
||||
|
||||
#### D. analyze +network — 贡献者协作网络
|
||||
|
||||
```bash
|
||||
# 输出 Graphviz DOT 格式,可直接渲染为协作网络图
|
||||
gitlink-cli analyze +network --owner jiangtx --repo gitlink-cli \
|
||||
--since 2026-01-01 --format dot --output network.dot
|
||||
|
||||
# 渲染为 PNG
|
||||
dot -Tpng network.dot -o network.png
|
||||
```
|
||||
|
||||
#### E. analyze +trends — 多维度趋势
|
||||
|
||||
```bash
|
||||
# 输出时间序列数据
|
||||
gitlink-cli analyze +trends --owner jiangtx --repo gitlink-cli \
|
||||
--metrics commits,issues,prs,contributors --since 2025-06-01 --format csv
|
||||
|
||||
# 输出
|
||||
# date,commits,issues_created,issues_closed,prs_created,prs_merged,contributors
|
||||
# 2025-06,45,12,10,8,7,5
|
||||
# 2025-07,62,18,15,12,10,7
|
||||
# ...
|
||||
```
|
||||
|
||||
#### F. analyze +profile — 贡献者画像
|
||||
|
||||
```bash
|
||||
gitlink-cli analyze +profile --login jiangtx --format markdown
|
||||
```
|
||||
|
||||
```markdown
|
||||
## 👤 贡献者画像: @jiangtx
|
||||
|
||||
### 活跃概览
|
||||
- 总提交数: 847
|
||||
- 总 Issue 关闭: 203
|
||||
- 总 PR 合并: 156
|
||||
- 主要语言: Go (72%), Python (18%), Shell (10%)
|
||||
|
||||
### 活跃模式
|
||||
- 活跃时段: 工作日 9:00-18:00 CST
|
||||
- 活跃日期: 周二最活跃
|
||||
- 贡献热力: ██████████░░░░░
|
||||
|
||||
### 标签分布
|
||||
- 仓库: gitlink-cli (60%), forgeplus (25%), ...
|
||||
- 角色: 核心维护者
|
||||
|
||||
### 协作网络
|
||||
- 主要协作者: wangyue111, wbtiger, Mengz
|
||||
- 审查数: 423 次 PR 审查
|
||||
```
|
||||
|
||||
## 四、实现架构
|
||||
|
||||
```text
|
||||
shortcuts/
|
||||
├── analyze/ # ★ 新增
|
||||
│ ├── analyze.go # Shortcuts 定义
|
||||
│ ├── analyze_test.go
|
||||
│ ├── health.go # 健康度计算引擎
|
||||
│ ├── health_test.go
|
||||
│ ├── velocity.go # Burndown 算法
|
||||
│ ├── hotspots.go # 热点分析(变更频率 × 复杂度)
|
||||
│ ├── network.go # 协作网络(Graphviz DOT 生成)
|
||||
│ ├── trends.go # 时间序列聚合
|
||||
│ ├── profile.go # 贡献者画像
|
||||
│ └── engine/ # 分析引擎子包
|
||||
│ ├── metrics.go # 通用指标计算
|
||||
│ ├── aggregator.go # 数据聚合
|
||||
│ ├── scorer.go # 评分算法(加权、归一化)
|
||||
│ └── forecast.go # 简单预测(移动平均、趋势推断)
|
||||
```
|
||||
|
||||
### 核心算法
|
||||
|
||||
```go
|
||||
// engine/scorer.go
|
||||
|
||||
// HealthScore 综合健康评分(0-100)
|
||||
func HealthScore(metrics HealthMetrics) int {
|
||||
// 加权评分
|
||||
score := 0.0
|
||||
score += float64(metrics.ActivityScore) * 0.30 // 活跃度权重 30%
|
||||
score += float64(metrics.CommunityScore) * 0.25 // 社区健康 25%
|
||||
score += float64(metrics.QualityScore) * 0.25 // 代码质量 25%
|
||||
score += float64(metrics.ResponsivenessScore) * 0.20 // 响应速度 20%
|
||||
return int(score)
|
||||
}
|
||||
|
||||
// BusFactor 总线因子(最小关键贡献者数)
|
||||
func BusFactor(contributions []Contribution) int {
|
||||
// 累计贡献 ≥ 50% 的最少人数
|
||||
sort.Slice(contributions, func(i, j int) bool {
|
||||
return contributions[i].Percentage > contributions[j].Percentage
|
||||
})
|
||||
sum := 0.0
|
||||
for i, c := range contributions {
|
||||
sum += c.Percentage
|
||||
if sum >= 50.0 {
|
||||
return i + 1
|
||||
}
|
||||
}
|
||||
return len(contributions)
|
||||
}
|
||||
|
||||
// HotspotScore 热点评分(变更频率 × 复杂度 × 作者分布)
|
||||
func HotspotScore(changes, authors int, complexity float64) float64 {
|
||||
const (
|
||||
freqWeight = 0.5
|
||||
authorWeight = 0.3
|
||||
complexWeight = 0.2
|
||||
)
|
||||
freqScore := math.Min(float64(changes)/50.0, 1.0)
|
||||
authorScore := 1.0 - math.Min(float64(authors-1)/10.0, 1.0)
|
||||
complexScore := math.Min(complexity/1000.0, 1.0)
|
||||
return freqScore*freqWeight + authorScore*authorWeight + complexScore*complexWeight
|
||||
}
|
||||
```
|
||||
|
||||
## 五、与 export 模块的关系
|
||||
|
||||
| 维度 | export | analyze |
|
||||
|------|--------|---------|
|
||||
| 输入 | GitLink API | export 输出 + GitLink API |
|
||||
| 处理 | 无 | 统计建模 + 评分算法 |
|
||||
| 输出 | CSV / JSON | JSON / Markdown / Table / DOT |
|
||||
| 目标用户 | 数据分析师 | 所有人(含非技术背景) |
|
||||
| 可用性 | 原始数据 | 直接可读的结论 |
|
||||
|
||||
```text
|
||||
工作流:
|
||||
export +issues → issues.csv ──┐
|
||||
export +prs → prs.csv ────────┤
|
||||
export +contributors → ... ───┤
|
||||
│
|
||||
┌───────────────▼────────────────┐
|
||||
│ analyze +health │
|
||||
│ analyze +velocity │
|
||||
│ analyze +hotspots ← 读取 CSV │
|
||||
│ analyze +network │
|
||||
└───────────────┬────────────────┘
|
||||
│
|
||||
┌───────────────▼────────────────┐
|
||||
│ 输出: 报告 / 图表 / 数据 │
|
||||
│ 直接用于论文 / 演示 / 报告 │
|
||||
└────────────────────────────────┘
|
||||
```
|
||||
|
||||
## 六、演示场景
|
||||
|
||||
```bash
|
||||
# 演示 1: 仓库健康报告(配合 workflow +health)
|
||||
gitlink-cli analyze +health --owner jiangtx --repo gitlink-cli --format markdown
|
||||
|
||||
# 演示 2: 导出 + 分析流水线(展示科研价值)
|
||||
gitlink-cli export +issues --owner jiangtx --repo gitlink-cli --output issues.csv
|
||||
gitlink-cli export +prs --owner jiangtx --repo gitlink-cli --output prs.csv
|
||||
gitlink-cli analyze +velocity --from issues.csv,prs.csv --format json > velocity.json
|
||||
|
||||
# 演示 3: 贡献者网络图生成
|
||||
gitlink-cli analyze +network --owner jiangtx --repo gitlink-cli --format dot > network.dot
|
||||
echo "可使用 Graphviz 渲染: dot -Tpng network.dot -o network.png"
|
||||
```
|
||||
|
||||
## 七、实质性价值
|
||||
|
||||
| 功能 | 解决的实质问题 |
|
||||
|------|--------------|
|
||||
| `analyze +health` | 项目管理者从"凭感觉"到"看数据"做决策 |
|
||||
| `analyze +velocity` | Sprint 回顾会议有量化数据支撑 |
|
||||
| `analyze +hotspots` | Code Review 优先级有数据依据 |
|
||||
| `analyze +network` | 识别社区核心人物和孤立贡献者 |
|
||||
| `analyze +trends` | 为论文提供时间序列分析数据 |
|
||||
| `analyze +profile` | 为社区运营提供个人层面洞察 |
|
||||
|
|
@ -1,214 +0,0 @@
|
|||
# GitLink-CLI 项目上下文管理系统
|
||||
|
||||
## 一、设计目标
|
||||
|
||||
建立**持久化项目工作上下文**,使开发者能在多个项目间快速切换,
|
||||
一键恢复上次工作状态。解决"每天开始工作要重新回忆上下文"的痛点。
|
||||
|
||||
## 二、痛点分析
|
||||
|
||||
```text
|
||||
场景:开发者同时维护 3 个项目
|
||||
|
||||
每天早上:
|
||||
cd ~/project-a && git checkout feature/x
|
||||
# 回忆: 昨天做到哪了?哪个 Issue?哪个 PR?
|
||||
# 回忆: 这个项目的 GitLink 链接是什么?
|
||||
|
||||
gitlink-cli 现状: 可以通过 git remote 推断 owner/repo
|
||||
但不能保存"我当时在处理 Issue #42"或"我在审查 PR #128"
|
||||
```
|
||||
|
||||
## 三、命令设计
|
||||
|
||||
### 3.1 context — 工作上下文管理
|
||||
|
||||
```bash
|
||||
# 保存当前工作上下文
|
||||
gitlink-cli context +save
|
||||
# → ✓ 上下文已保存: "gitlink-cli" (2026-06-08 15:30)
|
||||
# 仓库: jiangtx/gitlink-cli
|
||||
# 分支: feature/enhancement
|
||||
# 当前 Issue: #42
|
||||
# 当前 PR: #128
|
||||
|
||||
# 保存时添加备注
|
||||
gitlink-cli context +save --note "修复 Webhook 测试问题"
|
||||
|
||||
# 列出已保存的上下文
|
||||
gitlink-cli context +list
|
||||
```
|
||||
|
||||
```
|
||||
┌──────────────────────────────────────────────────────┬──────────────┬─────────────┐
|
||||
│ 名称 │ 仓库 │ 保存时间 │
|
||||
├──────────────────────────────────────────────────────┼──────────────┼─────────────┤
|
||||
│ gitlink-cli (当前) │ jiangtx/... │ 15:30 今天 │
|
||||
│ forgeplus │ Gitlink/... │ 09:00 昨天 │
|
||||
│ help-center │ jiangtx/... │ 14:00 06-06 │
|
||||
└──────────────────────────────────────────────────────┴──────────────┴─────────────┘
|
||||
|
||||
# 恢复保存的上下文
|
||||
gitlink-cli context +restore forgeplus
|
||||
# → ✓ 已切换到 forgeplus
|
||||
# 仓库: Gitlink/forgeplus
|
||||
# 上次活动 Issue: #307
|
||||
# 上次活动 PR: #42
|
||||
|
||||
# 查看上下文详情
|
||||
gitlink-cli context +view gitlink-cli
|
||||
# → 显示完整保存信息,含上次命令历史
|
||||
|
||||
# 删除上下文
|
||||
gitlink-cli context +delete old-project
|
||||
```
|
||||
|
||||
### 3.2 project — 项目配置管理
|
||||
|
||||
```bash
|
||||
# 初始化项目配置(在当前仓库根目录创建 .gitlink.yml)
|
||||
gitlink-cli project +init
|
||||
# → ✓ 已创建 .gitlink.yml
|
||||
# 仓库: jiangtx/gitlink-cli
|
||||
# 默认分支: master
|
||||
# 默认标签: enhancement, bug, documentation
|
||||
# 默认 CI 配置: .devops/*.yml
|
||||
|
||||
# .gitlink.yml 内容:
|
||||
# name: gitlink-cli
|
||||
# owner: jiangtx
|
||||
# repo: gitlink-cli
|
||||
# default_branch: master
|
||||
# labels:
|
||||
# - name: bug
|
||||
# color: "#d73a4a"
|
||||
# - name: enhancement
|
||||
# color: "#a2eeef"
|
||||
# templates:
|
||||
# issue: .gitlink/ISSUE_TEMPLATE.md
|
||||
# pr: .gitlink/PR_TEMPLATE.md
|
||||
|
||||
# 查看项目配置
|
||||
gitlink-cli project +view
|
||||
# → 显示 .gitlink.yml 内容
|
||||
|
||||
# 编辑项目配置
|
||||
gitlink-cli project +edit
|
||||
# → 打开 $EDITOR 编辑 .gitlink.yml
|
||||
|
||||
# 从模板初始化
|
||||
gitlink-cli project +init --template go-library
|
||||
gitlink-cli project +init --template web-service
|
||||
gitlink-cli project +init --template mobile-app
|
||||
```
|
||||
|
||||
### 3.3 自动上下文注入
|
||||
|
||||
```bash
|
||||
# 在 git 仓库中运行任何命令时,自动加载项目配置
|
||||
cd ~/gitlink-cli
|
||||
gitlink-cli issue +create -t "Bug"
|
||||
# → 自动从 .gitlink.yml 读取 default_branch, labels, templates
|
||||
# → 自动从 context 读取"上次处理的 Issue"
|
||||
|
||||
# 切换分支时自动提示上下文
|
||||
cd ~/forgeplus && git checkout feature/x
|
||||
# gitlink-cli 检测到分支变化 → 提示:
|
||||
# ℹ 检测到分支切换: master → feature/x
|
||||
# 上次在此分支的工作: Issue #307, PR #42
|
||||
# 恢复上下文? [Y/n]
|
||||
```
|
||||
|
||||
## 四、实现架构
|
||||
|
||||
```text
|
||||
internal/
|
||||
├── context/
|
||||
│ ├── repo.go # 现有:git remote 解析
|
||||
│ ├── repo_test.go
|
||||
│ ├── session.go # ★ 新增:工作会话管理
|
||||
│ ├── session_test.go
|
||||
│ └── storage.go # ★ 新增:上下文持久化
|
||||
├── project/ # ★ 新增
|
||||
│ ├── config.go # .gitlink.yml 解析/写入
|
||||
│ ├── config_test.go
|
||||
│ ├── template.go # 模板管理
|
||||
│ └── template_test.go
|
||||
cmd/
|
||||
├── context/ # ★ 新增
|
||||
│ ├── context.go # context 命令组
|
||||
│ └── context_test.go
|
||||
└── project/ # ★ 新增
|
||||
├── project.go # project 命令组
|
||||
└── project_test.go
|
||||
```
|
||||
|
||||
### 核心数据结构
|
||||
|
||||
```go
|
||||
// internal/context/session.go
|
||||
|
||||
type WorkSession struct {
|
||||
Name string `yaml:"name"` // 会话名(默认=仓库名)
|
||||
Owner string `yaml:"owner"`
|
||||
Repo string `yaml:"repo"`
|
||||
Branch string `yaml:"branch"`
|
||||
IssueID int `yaml:"issue_id,omitempty"`
|
||||
PRID int `yaml:"pr_id,omitempty"`
|
||||
Note string `yaml:"note,omitempty"`
|
||||
SavedAt time.Time `yaml:"saved_at"`
|
||||
LastCmd string `yaml:"last_command,omitempty"`
|
||||
}
|
||||
|
||||
// 存储路径: ~/.config/gitlink-cli/contexts.yaml
|
||||
```
|
||||
|
||||
```go
|
||||
// internal/project/config.go
|
||||
|
||||
type ProjectConfig struct {
|
||||
Name string `yaml:"name"`
|
||||
Owner string `yaml:"owner"`
|
||||
Repo string `yaml:"repo"`
|
||||
DefaultBranch string `yaml:"default_branch"`
|
||||
Labels []LabelDef `yaml:"labels,omitempty"`
|
||||
Templates TemplateConfig `yaml:"templates,omitempty"`
|
||||
CI CIConfig `yaml:"ci,omitempty"`
|
||||
}
|
||||
|
||||
// 存储路径: <repo_root>/.gitlink.yml
|
||||
```
|
||||
|
||||
## 五、演示场景
|
||||
|
||||
```bash
|
||||
# 演示 1: 上下文保存与恢复
|
||||
gitlink-cli issue +view --number 42
|
||||
gitlink-cli context +save --note "分析 Issue #42"
|
||||
gitlink-cli context +list
|
||||
gitlink-cli context +restore gitlink-cli
|
||||
# → 直接打开上次的工作上下文
|
||||
|
||||
# 演示 2: 项目初始化
|
||||
gitlink-cli project +init --template go-library
|
||||
cat .gitlink.yml
|
||||
gitlink-cli label +clone --from Gitlink/forgeplus
|
||||
# → 新项目一键搭好标签体系
|
||||
|
||||
# 演示 3: 快速切换项目
|
||||
gitlink-cli context +list
|
||||
gitlink-cli context +restore forgeplus
|
||||
# → 终端输出切换到 forgeplus 上下文
|
||||
gitlink-cli show repo
|
||||
# → 输出 forgeplus 的网页链接
|
||||
```
|
||||
|
||||
## 六、实质性价值
|
||||
|
||||
| 功能 | 解决的问题 |
|
||||
|------|----------|
|
||||
| `context +save/restore` | 多项目切换零成本恢复工作状态 |
|
||||
| `project +init` | 新项目标准化配置,从模板一键创建 |
|
||||
| `.gitlink.yml` | CI/CD/Issue/PR 配置版本化管理 |
|
||||
| 自动上下文提示 | 分支切换时自动提示上次工作状态 |
|
||||
| 会话历史 | 跨天/跨周工作无需重新回忆上下文 |
|
||||
|
|
@ -1,329 +0,0 @@
|
|||
# GitLink-CLI 演示模式(Demo Mode)设计
|
||||
|
||||
## 一、问题分析
|
||||
|
||||
### 1.1 课程演示的三大痛点
|
||||
|
||||
| # | 痛点 | 表现 |
|
||||
|---|------|------|
|
||||
| 1 | **Token 依赖** | 没有有效 Token → 所有写操作命令无法执行 → 演示失败 |
|
||||
| 2 | **网络依赖** | 教室 WiFi 不稳定 → API 请求超时 → 演示卡顿 |
|
||||
| 3 | **环境差异** | 不同学生电脑上 GITLINK_TOKEN 配置不一致 → 无法复现 |
|
||||
|
||||
### 1.2 现有方案的局限
|
||||
|
||||
展示方案文档中所有演示命令都依赖真实的 GitLink 服务器:
|
||||
|
||||
```bash
|
||||
# 这些全部需要 API 调用成功
|
||||
gitlink-cli repo +languages --owner jiangtx --repo gitlink-cli-demo
|
||||
gitlink-cli webhook +list --owner jiangtx --repo gitlink-cli-demo
|
||||
gitlink-cli issue +batch-close --owner jiangtx --repo gitlink-cli-demo --numbers 1,2,3
|
||||
```
|
||||
|
||||
**一旦 Token 过期或网络断开 → 整个演示失败。**
|
||||
|
||||
---
|
||||
|
||||
## 二、解决方案:Mock 模式 + 演示脚本
|
||||
|
||||
### 2.1 核心设计
|
||||
|
||||
```text
|
||||
┌──────────────────────┐
|
||||
│ gitlink-cli ... │
|
||||
│ 用户命令 │
|
||||
└──────────┬───────────┘
|
||||
│
|
||||
┌──────────▼───────────┐
|
||||
│ 检测 --demo flag │
|
||||
│ 或 GITLINK_DEMO=1 │
|
||||
└──────────┬───────────┘
|
||||
│
|
||||
┌────────────────┼────────────────┐
|
||||
│ │ │
|
||||
┌────────▼──────┐ ┌──────▼──────┐ ┌─────▼──────┐
|
||||
│ Demo Mode: ON │ │ Demo Mode: ON│ │ Normal Mode│
|
||||
│ (读操作) │ │ (写操作) │ │ │
|
||||
│ │ │ │ │ │
|
||||
│ 返回预置 JSON │ │ Dry-run + │ │ 真实 API │
|
||||
│ 数据(来自 │ │ 模拟成功 │ │ 调用 │
|
||||
│ fixtures/) │ │ 响应 │ │ │
|
||||
└───────────────┘ └─────────────┘ └────────────┘
|
||||
```
|
||||
|
||||
### 2.2 使用方式
|
||||
|
||||
```bash
|
||||
# 方式 1: --demo flag(单次)
|
||||
gitlink-cli repo +list --demo
|
||||
gitlink-cli issue +create -t "test" -b "body" --demo
|
||||
gitlink-cli webhook +list --owner jiangtx --repo demo --demo
|
||||
|
||||
# 方式 2: 环境变量(全局)
|
||||
export GITLINK_DEMO=1
|
||||
gitlink-cli repo +list # 自动进入 Demo 模式
|
||||
gitlink-cli issue +list # 自动进入 Demo 模式
|
||||
|
||||
# 方式 3: 演示脚本(预编排)
|
||||
gitlink-cli demo +run showcase # 运行内置演示脚本
|
||||
gitlink-cli demo +run custom.yml # 运行自定义脚本
|
||||
|
||||
# 方式 4: 录制模式
|
||||
gitlink-cli demo +record # 录制真实操作到脚本
|
||||
gitlink-cli demo +replay # 回放录制的脚本
|
||||
```
|
||||
|
||||
### 2.3 预置演示数据(fixtures/)
|
||||
|
||||
```text
|
||||
internal/demo/
|
||||
├── demo.go # Demo 模式核心逻辑
|
||||
├── demo_test.go
|
||||
├── fixtures/ # 预置响应数据
|
||||
│ ├── repo_list.json # repo +list 响应
|
||||
│ ├── repo_languages.json # repo +languages 响应
|
||||
│ ├── issue_list.json # issue +list 响应
|
||||
│ ├── issue_create.json # issue +create 响应
|
||||
│ ├── pr_list.json # pr +list 响应
|
||||
│ ├── webhook_list.json # webhook +list 响应
|
||||
│ ├── webhook_create.json # webhook +create 响应
|
||||
│ ├── label_list.json # label +list 响应
|
||||
│ ├── member_list.json # member +list 响应
|
||||
│ ├── pipeline_list.json # pipeline +list 响应
|
||||
│ ├── pm_dashboards.json # pm +dashboards 响应
|
||||
│ ├── wiki_pages.json # wiki +pages 响应
|
||||
│ ├── user_me.json # user +me 响应
|
||||
│ ├── user_heatmap.json # user +heatmap 响应
|
||||
│ ├── notification_list.json # notification +list 响应
|
||||
│ └── search_code.json # search +code 响应
|
||||
└── showcases/ # 演示脚本
|
||||
├── showcase.yml # 默认演示:涵盖所有新模块
|
||||
├── quick-tour.yml # 快速导览:5 分钟版
|
||||
└── deep-dive.yml # 深度演示:20 分钟版
|
||||
```
|
||||
|
||||
### 2.4 演示脚本格式(showcase.yml)
|
||||
|
||||
```yaml
|
||||
# demo/showcases/showcase.yml
|
||||
name: "GitLink-CLI 全功能展示"
|
||||
description: "覆盖所有新增模块的完整演示"
|
||||
version: "1.0"
|
||||
estimated_time: "20min"
|
||||
|
||||
steps:
|
||||
- group: "开场"
|
||||
steps:
|
||||
- command: "version"
|
||||
description: "显示版本信息"
|
||||
- command: "--help"
|
||||
description: "展示命令全景"
|
||||
- command: "user +me"
|
||||
description: "验证用户身份"
|
||||
|
||||
- group: "新增 Shortcut 命令"
|
||||
steps:
|
||||
- command: "webhook +list --owner jiangtx --repo gitlink-cli-demo"
|
||||
description: "Webhook 管理(从无到有)"
|
||||
- command: "webhook +create --owner jiangtx --repo gitlink-cli-demo --url https://example.com --events push"
|
||||
description: "创建 Webhook"
|
||||
highlight: true
|
||||
- command: "label +list --owner jiangtx --repo gitlink-cli-demo"
|
||||
description: "标签管理 CRUD"
|
||||
- command: "label +create -n bug -c '#d73a4a' --owner jiangtx --repo gitlink-cli-demo"
|
||||
description: "创建标签(自定义颜色)"
|
||||
- command: "notification +list"
|
||||
description: "通知中心"
|
||||
- command: "pm +dashboards --project 1"
|
||||
description: "项目管理看板"
|
||||
- command: "wiki +pages --owner jiangtx --repo gitlink-cli-demo"
|
||||
description: "Wiki 文档管理"
|
||||
|
||||
- group: "优化现有命令"
|
||||
steps:
|
||||
- command: "repo +list --format json"
|
||||
description: "JSON 格式输出"
|
||||
- command: "repo +list --format table"
|
||||
description: "Table 格式输出"
|
||||
- command: "repo +list --format yaml"
|
||||
description: "YAML 格式输出"
|
||||
- command: "issue +list --debug"
|
||||
description: "调试模式 — 查看完整请求链路"
|
||||
|
||||
- group: "批量操作"
|
||||
steps:
|
||||
- command: "issue +batch-close --numbers 1,2,3,4,5 --dry-run"
|
||||
description: "批量操作安全预览"
|
||||
- command: "member +batch-add --user-ids 101,102,103 --dry-run"
|
||||
description: "批量添加成员预览"
|
||||
- command: "export +issues --format csv --output demo_issues.csv"
|
||||
description: "数据导出 — 支撑科研分析"
|
||||
|
||||
- group: "跨平台体验"
|
||||
steps:
|
||||
- command: "status"
|
||||
description: "登录状态 + 平台信息"
|
||||
- command: "browse"
|
||||
description: "打开 GitLink 网页端"
|
||||
- command: "show repo"
|
||||
description: "获取仓库网页链接"
|
||||
|
||||
- group: "工作流 Agent"
|
||||
steps:
|
||||
- command: "workflow +health --owner jiangtx --repo gitlink-cli --format table"
|
||||
description: "仓库健康检查"
|
||||
- command: "workflow +triage --title 'Bug: crash' --body 'app crashes on start' --format markdown"
|
||||
description: "Issue 自动分类"
|
||||
- command: "workflow +repo-report --owner jiangtx --repo gitlink-cli --format markdown"
|
||||
description: "仓库综合报告"
|
||||
|
||||
- group: "收尾"
|
||||
steps:
|
||||
- command: "capability +summary"
|
||||
description: "API 能力探测结果"
|
||||
- command: "--help"
|
||||
description: "完整命令全景"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 三、核心实现
|
||||
|
||||
### 3.1 Demo Mode Transport
|
||||
|
||||
```go
|
||||
// internal/demo/demo.go
|
||||
|
||||
// Transport 实现 http.RoundTripper 接口,替代真实网络调用
|
||||
type Transport struct {
|
||||
fixtures map[string][]byte // 路径 → JSON 响应
|
||||
recorder *Recorder // 可选:录制模式
|
||||
}
|
||||
|
||||
func (t *Transport) RoundTrip(req *http.Request) (*http.Response, error) {
|
||||
// 1. 构建 fixture key: "GET:/api/v1/jiangtx/demo/issues.json?page=1"
|
||||
key := buildFixtureKey(req)
|
||||
|
||||
// 2. 查找预置响应
|
||||
if data, ok := t.fixtures[key]; ok {
|
||||
return mockResponse(200, data), nil
|
||||
}
|
||||
|
||||
// 3. 模糊匹配(忽略查询参数变化)
|
||||
if data, ok := t.fuzzyMatch(key); ok {
|
||||
return mockResponse(200, data), nil
|
||||
}
|
||||
|
||||
// 4. 录制模式:转发真实请求并记录响应
|
||||
if t.recorder != nil {
|
||||
return t.recorder.RoundTrip(req)
|
||||
}
|
||||
|
||||
// 5. 默认响应
|
||||
return mockResponse(200, []byte(`{"ok":true,"data":{}}`)), nil
|
||||
}
|
||||
```
|
||||
|
||||
### 3.2 Demo 命令
|
||||
|
||||
```go
|
||||
// cmd/demo/demo.go
|
||||
|
||||
func NewDemoCmd() *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "demo",
|
||||
Short: "演示模式 — 无需 Token 即可展示全部功能",
|
||||
}
|
||||
|
||||
cmd.AddCommand(
|
||||
newDemoRunCmd(), // demo +run <showcase>
|
||||
newDemoRecordCmd(), // demo +record
|
||||
newDemoReplayCmd(), // demo +replay
|
||||
)
|
||||
return cmd
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 四、演示脚本运行器
|
||||
|
||||
### 4.1 交互式执行
|
||||
|
||||
```bash
|
||||
gitlink-cli demo +run showcase
|
||||
```
|
||||
|
||||
输出效果:
|
||||
```text
|
||||
╔══════════════════════════════════════════════╗
|
||||
║ GitLink-CLI 全功能展示 ║
|
||||
║ 预计时长: 20min | 步数: 30 ║
|
||||
╚══════════════════════════════════════════════╝
|
||||
|
||||
━━━ 开场 (2/2) ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
|
||||
[1/30] ▶ gitlink-cli version
|
||||
gitlink-cli v3.0.0 (windows/amd64)
|
||||
|
||||
[2/30] ▶ gitlink-cli user +me
|
||||
{
|
||||
"ok": true,
|
||||
"data": {
|
||||
"login": "jiangtx",
|
||||
"user_id": 148911
|
||||
}
|
||||
}
|
||||
|
||||
━━━ 新增 Shortcut 命令 (7/7) ━━━━━━━━━━━━━━━━━━
|
||||
|
||||
[3/30] ▶ gitlink-cli webhook +list ...
|
||||
{
|
||||
"ok": true,
|
||||
"data": { "total_count": 1, "webhooks": [...] }
|
||||
}
|
||||
🔗 网页对应: https://gitlink.org.cn/jiangtx/gitlink-cli-demo/settings/hooks
|
||||
|
||||
[按 Enter 继续下一步, 输入 s 跳过本组, 输入 q 退出]
|
||||
```
|
||||
|
||||
### 4.2 自动执行模式
|
||||
|
||||
```bash
|
||||
# 自动执行,每步间隔 3 秒
|
||||
gitlink-cli demo +run showcase --auto --delay 3s
|
||||
|
||||
# 只执行指定分组
|
||||
gitlink-cli demo +run showcase --group "新增 Shortcut 命令"
|
||||
|
||||
# 列出所有可用演示脚本
|
||||
gitlink-cli demo +run --list
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 五、对展示方案的增量价值
|
||||
|
||||
| 原方案痛点 | Demo Mode 解决方案 |
|
||||
|-----------|-------------------|
|
||||
| Token 必需 → 演示脆弱 | `--demo` flag / `GITLINK_DEMO=1` → 零依赖 |
|
||||
| 网络不稳定 → 演示卡顿 | Mock Transport → 本地响应,零延迟 |
|
||||
| 只能看终端输出 → 不直观 | `show` + `browse` → 终端+网页联动 |
|
||||
| 演示步骤多 → 容易出错 | `demo +run showcase.yml` → 预编排脚本 |
|
||||
| 无法复现 → 学生无法练习 | `demo +replay` → 一键复现 |
|
||||
| 演示过程无记录 → 无法回顾 | `demo +record` → 录制真实操作 |
|
||||
|
||||
---
|
||||
|
||||
## 六、实现优先级
|
||||
|
||||
| 优先级 | 功能 | 理由 |
|
||||
|--------|------|------|
|
||||
| P0 | `--demo` flag + Mock Transport | 核心演示基础设施,无此无法脱网演示 |
|
||||
| P0 | `fixtures/` 预置数据 | 覆盖所有新模块的关键命令 |
|
||||
| P1 | `demo +run` 演示脚本 | 按步执行、分组展示 |
|
||||
| P1 | `show` 命令 | 终端↔网页映射的核心入口 |
|
||||
| P2 | `--web` 全局 flag | 自动打开浏览器的便利功能 |
|
||||
| P2 | `demo +record/replay` | 录制和回放 |
|
||||
| P3 | 自动执行 + 延迟 | 无人值守的全自动演示 |
|
||||
|
|
@ -1,310 +0,0 @@
|
|||
# GitLink-CLI 终端↔网页功能对应展示系统
|
||||
|
||||
## 一、设计目标
|
||||
|
||||
使每个 CLI 命令都能**直观对应**到 GitLink 网页上的一个或多个页面,
|
||||
支撑课程演示中"终端操作"与"网页效果"的同步对比展示。
|
||||
|
||||
### 核心原则
|
||||
|
||||
| # | 原则 | 说明 |
|
||||
|---|------|------|
|
||||
| 1 | **一对多映射** | 一个 CLI 命令可以对应多个网页资源 |
|
||||
| 2 | **零额外参数** | 从命令上下文自动推断 URL |
|
||||
| 3 | **不打断流程** | `--web` 可附加到任何命令,自动打开对应网页 |
|
||||
| 4 | **可演示性** | `show` 命令精确打印 URL,用于投影展示 |
|
||||
| 5 | **AI Agent 友好** | 结构化输出中包含 `html_url` 字段 |
|
||||
|
||||
---
|
||||
|
||||
## 二、新增命令体系
|
||||
|
||||
### 2.1 `show` — 打印网页 URL(不打开)
|
||||
|
||||
```text
|
||||
gitlink-cli show <resource> [flags]
|
||||
|
||||
核心设计理念:终端操作完,直接用 show 获取对应网页链接。
|
||||
适用于演示场景——先在终端执行,再投影网页效果。
|
||||
```
|
||||
|
||||
```bash
|
||||
# 显示仓库主页 URL
|
||||
gitlink-cli show repo
|
||||
# → https://gitlink.org.cn/jiangtx/gitlink-cli-demo
|
||||
|
||||
# 显示 Issue #42 的 URL
|
||||
gitlink-cli show issue --number 42
|
||||
# → https://gitlink.org.cn/jiangtx/gitlink-cli-demo/issues/42
|
||||
|
||||
# 显示 PR 网页 URL
|
||||
gitlink-cli show pr --number 128
|
||||
# → https://gitlink.org.cn/jiangtx/gitlink-cli-demo/pulls/128
|
||||
|
||||
# 显示 Wiki 页面 URL
|
||||
gitlink-cli show wiki --page "API 使用指南"
|
||||
# → https://gitlink.org.cn/jiangtx/gitlink-cli-demo/wiki/API%20使用指南
|
||||
|
||||
# 显示成员管理页 URL
|
||||
gitlink-cli show member
|
||||
# → https://gitlink.org.cn/jiangtx/gitlink-cli-demo/settings/collaboration
|
||||
|
||||
# 显示 Webhook 设置页 URL
|
||||
gitlink-cli show webhook
|
||||
# → https://gitlink.org.cn/jiangtx/gitlink-cli-demo/settings/hooks
|
||||
|
||||
# 显示标签管理页 URL
|
||||
gitlink-cli show label
|
||||
# → https://gitlink.org.cn/jiangtx/gitlink-cli-demo/settings/labels
|
||||
|
||||
# 显示里程碑页 URL
|
||||
gitlink-cli show milestone
|
||||
# → https://gitlink.org.cn/jiangtx/gitlink-cli-demo/milestones
|
||||
|
||||
# 输出为 JSON(给 AI Agent 用)
|
||||
gitlink-cli show issue --number 42 --format json
|
||||
# → {"resource":"issue","number":42,"html_url":"https://...","cli_command":"..."}
|
||||
```
|
||||
|
||||
**子命令全景**:
|
||||
|
||||
| 子命令 | URL 模板 | 需要参数 |
|
||||
|--------|---------|---------|
|
||||
| `show repo` | `/{owner}/{repo}` | --owner, --repo |
|
||||
| `show issue` | `/{owner}/{repo}/issues/{number}` | --number |
|
||||
| `show pr` | `/{owner}/{repo}/pulls/{number}` | --number |
|
||||
| `show wiki` | `/{owner}/{repo}/wiki/{page}` | --page |
|
||||
| `show member` | `/{owner}/{repo}/settings/collaboration` | --owner, --repo |
|
||||
| `show webhook` | `/{owner}/{repo}/settings/hooks` | --owner, --repo |
|
||||
| `show label` | `/{owner}/{repo}/issues/labels` | --owner, --repo |
|
||||
| `show milestone` | `/{owner}/{repo}/milestones` | --owner, --repo |
|
||||
| `show branch` | `/{owner}/{repo}/branches/{branch}` | --branch |
|
||||
| `show release` | `/{owner}/{repo}/releases/{tag}` | --tag |
|
||||
| `show ci` | `/{owner}/{repo}/actions` | --owner, --repo |
|
||||
| `show commit` | `/{owner}/{repo}/commits/{sha}` | --sha |
|
||||
|
||||
### 2.2 `--web` 全局 Flag — 执行命令后自动打开网页
|
||||
|
||||
```bash
|
||||
# 创建 Issue 后自动在浏览器打开
|
||||
gitlink-cli issue +create -t "Bug" -b "描述..." --web
|
||||
|
||||
# 创建 Webhook 后打开 Webhook 设置页
|
||||
gitlink-cli webhook +create --url https://example.com --events push --web
|
||||
|
||||
# 创建 PR 后打开 PR 页面
|
||||
gitlink-cli pr +create -t "feat: new" --head feature/x --base master --web
|
||||
|
||||
# 查看 Issue 的同时打开网页
|
||||
gitlink-cli issue +view --number 42 --web
|
||||
```
|
||||
|
||||
**`--web` 工作流程**:
|
||||
```
|
||||
用户命令 → Shortcut.Run() → API调用成功
|
||||
│
|
||||
▼
|
||||
检测 --web flag → 从响应提取 resource_id
|
||||
│
|
||||
▼
|
||||
构建 GitLink URL → 尝试打开浏览器
|
||||
│
|
||||
▼
|
||||
输出追加: "🔗 网页链接: https://gitlink.org.cn/..."
|
||||
```
|
||||
|
||||
### 2.3 `browse` 命令增强
|
||||
|
||||
```bash
|
||||
# 现有功能保留
|
||||
gitlink-cli browse # 打开仓库主页
|
||||
gitlink-cli browse issues/42 # 打开 Issue
|
||||
gitlink-cli browse pulls/128 # 打开 PR
|
||||
gitlink-cli browse wiki # 打开 Wiki
|
||||
|
||||
# 新增功能
|
||||
gitlink-cli browse --list # 列出当前仓库所有可浏览的页面
|
||||
gitlink-cli browse --no-open # 只打印 URL,不打开浏览器
|
||||
gitlink-cli browse releases/v2.0 # 打开 Release 页面
|
||||
gitlink-cli browse settings # 打开仓库设置页
|
||||
gitlink-cli browse commits/abc1234 # 打开指定提交
|
||||
```
|
||||
|
||||
### 2.4 输出增强:每个命令的数据自动附带 `html_url`
|
||||
|
||||
```json
|
||||
{
|
||||
"ok": true,
|
||||
"data": {
|
||||
"id": 42,
|
||||
"number": 42,
|
||||
"title": "Bug: Login failed",
|
||||
"state": "open",
|
||||
"html_url": "https://gitlink.org.cn/jiangtx/gitlink-cli-demo/issues/42"
|
||||
},
|
||||
"meta": {
|
||||
"show_url": "gitlink-cli show issue --number 42"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
`html_url` 字段由 `internal/output/envelope.go` 的 `SuccessEnvelope` 自动注入,
|
||||
前提是响应数据中包含 `id`/`number` 和 `owner`/`repo` 上下文。
|
||||
|
||||
---
|
||||
|
||||
## 三、URL 模式映射总表
|
||||
|
||||
### 3.1 GitLink 网页 URL 模式
|
||||
|
||||
| 资源类型 | URL 模式 | CLI 操作 | Show 命令 |
|
||||
|---------|---------|---------|----------|
|
||||
| 仓库主页 | `https://gitlink.org.cn/{owner}/{repo}` | repo +info | `show repo` |
|
||||
| 仓库设置 | `.../{owner}/{repo}/settings` | — | `show repo --settings` |
|
||||
| Issue 列表 | `.../{owner}/{repo}/issues` | issue +list | `show repo --tab issues` |
|
||||
| Issue 详情 | `.../{owner}/{repo}/issues/{number}` | issue +view | `show issue --number N` |
|
||||
| New Issue | `.../{owner}/{repo}/issues/new` | issue +create | `show issue --new` |
|
||||
| PR 列表 | `.../{owner}/{repo}/pulls` | pr +list | `show repo --tab pulls` |
|
||||
| PR 详情 | `.../{owner}/{repo}/pulls/{number}` | pr +view | `show pr --number N` |
|
||||
| New PR | `.../{owner}/{repo}/pulls/new` | pr +create | `show pr --new` |
|
||||
| Wiki 首页 | `.../{owner}/{repo}/wiki` | wiki +pages | `show wiki` |
|
||||
| Wiki 页面 | `.../{owner}/{repo}/wiki/{page}` | wiki +get | `show wiki --page P` |
|
||||
| 标签管理 | `.../{owner}/{repo}/issues/labels` | label +list | `show label` |
|
||||
| 里程碑 | `.../{owner}/{repo}/milestones` | milestone +list | `show milestone` |
|
||||
| 成员管理 | `.../{owner}/{repo}/settings/collaboration` | member +list | `show member` |
|
||||
| Webhook | `.../{owner}/{repo}/settings/hooks` | webhook +list | `show webhook` |
|
||||
| 分支列表 | `.../{owner}/{repo}/branches` | branch +list | `show branch` |
|
||||
| Release | `.../{owner}/{repo}/releases/{tag}` | release +view | `show release --tag T` |
|
||||
| CI/CD | `.../{owner}/{repo}/actions` | ci +list | `show ci` |
|
||||
| 提交详情 | `.../{owner}/{repo}/commits/{sha}` | repo +commits | `show commit --sha S` |
|
||||
| 对比页面 | `.../{owner}/{repo}/compare/{base}...{head}` | compare +view | `show compare --base B --head H` |
|
||||
| 项目管理 | `.../{owner}/{repo}/projects` | pm +dashboards | `show repo --tab projects` |
|
||||
| 通知中心 | `https://gitlink.org.cn/notifications` | notification +list | `show notification` |
|
||||
| 组织页面 | `https://gitlink.org.cn/{org}` | org +info | `show org --name N` |
|
||||
| 用户主页 | `https://gitlink.org.cn/{user}` | user +info | `show user --login L` |
|
||||
|
||||
---
|
||||
|
||||
## 四、实现架构
|
||||
|
||||
### 4.1 新增文件
|
||||
|
||||
```text
|
||||
gitlink-cli/
|
||||
├── cmd/
|
||||
│ ├── show/ # ★ 新增
|
||||
│ │ ├── show.go # show 主命令
|
||||
│ │ ├── show_test.go
|
||||
│ │ ├── url_patterns.go # URL 模式注册表
|
||||
│ │ └── url_patterns_test.go
|
||||
│ └── browse/
|
||||
│ ├── browse.go # 增强(增加 --list, --no-open, 更多资源类型)
|
||||
│ └── browse_test.go # 增强
|
||||
├── internal/
|
||||
│ ├── web/ # ★ 新增
|
||||
│ │ ├── url_builder.go # GitLink URL 构造器
|
||||
│ │ ├── url_builder_test.go
|
||||
│ │ ├── browser.go # 跨平台浏览器打开(从 browse 迁移)
|
||||
│ │ └── browser_test.go
|
||||
│ └── output/
|
||||
│ └── envelope.go # 增强(注入 html_url)
|
||||
```
|
||||
|
||||
### 4.2 核心接口设计
|
||||
|
||||
```go
|
||||
// internal/web/url_builder.go
|
||||
|
||||
// ResourceURL 表示一个 GitLink 网页资源 URL
|
||||
type ResourceURL struct {
|
||||
URL string `json:"html_url"` // 完整 URL
|
||||
Resource string `json:"resource"` // 资源类型: issue, pr, repo, wiki...
|
||||
Identifier string `json:"identifier"` // 资源标识: issue#42, pr#128...
|
||||
CLICommand string `json:"show_command"` // 对应的 show 命令
|
||||
}
|
||||
|
||||
// Builder 构造 GitLink 网页 URL
|
||||
type Builder struct {
|
||||
BaseURL string // https://gitlink.org.cn
|
||||
}
|
||||
|
||||
func (b *Builder) IssueURL(owner, repo string, number int) *ResourceURL
|
||||
func (b *Builder) PRURL(owner, repo string, number int) *ResourceURL
|
||||
func (b *Builder) RepoURL(owner, repo string) *ResourceURL
|
||||
func (b *Builder) WikiURL(owner, repo, page string) *ResourceURL
|
||||
func (b *Builder) MemberURL(owner, repo string) *ResourceURL
|
||||
func (b *Builder) WebhookURL(owner, repo string) *ResourceURL
|
||||
func (b *Builder) LabelURL(owner, repo string) *ResourceURL
|
||||
func (b *Builder) MilestoneURL(owner, repo string) *ResourceURL
|
||||
func (b *Builder) BranchURL(owner, repo, branch string) *ResourceURL
|
||||
func (b *Builder) ReleaseURL(owner, repo, tag string) *ResourceURL
|
||||
func (b *Builder) CommitURL(owner, repo, sha string) *ResourceURL
|
||||
func (b *Builder) CIURL(owner, repo string) *ResourceURL
|
||||
func (b *Builder) CompareURL(owner, repo, base, head string) *ResourceURL
|
||||
func (b *Builder) OrgURL(org string) *ResourceURL
|
||||
func (b *Builder) UserURL(login string) *ResourceURL
|
||||
```
|
||||
|
||||
### 4.3 实现分步
|
||||
|
||||
**Phase 1**: 创建 `internal/web/` 包(URL Builder + Browser)
|
||||
**Phase 2**: 创建 `cmd/show/` 命令(show 子命令)
|
||||
**Phase 3**: 增强 `cmd/browse/` 命令(更多资源类型)
|
||||
**Phase 4**: 在 `shortcuts/common/runner.go` 中加入 `--web` 后处理
|
||||
**Phase 5**: 在 `internal/output/envelope.go` 中自动注入 `html_url`
|
||||
**Phase 6**: 添加测试覆盖所有 URL 模式
|
||||
|
||||
### 4.4 演示场景示例
|
||||
|
||||
```bash
|
||||
# === 演示 1: Issue 创建 → 网页查看 ===
|
||||
# 终端 Step 1
|
||||
gitlink-cli issue +create -t "演示Issue" -b "教学内容"
|
||||
# → ✓ Issue #99 创建成功
|
||||
|
||||
# 终端 Step 2: 获取网页链接
|
||||
gitlink-cli show issue --number 99
|
||||
# → 🔗 https://gitlink.org.cn/jiangtx/gitlink-cli-demo/issues/99
|
||||
|
||||
# 投影仪:打开浏览器,展示 Issue #99 页面
|
||||
|
||||
# === 演示 2: Webhook 配置 → 网页验证 ===
|
||||
gitlink-cli webhook +create --url https://my-ci.com/hook --events push,create
|
||||
# → ✓ Webhook 创建成功 (id: 12345)
|
||||
|
||||
gitlink-cli show webhook
|
||||
# → 🔗 https://gitlink.org.cn/jiangtx/gitlink-cli-demo/settings/hooks
|
||||
|
||||
# 投影仪:打开浏览器,展示 Webhook 设置页面
|
||||
|
||||
# === 演示 3: 批量操作 → 网页查看批量结果 ===
|
||||
gitlink-cli issue +batch-close --numbers 1,2,3,4,5
|
||||
# → ✓ 已关闭 5 个 Issue
|
||||
|
||||
gitlink-cli show repo --tab issues?state=closed
|
||||
# → 🔗 https://gitlink.org.cn/jiangtx/gitlink-cli-demo/issues?state=closed
|
||||
|
||||
# 投影仪:浏览器显示已关闭的 Issue 列表
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 五、对展示方案的增量价值
|
||||
|
||||
### 5.1 原方案缺失的问题
|
||||
|
||||
原展示方案中每条命令都在终端运行,听众看不到 GitLink 网页端的实际效果。
|
||||
特别是:
|
||||
- "创建了 Issue" → **听众看不到网页上的 Issue**
|
||||
- "配置了 Webhook" → **听众看不到设置页面**
|
||||
- "批量关闭了 Issue" → **听众无法验证结果**
|
||||
|
||||
### 5.2 本案的解决方案
|
||||
|
||||
`show` + `--web` + `browse` 三层体系实现了"终端操作 → 网页验证"的闭环:
|
||||
1. 讲师终端运行 CLI 命令
|
||||
2. `show` 打印 / `--web` 自动打开对应网页
|
||||
3. 投影仪展示 GitLink 网页端效果
|
||||
4. 听众直观理解 CLI 与 Web 的对应关系
|
||||
|
||||
这使得课程演示从"纯终端操作展示"升级为"终端↔网页联动展示"。
|
||||
|
|
@ -1,195 +0,0 @@
|
|||
# 子任务一 验证报告:GitLink-CLI 能力增强(全量补强 + 系统化验证)
|
||||
|
||||
> 对齐 `课程实践任务及要求 - 0613.pdf` 子任务一与 `doc/DEMO-SCRIPT-SUBTASK1.md` 五大类别 + 7 项验收清单。
|
||||
>
|
||||
> 复现入口:`bash scripts/verify.sh`(七段全绿 = 子任务一达标)。
|
||||
|
||||
---
|
||||
|
||||
## 1. 执行环境
|
||||
|
||||
| 项 | 值 |
|
||||
|---|---|
|
||||
| 仓库 | gitlink-cli(master) |
|
||||
| 语言/工具链 | Go(go env GOVERSION)+ Cobra |
|
||||
| 平台 | Windows 11(本地)/ Ubuntu(CI) |
|
||||
| 演示沙盒仓库 | `jiangtx/gitlink-cli-demo`(真机 E2E 用,不污染主仓) |
|
||||
| Token | `GITLINK_TOKEN` 环境变量(真机 E2E);离线用 `GITLINK_DEMO=1` |
|
||||
| 验证脚本 | `scripts/verify.sh`(七段,可 `bash verify.sh <段号>` 单段跑) |
|
||||
|
||||
---
|
||||
|
||||
## 2. 段 1:编译 + 静态检查
|
||||
|
||||
| 检查项 | 命令 | 结果 |
|
||||
|---|---|---|
|
||||
| 编译 | `go build ./...` | ✅ |
|
||||
| 静态分析 | `go vet ./...` | ✅ |
|
||||
| 格式化 | `gofmt -s -l .`(空 = 通过) | ✅ |
|
||||
| i18n 校验 | `go run ./internal/i18n/cmd/check` | ✅(修复 `en-US.json` 格式后通过) |
|
||||
|
||||
---
|
||||
|
||||
## 3. 段 2:单元测试 + 覆盖率
|
||||
|
||||
```
|
||||
go test ./... -coverprofile=coverage.out
|
||||
total: 80.1%(≥ 80% 门禁 ✅)
|
||||
```
|
||||
|
||||
- 测试规模:70+ 个 `_test.go`、800+ 用例(含本次新增 8 个测试文件)。
|
||||
- 无网/无 Token:`httptest` mock 服务器(36+ 处)+ 新增 `internal/demo` Mock Transport 双保险。
|
||||
- **CI 门禁已落地**(P1-1):
|
||||
- `Makefile` `cover` 目标加阈值判定(`COVER_THRESHOLD ?= 78`,留余量)
|
||||
- `.github/workflows/test.yml`:`make cover` + 上传 coverage artifact
|
||||
- `.gitea/workflows/ci.yml`:`make test` → `make cover`
|
||||
- `.devops/ci.yml`:SSH 流水线加 `-coverprofile` + `[ $COV -ge 78 ]` 门禁
|
||||
|
||||
---
|
||||
|
||||
## 4. 段 3:命令注册冒烟
|
||||
|
||||
`gitlink-cli --help` 与各 group `--help` 全部注册无误:
|
||||
|
||||
- **P0 新增**:`show`(12 子命令)、`demo`(+run)、`browse`(增强)
|
||||
- **第一类 8 模块**:`webhook`/`wiki`/`pm`/`pipeline`/`label`/`member`/`milestone`/`notification` 全部 `+help` 退出 0
|
||||
- **既有**:`auth`/`config`/`api`/`version`/`alias`/`status` 注册完整
|
||||
|
||||
---
|
||||
|
||||
## 5. 段 5:终端 ↔ 网页联动(核心答辩点,P0 新增)
|
||||
|
||||
> 实现:`internal/web/url_builder.go`(16 条 URL 模式)+ `cmd/show/`(12 子命令)+ `--web` 后处理 + `envelope.Meta.html_url` 注入。
|
||||
|
||||
| 终端命令 | 输出 URL | 状态 |
|
||||
|---|---|---|
|
||||
| `show repo --owner jiangtx --repo gitlink-cli-demo` | `🔗 https://gitlink.org.cn/jiangtx/gitlink-cli-demo` | ✅ |
|
||||
| `show issue --owner o --repo r --number 42 --format json` | `{html_url:".../issues/42"}` | ✅ |
|
||||
| `show webhook --owner o --repo r` | `🔗 .../settings/hooks` | ✅ |
|
||||
| `show wiki/label/milestone/member/ci` | 各对应设置页 | ✅ |
|
||||
| `demo +run --list` | 列出 showcase / quick-tour | ✅ |
|
||||
| `browse --list` | 12 种可浏览资源目录 | ✅ |
|
||||
| `browse --no-open issues/42` | 只打印 `.../issues/42` 不打开 | ✅ |
|
||||
| `issue +create ... --web`(任意 create 类) | 执行后追加 `🔗 网页链接` 并打开 | ✅ |
|
||||
|
||||
**设计亮点**:`show` 默认 `🔗 <URL>`(投影友好),`--format json` 走 `output.Print` envelope(AI Agent 友好)。`browse` 全部走 `web.Builder`,消除双份 URL 表。
|
||||
|
||||
---
|
||||
|
||||
## 6. 段 6:五大类别功能 E2E(Demo Mock,离线)
|
||||
|
||||
> 开启 `GITLINK_DEMO=1`,所有命令走内置 Mock(`internal/demo`,20 个 fixtures + 通用 fallback)。
|
||||
|
||||
| 类别 | 命令 | 结果 |
|
||||
|---|---|---|
|
||||
| **第一类·新增模块** | `webhook +list` / `label +list` / `milestone +list` / `member +list` / `notification +list` | ✅ 全 `"ok": true` |
|
||||
| **第二类·多格式输出** | `repo +list --format json/table/yaml` | ✅ 三格式均可 |
|
||||
| **第三类·批量 dry-run** | `issue +batch-close --numbers 1,2,3 --dry-run` | ✅ `[DRY RUN]` |
|
||||
| **第五类·Raw API 补全** | `repo +languages` | ✅ 返回语言占比 |
|
||||
|
||||
(第四类跨平台安装:keyring 多平台 + npm postinstall 已在 README 验证;HTML 检测:`internal/client.detectHTMLResponse` + 中文指引已就绪。)
|
||||
|
||||
---
|
||||
|
||||
## 7. 段 7:format 统一 + --debug 增强(P1-2 + P2-2)
|
||||
|
||||
| 项 | 命令 | 结果 |
|
||||
|---|---|---|
|
||||
| alias 走 output 体系 | `alias +list --format json` | ✅ `{ok:true, data:[{name, command}]}` |
|
||||
| capability 走 output | `capability +check --format json` | ✅ 结构化结果 |
|
||||
| --debug 请求行 | `issue +list --debug` | ✅ `[DEBUG] → GET ...` |
|
||||
| --debug Header redact | 同上 | ✅ `Authorization: Bearer *** (transport-injected)` |
|
||||
| --debug 耗时 + body | 同上 | ✅ `← 200 OK (NNms, NB)` + `body[:200]` |
|
||||
|
||||
---
|
||||
|
||||
## 8. 终端 ↔ 网页映射总表(对齐 DEMO-SCRIPT-SUBTASK1.md)
|
||||
|
||||
16 条映射,全部由 `internal/web.Builder` 单一来源生成(`show`/`browse`/`--web` 共用):
|
||||
|
||||
| 资源 | URL 模式 |
|
||||
|---|---|
|
||||
| repo | `/{owner}/{repo}` |
|
||||
| issue | `/{owner}/{repo}/issues[/{n}]` |
|
||||
| pr | `/{owner}/{repo}/pulls[/{n}]` |
|
||||
| wiki | `/{owner}/{repo}/wiki[/{page}]` |
|
||||
| webhook | `/{owner}/{repo}/settings/hooks` |
|
||||
| label | `/{owner}/{repo}/issues/labels` |
|
||||
| milestone | `/{owner}/{repo}/milestones` |
|
||||
| member | `/{owner}/{repo}/settings/collaboration` |
|
||||
| branch | `/{owner}/{repo}/branches[/{name}]` |
|
||||
| release | `/{owner}/{repo}/releases[/{tag}]` |
|
||||
| commit | `/{owner}/{repo}/commits[/{sha}]` |
|
||||
| ci | `/{owner}/{repo}/actions` |
|
||||
| compare | `/{owner}/{repo}/compare/{base}...{head}` |
|
||||
| org / user / notification | `/{org}` / `/{user}` / `/notifications` |
|
||||
|
||||
---
|
||||
|
||||
## 9. 验收清单对照(DEMO-SCRIPT-SUBTASK1.md §B)
|
||||
|
||||
| 验收项 | 状态 | 证据 |
|
||||
|---|---|---|
|
||||
| 新增 Shortcut 命令(8 模块 53 命令) | ✅ | `shortcuts/register.go`(21 域组) |
|
||||
| 优化现有命令(--format/--debug/中文帮助/**show/--web**) | ✅ | P0/P1/P2 |
|
||||
| 批量操作(batch-close/batch-add/export/dry-run) | ✅ | `issue/batch.go`、`member/member.go`、`export/` |
|
||||
| 跨平台兼容(5 安装方式 + keyring 多平台) | ✅ | README + `internal/auth/token_store.go` |
|
||||
| 补全 Raw API(56 命令 + HTML 检测) | ✅ | `repo/user/org` shortcuts + `client.detectHTMLResponse` |
|
||||
| 测试覆盖率 ≥ 80% | ✅ | **80.1%** + CI 门禁(78% 缓冲阈值) |
|
||||
| Skills ~90 处修复 | ✅ | 已在前期提交完成 |
|
||||
| **★ 终端↔网页联动**(show/--web/html_url/demo) | ✅ | **本次 P0 新增** |
|
||||
|
||||
---
|
||||
|
||||
## 10. 本次(验证周期)新增/修改的文件
|
||||
|
||||
**新增**(10 个源文件 + 20 fixtures + 验证脚本/报告):
|
||||
- `internal/web/url_builder.go` + `browser.go` + 测试(覆盖率 94%)
|
||||
- `cmd/show/show.go` + 测试(90%)
|
||||
- `cmd/demo/demo.go` + `showcases.go` + 测试(94%)
|
||||
- `internal/demo/demo.go` + `loader.go` + `fixtures/*.json`(20)+ 测试(91-97%)
|
||||
- `shortcuts/common/web_post.go` + 测试(--web 后处理)
|
||||
- `shortcuts/capability/capability_test.go`
|
||||
- `scripts/verify.sh`、`doc/VERIFICATION-REPORT-SUBTASK1.md`
|
||||
|
||||
**修改**:
|
||||
- `cmd/root.go`:注册 show/demo,加 `--web`/`--demo` 全局 flag
|
||||
- `cmd/cmdutil/globals.go`:加 `Web`/`Demo` 全局变量
|
||||
- `cmd/browse/browse.go`:增强 `--list`/`--no-open`/实体识别,复用 `web.Builder`
|
||||
- `cmd/alias/alias.go`:`+list` 支持 `--format json/yaml/table`
|
||||
- `internal/output/envelope.go`:`Meta` 加 `HTMLURL`/`ShowURL`
|
||||
- `internal/client/client.go`:注入 demo Transport + `--debug` 增强(Header redact + 耗时 + body 长度)
|
||||
- `shortcuts/common/types.go`:`RuntimeContext.LastEnvelope` 缓存
|
||||
- `shortcuts/common/runner.go`:`--web` 后处理 + 测试注入点
|
||||
- `shortcuts/capability/capability.go`:`+check` 支持 `--format`,结构化输出
|
||||
- `Makefile` + `.github/.gitea/.devops` 三处 CI:覆盖率门禁
|
||||
|
||||
---
|
||||
|
||||
## 11. 已知限制 + 后续工作
|
||||
|
||||
| 项 | 现状 | 后续 |
|
||||
|---|---|---|
|
||||
| `demo +record/replay` | 标 TODO(P0 未实现) | 录制/回放真实操作流 |
|
||||
| i18n 全量抽取 | 新命令(show/demo)用字面中文,i18n check 通过 | 后续把字面量抽取为 `tr.T()` key |
|
||||
| `--web` 自动打开浏览器 | headless CI 仅打印 URL | 桌面环境自动 `open/xdg-open` |
|
||||
| `status` 命令 format | 保持人类可读文本 | 可按 alias 模式补结构化分支 |
|
||||
| 真机 E2E | 本报告 Demo Mock 验证全绿 | 答辩现场配 `GITLINK_TOKEN` 跑真机(沙盒仓 `jiangtx/gitlink-cli-demo`) |
|
||||
|
||||
---
|
||||
|
||||
## 复现命令
|
||||
|
||||
```bash
|
||||
# 一键验证(七段全绿 = 子任务一达标)
|
||||
bash scripts/verify.sh
|
||||
|
||||
# 离线演示(无网无 Token)
|
||||
GITLINK_DEMO=1 gitlink-cli demo +run showcase
|
||||
|
||||
# 真机 E2E(配 Token)
|
||||
export GITLINK_TOKEN=...
|
||||
gitlink-cli capability +check
|
||||
gitlink-cli issue +list --owner jiangtx --repo gitlink-cli-demo
|
||||
gitlink-cli show issue --owner jiangtx --repo gitlink-cli-demo --number 42
|
||||
```
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
# 新增 Raw API 批处理执行器
|
||||
|
||||
`gitlink-cli api` 现在支持通过 `--batch-file` 读取 JSON 批处理计划,把多个尚未封装为 shortcut 的 GitLink API 请求组织成一次可审计的自动化执行。计划文件支持 `vars` 模板变量,`--var key=value` 可以在执行时覆盖变量,便于同一批处理流程复用到不同仓库、Issue 或分支。
|
||||
|
||||
批处理模式提供 `--dry-run` 预览渲染后的 method、path、query 和 body,不会访问远端;实际执行时会输出每一步的成功/失败、响应数据和汇总计数。默认遇到失败立即停止,传入 `--continue-on-error` 后会继续执行后续请求,适合批量巡检、批量评论、批量元数据修复等场景。
|
||||
|
||||
本次变更包含计划文件解析、模板渲染、query/body 递归替换、失败控制、结构化汇总输出、中英文帮助文案、README 示例、Skill reference 和单元测试。测试覆盖 dry-run 不发请求、变量覆盖、模板缺失报错、失败默认中断以及失败继续执行等关键行为。
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
# 认证凭据 fallback 配置目录一致性修复
|
||||
|
||||
`gitlink-cli` 的主配置文件已经支持通过 `GITLINK_CONFIG_DIR` 指定配置目录,但认证模块在系统 Keychain 不可用时仍然把 fallback 凭据写到用户 home 下的 `~/.config/gitlink-cli/credentials`。这会让 CI、Windows 测试、Agent 沙箱和多账号隔离场景出现配置目录与凭据目录不一致的问题,也会导致测试中设置临时 HOME 后仍读写真实用户目录。
|
||||
|
||||
本次修复让文件凭据路径统一复用 `internal/config.ConfigDir()`:设置 `GITLINK_CONFIG_DIR` 时,fallback 凭据保存到 `$GITLINK_CONFIG_DIR/credentials`;未设置时仍保持原有默认路径。`auth logout` 在 fallback 文件不存在时也改为幂等成功,避免用户已经没有本地凭据时退出登录反而报错。
|
||||
|
||||
测试同步改为使用 `GITLINK_CONFIG_DIR` 隔离凭据目录,覆盖默认配置目录、文件创建、保存/读取/删除、Keychain 不可用 fallback、无凭据登出等场景。该修复提升了跨平台稳定性,也让本地全量测试不再因为 Windows `HOME`/`USERPROFILE` 解析差异污染真实用户凭据目录。
|
||||
|
|
@ -0,0 +1,70 @@
|
|||
# Dataset Shortcuts
|
||||
|
||||
## Summary
|
||||
|
||||
Adds a new `dataset` shortcut group for managing and querying GitLink research
|
||||
datasets, which previously had no shortcut coverage. Datasets carry
|
||||
research-oriented metadata (title, description, `paper_content`, license, owning
|
||||
project) that is valuable for research/scientometric scenarios.
|
||||
|
||||
## Commands
|
||||
|
||||
| Command | Purpose | Endpoint |
|
||||
|---------|---------|----------|
|
||||
| `gitlink-cli dataset +view` | View a repository's dataset and attachments | `GET /v1/{owner}/{repo}/dataset` |
|
||||
| `gitlink-cli dataset +list --ids <ids>` | List datasets for one or more projects | `GET /v1/project_datasets` |
|
||||
| `gitlink-cli dataset +create` | Create a repository's dataset | `POST /v1/{owner}/{repo}/dataset` |
|
||||
| `gitlink-cli dataset +update` | Update a repository's dataset | `PUT /v1/{owner}/{repo}/dataset` |
|
||||
| `gitlink-cli dataset +delete-attachment --uuid <uuid>` | Delete a dataset attachment | `DELETE /attachments/{uuid}` |
|
||||
|
||||
## Behaviour
|
||||
|
||||
- `+view` paginates attachments via `--page`/`--limit`.
|
||||
- `+list --ids 1,2,3` queries datasets by comma-separated numeric project IDs;
|
||||
IDs are validated client-side before the request.
|
||||
- `+create`/`+update` send `title`, `description`, optional `license-id`
|
||||
(validated as a positive integer) and `paper-content`. Both support
|
||||
`--dry-run` to preview the request body without writing.
|
||||
- `+delete-attachment` is destructive: it requires `--dry-run` preview or an
|
||||
explicit `--yes` confirmation before issuing the DELETE.
|
||||
|
||||
## Production status (verified)
|
||||
|
||||
Verified against production `gitlink.org.cn`:
|
||||
|
||||
- `GET /v1/project_datasets` (`+list`) — **available and verified** (e.g.
|
||||
`--ids 5988` returns the forgeplus dataset).
|
||||
- The per-repository routes `/v1/{owner}/{repo}/dataset`
|
||||
(`+view`/`+create`/`+update`) currently return `404` on production www
|
||||
(confirmed even for a repository's own owner; not reachable on the gateway
|
||||
host either). They follow the documented contract and are expected to work
|
||||
once the platform deploys these routes. `+delete-attachment` targets the
|
||||
generic attachments endpoint.
|
||||
|
||||
The commands and request shapes match the published OpenAPI spec, so they are
|
||||
ready the moment the routes go live; unit tests exercise every command against a
|
||||
mock server.
|
||||
|
||||
## Tests
|
||||
|
||||
Unit tests cover the view path with pagination, `--ids` normalization and
|
||||
validation, create/update request bodies and `license-id` validation, dry-run
|
||||
previews, and the destructive-delete confirmation guard (`--yes`).
|
||||
|
||||
## 中文说明
|
||||
|
||||
### 变更内容
|
||||
|
||||
- 新增 `dataset` 命令组:`+view`、`+list`、`+create`、`+update`、`+delete-attachment`。
|
||||
- `+view` 支持 `--page`/`--limit` 对附件分页;`+list --ids` 按项目 ID 查询。
|
||||
- `+create`/`+update` 发送 `title`/`description`/可选 `license-id`/`paper-content`,均支持 `--dry-run` 预览。
|
||||
- `+delete-attachment` 为破坏性操作,需 `--dry-run` 预览或显式 `--yes` 确认。
|
||||
|
||||
### 生产状态(已验证)
|
||||
|
||||
- `GET /v1/project_datasets`(`+list`)在生产**可用并已验证**(如 `--ids 5988` 返回 forgeplus 数据集)。
|
||||
- `/v1/{owner}/{repo}/dataset` 的 `+view`/`+create`/`+update` 当前在生产 www 返回 `404`(即使对仓库 owner 也如此,gateway 也未托管)。实现严格遵循已发布的 OpenAPI 契约,待平台部署后即可生效;单测以 mock 覆盖全部命令。
|
||||
|
||||
### 相对文档契约的增强
|
||||
|
||||
双语 i18n 帮助文案、写操作 `--dry-run` 预览、破坏性删除 `--yes` 二次确认、`license-id` 正整数校验。
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
# CLI 自诊断命令
|
||||
|
||||
新增 `gitlink-cli doctor`,用于在用户遇到“无法认证、仓库识别失败、配置异常、API 请求失败”等问题时快速定位原因。命令会一次性检查配置文件是否存在且可解析、`base_url` 和 `default_format` 是否合理、本地 Token 或 `GITLINK_TOKEN` 是否可用、当前目录能否解析出 GitLink 仓库上下文,以及认证 API 是否能正常返回当前用户。
|
||||
|
||||
输出沿用项目已有的 `ok/data/error/meta` 结构,诊断结果包含每个检查项的状态、说明、细节和可执行修复建议,便于人类阅读,也便于 Agent 或 CI 解析。默认会验证认证 API 连通性,`--skip-network` 可在离线环境或 CI 中只做本地检查。
|
||||
|
||||
本次变更同时补充了中英文帮助文案、README 使用示例和单元测试。测试覆盖了正常本地检查、损坏配置文件、非法 `base_url`、仓库上下文缺失、认证 API mock 成功,以及命令 JSON envelope 输出,确保诊断命令在常见失败场景下返回结构化结果而不是直接崩溃。
|
||||
|
|
@ -1,15 +0,0 @@
|
|||
# Export shortcut
|
||||
|
||||
新增 `export` Shortcut 组,将仓库数据导出为 CSV / JSON 文件,支撑离线分析、科研数据抽取与外部报表:
|
||||
|
||||
- `export +issues` — 导出 Issue 列表(GET `/v1/:owner/:repo/issues`)
|
||||
- `export +prs` — 导出 PR 列表(GET `/v1/:owner/:repo/pulls`)
|
||||
- `export +contributors` — 导出贡献者统计(GET `/:owner/:repo/contributors`)
|
||||
|
||||
实现要点:
|
||||
|
||||
- 统一 `--format csv|json`(默认 csv)与 `--output` 输出路径;`issues`/`prs` 额外支持 `--state open|closed|all` 过滤与 `--page/--limit` 分页。
|
||||
- CSV 表头固定(`id,title,state,created_at` 等),便于直接导入 Excel / pandas;JSON 保留原始字段,供 `workflow` 模块与科研 Skill 二次处理。
|
||||
- 导出过程只读、分页拉取全量,避免一次性请求超限。
|
||||
|
||||
背景:此前要做仓库数据导出只能手工拼 Raw API 并自行解析分页。`export` 组将其提升为一等命令,是子任务四科研场景(贡献排行、Issue 趋势、PR 效率)的数据入口,并与 `gitlink-contributor-insight`、`gitlink-research-tracker` 等 Skill 衔接。关联 PR #15。
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
# Ignore shortcut
|
||||
|
||||
新增 `ignore` Shortcut 组,补齐 GitLink 忽略文件模板(`.gitignore`)查询:
|
||||
|
||||
- `ignore +list`
|
||||
|
||||
同时补充了单元测试、README 示例。
|
||||
|
||||
|
|
@ -0,0 +1,68 @@
|
|||
# Issue batch maintenance shortcuts
|
||||
|
||||
## Summary
|
||||
|
||||
Add OpenAPI-backed Issue batch maintenance shortcuts:
|
||||
|
||||
- `issue +batch-update` — batch update Issue status, priority, milestone, tags, and assigners by API issue IDs.
|
||||
- `issue +batch-delete` — batch delete Issues by API issue IDs with explicit confirmation.
|
||||
|
||||
This complements the existing `issue +batch-close` command. `batch-close` uses web URL issue numbers, while the OpenAPI batch update/delete endpoints use API issue IDs.
|
||||
|
||||
## OpenAPI coverage
|
||||
|
||||
| Command | Method | Endpoint |
|
||||
|---|---|---|
|
||||
| `issue +batch-update` | PATCH | `/api/v1/{owner}/{repo}/issues/batch_update.json` |
|
||||
| `issue +batch-delete` | DELETE | `/api/v1/{owner}/{repo}/issues/batch_destroy.json` |
|
||||
|
||||
## ID semantics
|
||||
|
||||
- `issue +batch-close --numbers` uses web URL Issue numbers (`project_issues_index`).
|
||||
- `issue +batch-update --ids` and `issue +batch-delete --ids` use API Issue IDs returned by Issue APIs.
|
||||
|
||||
The docs and help text explicitly call this out to avoid mixing the two ID types.
|
||||
|
||||
## Safety and usability
|
||||
|
||||
- Both commands support `--dry-run`.
|
||||
- `issue +batch-update` requires at least one update field.
|
||||
- `issue +batch-delete` is destructive and requires `--yes` for real execution.
|
||||
- ID lists are validated as positive integers and de-duplicated.
|
||||
|
||||
## Examples
|
||||
|
||||
```bash
|
||||
gitlink-cli issue +batch-update \
|
||||
--owner Gitlink \
|
||||
--repo forgeplus \
|
||||
--ids 101,102 \
|
||||
--status-id 3 \
|
||||
--priority-id 2 \
|
||||
--tag-ids 7,8 \
|
||||
--assigner-ids 11,12 \
|
||||
--dry-run
|
||||
|
||||
gitlink-cli issue +batch-delete \
|
||||
--owner Gitlink \
|
||||
--repo forgeplus \
|
||||
--ids 101,102 \
|
||||
--dry-run
|
||||
|
||||
gitlink-cli issue +batch-delete \
|
||||
--owner Gitlink \
|
||||
--repo forgeplus \
|
||||
--ids 101,102 \
|
||||
--yes
|
||||
```
|
||||
|
||||
## Tests
|
||||
|
||||
```bash
|
||||
GOPROXY=https://goproxy.cn,direct go test ./...
|
||||
go vet ./...
|
||||
go run . issue +batch-update --help
|
||||
go run . issue +batch-delete --help
|
||||
go run . issue +batch-update --owner wangyue111 --repo gitlink-cli --ids 101,102 --status-id 3 --dry-run --format json
|
||||
go run . issue +batch-delete --owner wangyue111 --repo gitlink-cli --ids 101,102 --dry-run --format json
|
||||
```
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
# Issue/PR 列表筛选增强
|
||||
|
||||
本次变更修正并增强 `issue +list` 与 `pr +list` 的筛选能力。
|
||||
|
||||
此前 `issue +list --state open` 会向服务端发送 `state=open`,但 GitLink v1 Issue 列表接口实际使用 `category=opened/closed/all`,因此列表可能仍返回关闭 Issue。`pr +list --state open` 也没有映射到 PR 列表接口实际使用的 `status=0/1/2` 参数,Skill 文档中甚至需要提醒用户该参数可能只影响统计。现在两个命令都会保留原有 `--state` 用户体验,同时转换为服务端真实生效的参数。
|
||||
|
||||
新增筛选项:
|
||||
|
||||
- `issue +list` 支持 `--keyword`、`--participant`、`--author-id`、`--assignee-id`、`--milestone-id`、`--status-id`、`--tag-ids`、`--sort-by`、`--sort-direction`。
|
||||
- `pr +list` 支持 `--keyword`、`--priority-id`、`--tag-id`、`--milestone-id`、`--reviewer-id`、`--assignee-id`、`--sort-by`、`--sort-direction`。
|
||||
|
||||
单元测试覆盖了状态映射、筛选参数透传和 `all` 状态兼容;README、中文 README、Issue Skill 与 PR Skill 已同步更新。
|
||||
|
|
@ -1,18 +0,0 @@
|
|||
# PM (项目管理) shortcut
|
||||
|
||||
新增 `pm` Shortcut 组,封装 GitLink 项目管理相关只读接口,补齐仓库协作元数据的命令化访问:
|
||||
|
||||
- `pm +dashboards` — 查看项目仪表盘数据
|
||||
- `pm +sprints` — 查看 Sprint 任务列表
|
||||
- `pm +weekly` — 查看周报任务
|
||||
- `pm +tags` — 查看项目 Issue 标签
|
||||
- `pm +pipelines` — 查看项目 CI/CD 流水线列表
|
||||
- `pm +runs` — 查看项目 Action 运行记录
|
||||
|
||||
实现要点:
|
||||
|
||||
- 全部为只读(GET)命令,通过 Raw API 访问项目管理后端,统一 `owner/repo` 自动解析与 `--format json|table|yaml` 输出。
|
||||
- 面向「项目经理 / 科研课题负责人」视角:一条命令拿到仪表盘、Sprint、周报、流水线运行等聚合视图,无需在 Web 上多次跳转。
|
||||
- 与 `gitlink-pm` Skill 配套,供 AI Agent 做项目健康巡检与进度跟踪。
|
||||
|
||||
背景:项目管理数据此前散落在多个 Web 页面,无命令行入口。`pm` 组将其收敛为 6 条命令,是子任务三「项目一键初始化 / 进度跟踪」与子任务四「科研进度智能跟踪与预警」的基础数据层。关联 PR #12。
|
||||
|
|
@ -0,0 +1,56 @@
|
|||
# Profile (User Statistics) Shortcuts
|
||||
|
||||
## Summary
|
||||
|
||||
Adds a new read-only `profile` shortcut group that wraps GitLink's user statistics
|
||||
APIs (development ability, role positioning, major/discipline, recent activity, and
|
||||
contribution heatmap). These endpoints previously had no shortcut coverage, forcing
|
||||
agents to fall back to Raw API calls — the `gitlink-contributor-insight` skill even
|
||||
documents `user +stats`/`user +heatmap` as unavailable. The `profile` group surfaces
|
||||
the platform's native portrait data directly, powering "research subject portrait"
|
||||
scenarios.
|
||||
|
||||
## Commands
|
||||
|
||||
| Command | Purpose | Endpoint |
|
||||
|---------|---------|----------|
|
||||
| `gitlink-cli profile +ability` | Development ability scores + language breakdown | `GET /users/{user}/statistics/develop` |
|
||||
| `gitlink-cli profile +role` | Role positioning | `GET /users/{user}/statistics/role` |
|
||||
| `gitlink-cli profile +major` | Major/discipline categories | `GET /users/{user}/statistics/major` |
|
||||
| `gitlink-cli profile +activity` | Recent activity (issues/PRs/commits per day) | `GET /users/{user}/statistics/activity` |
|
||||
| `gitlink-cli profile +contribution` | Contribution heatmap | `GET /users/{user}/headmaps` |
|
||||
|
||||
## Behaviour
|
||||
|
||||
- `--user`/`-u` selects the target user. When omitted, the user is resolved from the
|
||||
authenticated account via `/users/me`, so `gitlink-cli profile +ability` works with
|
||||
no arguments.
|
||||
- `+ability`, `+role`, and `+major` accept optional `--start-time` / `--end-time`
|
||||
(Unix timestamps) that map to the `start_time` / `end_time` query parameters.
|
||||
- `+contribution` accepts an optional `--year` query parameter.
|
||||
|
||||
## Tests
|
||||
|
||||
Unit tests cover endpoint paths for every subcommand, the `start_time`/`end_time` and
|
||||
`year` query parameter mapping, current-user fallback via `/users/me`, the missing-login
|
||||
error path, and HTTP error handling.
|
||||
|
||||
## 中文说明
|
||||
|
||||
### 变更内容
|
||||
|
||||
- 新增 `profile` 命令组,封装 GitLink 用户画像统计接口:
|
||||
- `profile +ability` 开发能力评分(影响力/贡献度/活跃度/项目经验/语言能力)及语言分布
|
||||
- `profile +role` 角色定位
|
||||
- `profile +major` 专业/学科定位(如深度学习、量子计算)
|
||||
- `profile +activity` 近期活动统计(每日疑修/合并请求/提交数量)
|
||||
- `profile +contribution` 贡献热力图
|
||||
- `--user`/`-u` 指定目标用户;缺省时通过 `/users/me` 解析为当前认证用户。
|
||||
- `+ability`/`+role`/`+major` 支持 `--start-time`/`--end-time`(Unix 时间戳)。
|
||||
- `+contribution` 支持 `--year`。
|
||||
|
||||
### 价值
|
||||
|
||||
这些接口此前无任何 shortcut 封装,`gitlink-contributor-insight` Skill 甚至将
|
||||
`user +stats`/`user +heatmap` 标注为"不可用"并改用 PR 时间戳手工推算。`profile`
|
||||
命令组直接暴露平台原生画像数据,为"科研主体画像"等场景提供数据底座。
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
# Release Update Shortcuts
|
||||
|
||||
Submitter: Wang Yue
|
||||
|
||||
This change completes the release shortcut coverage for the release edit/update OpenAPI endpoints and improves release write safety.
|
||||
|
||||
## Commands
|
||||
|
||||
- Add `release +edit` for `/api/{owner}/{repo}/releases/{id}/edit.json`.
|
||||
- Add `release +update` for `PUT /api/{owner}/{repo}/releases/{id}.json`.
|
||||
- Extend `release +create` with `--draft` and `--attachment-ids`.
|
||||
- Extend `release +delete` with `--dry-run`.
|
||||
|
||||
## Behavior
|
||||
|
||||
- `release +update` fetches current edit data first, then preserves unspecified fields such as `name`, `tag_name`, `body`, `target_commitish`, `draft`, `prerelease`, and existing attachment IDs.
|
||||
- `release +update` validates boolean flags before reading remote data.
|
||||
- `release +update` and `release +delete` support `--dry-run` to preview write/delete requests.
|
||||
- `release +create` validates boolean flags and de-duplicates comma-separated attachment IDs.
|
||||
|
||||
## Verification
|
||||
|
||||
- Unit tests cover create payloads, edit endpoint routing, update field preservation, attachment overrides, dry-run behavior, and invalid argument validation.
|
||||
|
|
@ -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`
|
||||
|
|
@ -0,0 +1,40 @@
|
|||
# Repository Settings Shortcuts
|
||||
|
||||
Submitter: Wang Yue
|
||||
|
||||
This change expands repository shortcut coverage for repository metadata, settings, project topics, navigation units, and transfer OpenAPI endpoints.
|
||||
|
||||
## Commands
|
||||
|
||||
- `repo +detail`
|
||||
- `repo +simple`
|
||||
- `repo +settings`
|
||||
- `repo +units`
|
||||
- `repo +units-update`
|
||||
- `repo +topics`
|
||||
- `repo +topic-add`
|
||||
- `repo +topic-delete`
|
||||
- `repo +transfer-orgs`
|
||||
- `repo +transfer`
|
||||
- `repo +transfer-cancel`
|
||||
|
||||
## API Mapping
|
||||
|
||||
| Shortcut | Method | API path |
|
||||
|----------|--------|----------|
|
||||
| `repo +detail` | GET | `/api/{owner}/{repo}/detail.json` |
|
||||
| `repo +simple` | GET | `/api/{owner}/{repo}/simple.json` |
|
||||
| `repo +settings` | GET | `/api/{owner}/{repo}/edit.json` |
|
||||
| `repo +units` | GET | `/api/{owner}/{repo}/project_units.json` |
|
||||
| `repo +units-update` | POST | `/api/{owner}/{repo}/project_units.json` |
|
||||
| `repo +topics` | GET | `/api/v1/project_topics.json` |
|
||||
| `repo +topic-add` | POST | `/api/v1/project_topics.json` |
|
||||
| `repo +topic-delete` | DELETE | `/api/v1/project_topics/{id}.json` |
|
||||
| `repo +transfer-orgs` | GET | `/api/{owner}/{repo}/applied_transfer_projects/organizations.json` |
|
||||
| `repo +transfer` | POST | `/api/{owner}/{repo}/applied_transfer_projects.json` |
|
||||
| `repo +transfer-cancel` | POST | `/api/{owner}/{repo}/applied_transfer_projects/cancel.json` |
|
||||
|
||||
## Verification
|
||||
|
||||
- Unit tests cover request methods, paths, query parameters, JSON payloads, dry-run behavior, CSV de-duplication, and invalid project ID validation.
|
||||
- Write and state-changing commands support `--dry-run`.
|
||||
|
|
@ -0,0 +1,65 @@
|
|||
# repo +tree 仓库文件树查询命令
|
||||
|
||||
## 背景
|
||||
|
||||
`gitlink-cli repo` 已经提供仓库详情、README、语言统计和贡献者查询能力,但缺少直接查看仓库目录结构的 Shortcut。用户或 AI Agent 如果要判断仓库中是否存在 README、LICENSE、依赖清单、测试目录、文档目录等文件,过去需要手动调用 Raw API `/sub_entries`。
|
||||
|
||||
本次变更把仓库文件树查询封装为 `repo +tree`,降低普通用户和自动化工作流的使用门槛。
|
||||
|
||||
## 变更内容
|
||||
|
||||
- 新增 `gitlink-cli repo +tree` Shortcut。
|
||||
- 调用 `GET /{owner}/{repo}/sub_entries` 获取仓库根目录或指定目录下的文件和子目录。
|
||||
- 支持 `--path, -p` 指定目录路径;不传时查询仓库根目录。
|
||||
- 支持 `--ref, -r` 指定分支、标签或提交引用;默认值为 `master`。
|
||||
- 复用现有仓库上下文解析、API 调用和统一输出格式。
|
||||
- 补充中英文 i18n 文案,避免新增命令帮助信息硬编码。
|
||||
|
||||
## 命令示例
|
||||
|
||||
```bash
|
||||
# 查看仓库根目录
|
||||
gitlink-cli repo +tree --owner Gitlink --repo forgeplus --ref master
|
||||
|
||||
# 查看指定目录
|
||||
gitlink-cli repo +tree --owner Gitlink --repo forgeplus --path src --ref main
|
||||
|
||||
# Agent 场景建议使用 JSON 输出
|
||||
gitlink-cli repo +tree --owner Gitlink --repo forgeplus --format json
|
||||
```
|
||||
|
||||
## 参数说明
|
||||
|
||||
| 参数 | 必填 | 说明 |
|
||||
|------|------|------|
|
||||
| `--path, -p` | 否 | 要查看的目录路径,不传时查询仓库根目录 |
|
||||
| `--ref, -r` | 否 | 分支、标签或提交引用,默认 `master` |
|
||||
| `--owner` | 否 | 全局参数,仓库所有者,可从 git remote 自动解析 |
|
||||
| `--repo` | 否 | 全局参数,仓库名称,可从 git remote 自动解析 |
|
||||
| `--format` | 否 | 全局参数,输出格式:`json`、`table` 或 `yaml` |
|
||||
|
||||
## 测试覆盖
|
||||
|
||||
单元测试覆盖以下内容:
|
||||
|
||||
- 根目录查询默认使用 `master`。
|
||||
- 根目录查询不发送空 `filepath` 参数。
|
||||
- 指定 `--path` 和 `--ref` 时正确映射到 `filepath` 与 `ref` 查询参数。
|
||||
- `repo +tree` 的命令说明和 `--path/-p`、`--ref/-r` 参数注册完整。
|
||||
|
||||
验证命令:
|
||||
|
||||
```bash
|
||||
make test
|
||||
```
|
||||
|
||||
## 交付要求核对
|
||||
|
||||
- 功能代码:`shortcuts/repo/repo.go`
|
||||
- 单元测试:`shortcuts/repo/repo_test.go`
|
||||
- 命令帮助文档:`README.md`、`README.zh-CN.md`、`skills/gitlink-repo/SKILL.md`、`skills/gitlink-repo/references/gitlink-repo-tree.md`
|
||||
- 变更说明文档:`doc/changes/repo-tree-shortcut.md`
|
||||
|
||||
## 兼容性
|
||||
|
||||
该变更只新增 Shortcut、单元测试和文档,不修改已有命令参数或输出结构。根目录查询时不再发送空 `filepath` 查询参数,语义更清晰,对现有功能无破坏性影响。
|
||||
|
|
@ -1,17 +1,23 @@
|
|||
# Wiki shortcut
|
||||
# Wiki Shortcut
|
||||
|
||||
新增 `wiki` Shortcut 组,封装 GitLink 仓库 Wiki 页面的常用操作:
|
||||
新增 `wiki` Shortcut 组,支持 Wiki 页面管理:
|
||||
|
||||
- `wiki +pages` — 列出 Wiki 页面
|
||||
- `wiki +get` — 获取 Wiki 页面内容
|
||||
- `wiki +create` — 创建 Wiki 页面
|
||||
- `wiki +update` — 更新 Wiki 页面
|
||||
- `wiki +delete` — 删除 Wiki 页面
|
||||
- `wiki +list` - 列出 Wiki 页面(目录结构)
|
||||
- `wiki +view` - 按页面名称查看 Wiki 页面详情
|
||||
- `wiki +create` - 创建新的 Wiki 页面
|
||||
- `wiki +update` - 更新 Wiki 页面标题和/或内容
|
||||
- `wiki +delete` - 删除 Wiki 页面
|
||||
|
||||
实现要点:
|
||||
## 实现要点
|
||||
|
||||
- 列表/获取为 GET;创建/更新/删除为写操作,统一 `owner/repo` 自动解析与 `--format` 输出。
|
||||
- `+create` / `+update` 接收标题与正文(支持 Markdown),调用 Wiki 后端接口落库。
|
||||
- 命令化后可被 AI Agent 与 CI 脚本复用,例如自动发布周报 Wiki、同步文档。
|
||||
- **API 端点**:基于 `/api/wiki/open/{action}` 扁平路径结构,覆盖 5 个 Wiki 管理接口:
|
||||
- `GET /api/wiki/open/wikiPages` — 目录列表
|
||||
- `GET /api/wiki/open/getWiki` — 查看页面
|
||||
- `POST /api/wiki/open/createWiki` — 创建页面
|
||||
- `PUT /api/wiki/open/updateWiki` — 更新页面
|
||||
- `DELETE /api/wiki/open/deleteWiki` — 删除页面
|
||||
- **标识方式**:Wiki 页面通过 `pageName`(slug)标识,所有操作需要 `projectId`(GitLink 项目数字 ID)
|
||||
- **内容编码**:创建和更新时,内容自动进行 base64 编码后以 `content_base64` 字段发送
|
||||
- **更新保护**:`+update` 要求必须提供 `--title` 和 `--page-name`;`--content` 为可选
|
||||
- **Shortcut 模式**:使用 `common.Shortcut` + `RuntimeContext` 框架,与其他模块保持一致
|
||||
|
||||
背景:Wiki 管理此前只能在 Web 端手工进行,无法脚本化。`wiki` 组将其提升为一等命令,覆盖《课程实践任务》子任务一「新增 Wiki 管理」要求,并与 `gitlink-pm`(周报)、`gitlink-release-notes`(发布说明归档到 Wiki)等 Skill 衔接。关联 PR #13。
|
||||
|
|
|
|||
|
|
@ -1,104 +0,0 @@
|
|||
# alias — 命令别名管理
|
||||
|
||||
> 关联 Issue: #14 | PR: #11
|
||||
|
||||
## 概述
|
||||
|
||||
alias 命令用于管理 gitlink-cli 的命令别名,将常用长命令缩短为简短别名,提升日常使用效率。对标 `gh alias`。
|
||||
|
||||
## 命令列表
|
||||
|
||||
### alias +list
|
||||
- **用途**: 列出所有已定义的命令别名
|
||||
- **示例**: `gitlink-cli alias +list`
|
||||
|
||||
### alias +set \<name\> \<command\>
|
||||
- **用途**: 设置命令别名
|
||||
- **参数**: name(别名名称)和 command(实际命令)
|
||||
- **示例**:
|
||||
- `gitlink-cli alias +set rl "repo +list"`
|
||||
- `gitlink-cli alias +set ri "repo +info --owner Gitlink --repo gitlink-cli"`
|
||||
|
||||
### alias +delete \<name\>
|
||||
- **用途**: 删除已定义的命令别名
|
||||
- **参数**: name(要删除的别名名称)
|
||||
- **示例**: `gitlink-cli alias +delete rl`
|
||||
|
||||
## 存储
|
||||
|
||||
别名存储在 `~/.config/gitlink-cli/aliases.yaml` 文件中,格式为 YAML。
|
||||
|
||||
---
|
||||
|
||||
# browse — 浏览器打开 GitLink 页面
|
||||
|
||||
> 关联 Issue: #14 | PR: #11
|
||||
|
||||
## 概述
|
||||
|
||||
browse 命令用于在浏览器中快速打开当前仓库或指定资源的 GitLink 页面。对标 `gh browse`。
|
||||
|
||||
## 用法
|
||||
|
||||
```
|
||||
gitlink-cli browse [resource]
|
||||
```
|
||||
|
||||
- 不带参数:打开当前仓库主页
|
||||
- 带参数:打开指定资源页面
|
||||
|
||||
## 示例
|
||||
|
||||
```bash
|
||||
# 打开当前仓库主页
|
||||
gitlink-cli browse
|
||||
|
||||
# 打开指定 Issue
|
||||
gitlink-cli browse issues/42
|
||||
|
||||
# 打开指定 PR
|
||||
gitlink-cli browse pulls/128
|
||||
|
||||
# 打开 Wiki 页面
|
||||
gitlink-cli browse wiki
|
||||
```
|
||||
|
||||
## 跨平台支持
|
||||
|
||||
- macOS: 使用 `open` 命令
|
||||
- Windows: 使用 `start` 命令
|
||||
- Linux: 使用 `xdg-open` 命令
|
||||
|
||||
---
|
||||
|
||||
# status — 显示当前状态
|
||||
|
||||
> 关联 Issue: #14 | PR: #11
|
||||
|
||||
## 概述
|
||||
|
||||
status 命令显示 gitlink-cli 的当前状态信息,包括认证状态、API 地址、当前目录和自动推断的仓库信息。对标 `gh auth status`。
|
||||
|
||||
## 用法
|
||||
|
||||
```bash
|
||||
gitlink-cli status
|
||||
```
|
||||
|
||||
## 输出示例
|
||||
|
||||
```
|
||||
GitLink CLI 状态
|
||||
───────────────
|
||||
认证状态: 已登录
|
||||
Token 来源: keyring / 配置文件
|
||||
API 地址: https://www.gitlink.org.cn/api
|
||||
当前目录: /home/user/projects/gitlink-cli
|
||||
推断仓库: Gitlink/gitlink-cli
|
||||
```
|
||||
|
||||
## 功能说明
|
||||
|
||||
- 自动检测 Token 来源(环境变量 GITLINK_TOKEN / keyring / 配置文件)
|
||||
- 自动从 git remote 推断 owner/repo
|
||||
- 未登录时给出 `gitlink-cli auth login` 提示
|
||||
|
|
@ -1,30 +0,0 @@
|
|||
# capability — API 后端能力探测
|
||||
|
||||
> 关联:跨平台兼容性 / 各模块可用性自检
|
||||
|
||||
## 概述
|
||||
|
||||
`capability` 模块向 GitLink 后端发送探测请求,检查各命令模块依赖的 API 是否就绪。结果缓存 24 小时(`~/.config/gitlink-cli/capabilities.json`),用于在 `--help` 里给不可用模块标注 ⚠,调用时给出中文错误指引,提升跨实例兼容性。
|
||||
|
||||
## 命令列表
|
||||
|
||||
### capability +check — 探测后端能力
|
||||
向 GitLink 后端探测,检查各模块是否可用并缓存结果。
|
||||
- **探测域**:`label` `notification` `pm` `wiki` `pipeline` `webhook` `member` `milestone` `export` `search` `workflow`
|
||||
- **参数**:无显式参数;需要 owner/repo 上下文的域会自动从 `git remote` 推断,或用全局 `--owner/--repo` 指定。
|
||||
- **输出**:表格列出每个模块的状态(可用 ✓ / 不可用 ✗ / 错误 ✗ / 未知 ?)与说明。
|
||||
- **示例**:
|
||||
- `gitlink-cli capability +check`
|
||||
- `gitlink-cli capability +check --owner Gitlink --repo gitlink-cli`
|
||||
|
||||
### capability +list — 查看缓存结果
|
||||
读取上一次 `+check` 的缓存结果,不发起网络请求。
|
||||
- **示例**:
|
||||
- `gitlink-cli capability +list`
|
||||
- 缓存过期(>24h)会提示运行 `capability +check` 刷新。
|
||||
|
||||
## 与其它模块的关系
|
||||
|
||||
`register.go` 的 `annotatedDesc` 会在各命令组描述后追加状态标记:
|
||||
- 可用 → `✓`;不可用/错误 → `⚠`。
|
||||
因此 `gitlink-cli --help` 里带 ⚠ 的模块即表示后端暂不支持,调用前可先用 `capability +check` 确认。
|
||||
|
|
@ -1,34 +0,0 @@
|
|||
# compare — 分支/标签/提交对比
|
||||
|
||||
## 概述
|
||||
|
||||
`compare` 模块对比两个 ref(分支 / 标签 / commit)之间的差异,输出对比概览或变更文件列表。
|
||||
|
||||
## 命令列表
|
||||
|
||||
### compare +view — 对比两个 ref
|
||||
对比 `head` 相对 `base` 的差异概览。
|
||||
- **参数**:
|
||||
- `--head`(必填)源分支 / 标签 / commit
|
||||
- `--base`(必填)目标分支 / 标签 / commit
|
||||
- **API**:`GET /v1/:owner/:repo/compare/:head...:base`
|
||||
- **示例**:
|
||||
- `gitlink-cli compare +view --head feature-x --base master`
|
||||
- `gitlink-cli compare +view --owner Gitlink --repo gitlink-cli --head develop --base master`
|
||||
|
||||
### compare +files — 列出变更文件
|
||||
列出两个 ref 之间变更的文件。
|
||||
- **参数**:
|
||||
- `--head`(必填)源 ref
|
||||
- `--base`(必填)目标 ref
|
||||
- `-f, --file` 按文件路径过滤
|
||||
- `-p, --page` 页码,默认 `1`
|
||||
- `-l, --limit` 每页数量,默认 `20`
|
||||
- **API**:`GET /v1/:owner/:repo/compare/:head...:base/files`
|
||||
- **示例**:
|
||||
- `gitlink-cli compare +files --head feature-x --base master -l 50`
|
||||
- `gitlink-cli compare +files --head develop --base master -f "shortcuts/"`
|
||||
|
||||
## 输出
|
||||
|
||||
支持 `--format json|table|yaml`。
|
||||
|
|
@ -1,47 +0,0 @@
|
|||
# 跨平台兼容性验证报告
|
||||
|
||||
> 关联 Issue: #16 | PR: #13
|
||||
|
||||
## 测试矩阵
|
||||
|
||||
| 验证项 | Windows 11 | macOS | Ubuntu |
|
||||
|--------|:---:|:---:|:---:|
|
||||
| `git clone` + `go build ./...` | 待验证 | 待验证 | 待验证 |
|
||||
| `go test -race ./...` 全部通过 | 待验证 | 待验证 | 待验证 |
|
||||
| `gitlink-cli auth login` 登录 | 待验证 | 待验证 | 待验证 |
|
||||
| `gitlink-cli repo +list` 可用 | 待验证 | 待验证 | 待验证 |
|
||||
| `gitlink-cli pm +dashboards` 可用 | 待验证 | 待验证 | 待验证 |
|
||||
| `gitlink-cli wiki +pages` 可用 | 待验证 | 待验证 | 待验证 |
|
||||
| `gitlink-cli alias +list` 可用 | 待验证 | 待验证 | 待验证 |
|
||||
| `gitlink-cli browse` 打开浏览器 | 待验证 | 待验证 | 待验证 |
|
||||
| `gitlink-cli status` 显示状态 | 待验证 | 待验证 | 待验证 |
|
||||
| `gitlink-cli export +issues` 导出 | 待验证 | 待验证 | 待验证 |
|
||||
| Token 存储(keyring)正常 | 待验证 | 待验证 | 待验证 |
|
||||
|
||||
## 安装脚本
|
||||
|
||||
| 脚本 | 平台 | 路径 |
|
||||
|------|------|------|
|
||||
| install.sh | Linux / macOS | `scripts/install.sh` |
|
||||
| install.ps1 | Windows | `scripts/install.ps1` |
|
||||
|
||||
## CI 配置
|
||||
|
||||
| 配置文件 | 说明 |
|
||||
|---------|------|
|
||||
| `.devops/ci.yml` | 建木流水线:push 到 wyx_branch 时自动触发构建+测试+格式化检查 |
|
||||
|
||||
### CI 检查内容
|
||||
|
||||
| 检查项 | 命令 | 说明 |
|
||||
|--------|------|------|
|
||||
| 构建 | `go build ./...` | 确保代码编译通过 |
|
||||
| 静态分析 | `go vet ./...` | 检测常见代码问题 |
|
||||
| 测试 | `go test -race ./...` | 运行全部测试,含竞态检测 |
|
||||
| 格式化 | `gofmt -s -l .` | 确保代码格式符合 Go 标准 |
|
||||
|
||||
## 已知问题
|
||||
|
||||
1. **Windows keyring**: Windows Credential Manager 可能需要额外配置
|
||||
2. **Linux keyring**: 需要 dbus 服务支持,无桌面环境时可能不可用
|
||||
3. **browse 命令**: Linux 环境需要安装 xdg-utils 包
|
||||
|
|
@ -1,68 +0,0 @@
|
|||
# export — 数据导出命令
|
||||
|
||||
> 关联 Issue: #15 | PR: #12
|
||||
|
||||
## 概述
|
||||
|
||||
export 模块提供将仓库数据(Issue、PR、贡献者)导出为 CSV 或 JSON 文件的能力,支持离线分析和科研用途。
|
||||
|
||||
## 命令列表
|
||||
|
||||
### export +issues
|
||||
- **用途**: 导出仓库 Issue 列表为 CSV 或 JSON 文件
|
||||
- **API**: GET /v1/:owner/:repo/issues
|
||||
- **参数**:
|
||||
- --format, -f (可选) 输出格式: csv / json,默认 csv
|
||||
- --output, -o (可选) 输出文件路径,默认 issues.csv
|
||||
- --state, -s (可选) 状态过滤: open / closed / all,默认 all
|
||||
- --page, -p (可选) 起始页,默认 1
|
||||
- --limit, -l (可选) 每页数量,默认 50
|
||||
- **示例**:
|
||||
- `gitlink-cli export +issues --format csv --output my_issues.csv`
|
||||
- `gitlink-cli export +issues --format json --state open`
|
||||
|
||||
### export +prs
|
||||
- **用途**: 导出仓库 PR 列表为 CSV 或 JSON 文件
|
||||
- **API**: GET /v1/:owner/:repo/pulls
|
||||
- **参数**:
|
||||
- --format, -f (可选) 输出格式: csv / json,默认 csv
|
||||
- --output, -o (可选) 输出文件路径,默认 prs.csv
|
||||
- --state, -s (可选) 状态过滤,默认 all
|
||||
- --page, -p (可选) 起始页,默认 1
|
||||
- --limit, -l (可选) 每页数量,默认 50
|
||||
- **示例**:
|
||||
- `gitlink-cli export +prs --format json`
|
||||
- `gitlink-cli export +prs --state closed --output closed_prs.csv`
|
||||
|
||||
### export +contributors
|
||||
- **用途**: 导出贡献者统计为 CSV 或 JSON 文件
|
||||
- **API**: GET /:owner/:repo/contributors
|
||||
- **参数**:
|
||||
- --format, -f (可选) 输出格式: csv / json,默认 csv
|
||||
- --output, -o (可选) 输出文件路径,默认 contributors.csv
|
||||
- **示例**:
|
||||
- `gitlink-cli export +contributors --format json`
|
||||
|
||||
## CSV 输出格式
|
||||
|
||||
### issues.csv
|
||||
```csv
|
||||
id,title,state,created_at
|
||||
1,Bug fix,1,2026-01-01
|
||||
```
|
||||
|
||||
### prs.csv
|
||||
```csv
|
||||
id,title,state,created_at
|
||||
2,Feature PR,0,2026-02-01
|
||||
```
|
||||
|
||||
### contributors.csv
|
||||
```csv
|
||||
id,login,contributions
|
||||
1,dev1,42
|
||||
```
|
||||
|
||||
## 向后兼容性
|
||||
|
||||
无破坏性变更。所有命令通过 export 域组 + 前缀添加。
|
||||
|
|
@ -1,41 +0,0 @@
|
|||
# notification — 通知管理
|
||||
|
||||
## 概述
|
||||
|
||||
`notification` 模块封装 GitLink 通知相关 OpenAPI:列出通知、标记已读、关注/取消关注仓库通知。
|
||||
|
||||
## 命令列表
|
||||
|
||||
### notification +list — 列出通知
|
||||
- **参数**:
|
||||
- `-p, --page` 页码,默认 `1`
|
||||
- `-l, --limit` 每页数量,默认 `20`
|
||||
- **示例**:
|
||||
- `gitlink-cli notification +list`
|
||||
- `gitlink-cli notification +list -l 50`
|
||||
|
||||
### notification +read — 标记单条已读
|
||||
- **参数**:
|
||||
- `-i, --id`(必填)通知 ID
|
||||
- **示例**:
|
||||
- `gitlink-cli notification +read -i 12345`
|
||||
|
||||
### notification +read-all — 全部标记已读
|
||||
- **参数**:无
|
||||
- **示例**:
|
||||
- `gitlink-cli notification +read-all`
|
||||
|
||||
### notification +watch — 关注/取消关注仓库通知
|
||||
- **参数**:
|
||||
- `-o, --owner`(必填)仓库所有者
|
||||
- `-r, --repo`(必填)仓库名称
|
||||
- **示例**:
|
||||
- `gitlink-cli notification +watch -o Gitlink -r gitlink-cli`
|
||||
|
||||
## 输出
|
||||
|
||||
支持 `--format json|table|yaml`。
|
||||
|
||||
## 备注
|
||||
|
||||
该模块当前**尚无单元测试**(见 `开发日志.md` 待办),调用前建议用 `capability +check` 确认后端通知接口可用。
|
||||
|
|
@ -1,49 +0,0 @@
|
|||
# pm — 项目管理命令
|
||||
|
||||
> 关联 Issue: #12 | PR: #9
|
||||
|
||||
## 概述
|
||||
|
||||
pm 模块提供 GitLink 项目管理相关的命令,包括仪表盘、Sprint 任务、周报、标签、流水线和 Action 运行记录的查看。
|
||||
|
||||
## 命令列表
|
||||
|
||||
### pm +dashboards
|
||||
- **用途**: 查看项目仪表盘数据
|
||||
- **API**: GET /pm/dashboards?project_id=\<id\>
|
||||
- **参数**: --project (必填) 项目 ID
|
||||
- **示例**: `gitlink-cli pm +dashboards --project 123`
|
||||
|
||||
### pm +sprints
|
||||
- **用途**: 查看 Sprint 任务列表
|
||||
- **API**: GET /pm/sprint_issues?project_id=\<id\>
|
||||
- **参数**: --project (必填) 项目 ID
|
||||
- **示例**: `gitlink-cli pm +sprints --project 123`
|
||||
|
||||
### pm +weekly
|
||||
- **用途**: 查看周报任务
|
||||
- **API**: GET /pm/weekly_issues?project_id=\<id\>
|
||||
- **参数**: --project (必填) 项目 ID
|
||||
- **示例**: `gitlink-cli pm +weekly --project 123`
|
||||
|
||||
### pm +tags
|
||||
- **用途**: 查看项目 Issue 标签
|
||||
- **API**: GET /pm/issue_tags?project_id=\<id\>
|
||||
- **参数**: --project (必填) 项目 ID
|
||||
- **示例**: `gitlink-cli pm +tags --project 123`
|
||||
|
||||
### pm +pipelines
|
||||
- **用途**: 查看项目 CI/CD 流水线列表
|
||||
- **API**: GET /pm/pipelines?project_id=\<id\>
|
||||
- **参数**: --project (必填) 项目 ID
|
||||
- **示例**: `gitlink-cli pm +pipelines --project 123`
|
||||
|
||||
### pm +runs
|
||||
- **用途**: 查看项目 Action 运行记录
|
||||
- **API**: GET /pm/action_runs?project_id=\<id\>
|
||||
- **参数**: --project (必填) 项目 ID
|
||||
- **示例**: `gitlink-cli pm +runs --project 123`
|
||||
|
||||
## 向后兼容性
|
||||
|
||||
无破坏性变更。所有命令通过 pm 域组 + 前缀添加。
|
||||
|
|
@ -1,49 +0,0 @@
|
|||
# wiki — Wiki 管理命令
|
||||
|
||||
> 关联 Issue: #13 | PR: #10
|
||||
|
||||
## 概述
|
||||
|
||||
wiki 模块提供 GitLink 仓库 Wiki 页面的管理命令,支持列出、查看、创建、更新和删除 Wiki 页面。
|
||||
|
||||
## 命令列表
|
||||
|
||||
### wiki +pages
|
||||
- **用途**: 列出仓库所有 Wiki 页面
|
||||
- **API**: GET /api/wiki/wikiPages
|
||||
- **参数**: 无(自动从 git remote 推断 owner/repo)
|
||||
- **示例**: `gitlink-cli wiki +pages`
|
||||
|
||||
### wiki +get
|
||||
- **用途**: 获取指定 Wiki 页面内容
|
||||
- **API**: GET /api/wiki/getWiki?id=\<id\>
|
||||
- **参数**: --id, -i (必填) Wiki 页面 ID
|
||||
- **示例**: `gitlink-cli wiki +get --id 42`
|
||||
|
||||
### wiki +create
|
||||
- **用途**: 创建新的 Wiki 页面
|
||||
- **API**: POST /api/wiki/createWiki
|
||||
- **参数**:
|
||||
- --title, -t (必填) 页面标题
|
||||
- --content, -c (必填) 页面内容(Markdown)
|
||||
- --project (可选) 项目 ID
|
||||
- **示例**: `gitlink-cli wiki +create --title "Getting Started" --content "# Welcome"`
|
||||
|
||||
### wiki +update
|
||||
- **用途**: 更新已有 Wiki 页面
|
||||
- **API**: PUT /api/wiki/updateWiki
|
||||
- **参数**:
|
||||
- --id, -i (必填) Wiki 页面 ID
|
||||
- --title, -t (可选) 新标题
|
||||
- --content, -c (可选) 新内容(Markdown)
|
||||
- **示例**: `gitlink-cli wiki +update --id 42 --title "Updated Title"`
|
||||
|
||||
### wiki +delete
|
||||
- **用途**: 删除 Wiki 页面
|
||||
- **API**: POST /api/wiki/deleteWiki
|
||||
- **参数**: --id, -i (必填) Wiki 页面 ID
|
||||
- **示例**: `gitlink-cli wiki +delete --id 42`
|
||||
|
||||
## 向后兼容性
|
||||
|
||||
无破坏性变更。所有命令通过 wiki 域组 + 前缀添加。
|
||||
|
|
@ -1,67 +0,0 @@
|
|||
# workflow — AI Agent 工作流分析命令
|
||||
|
||||
> 关联:子任务三 端到端工作流 / `skills/gitlink-workflow`
|
||||
|
||||
## 概述
|
||||
|
||||
`workflow` 模块用**本地规则**对仓库做只读分析,输出结构化报告,供 AI Agent 与人类决策。所有命令默认**只读**(`--dry-run` 默认 true),不会对远端写入,适合在 PR/Issue 自动分拣、健康度评估等场景安全调用。
|
||||
|
||||
## 命令列表
|
||||
|
||||
### workflow +triage — Issue 分拣
|
||||
用本地规则对 Issue 分类(Bug/Feature/Question 等)、评估紧急度与复杂度,给出行动建议。
|
||||
- **参数**:
|
||||
- `--from` 从 JSON 文件读取 Issue(支持单条 / 数组 / `{issues:[]}`)
|
||||
- `-t, --title` 单条分析的 Issue 标题
|
||||
- `-b, --body` 单条分析的 Issue 正文
|
||||
- `-n, --number` 单条分析的 Issue 编号
|
||||
- `--author` / `--url` / `--labels` 单条分析的作者 / URL / 逗号分隔标签
|
||||
- `-s, --state` 过滤或指定状态,默认 `open`
|
||||
- `-p, --page` 远端分拣的 API 页码,默认 `1`
|
||||
- `-l, --limit` 最多分析条数,默认 `30`
|
||||
- `--since` 远端 Issue 更新时间过滤
|
||||
- `--dry-run` 只预览不写远端,默认 `true`
|
||||
- `--lang` 输出语言 `en` / `zh-CN`,默认 `en`
|
||||
- **示例**:
|
||||
- `gitlink-cli workflow +triage -n 42 -t "登录失败" -b "复现步骤..." --lang zh-CN`
|
||||
- `gitlink-cli workflow +triage --from issues.json --limit 50`
|
||||
- 远端批量:`gitlink-cli workflow +triage --owner Gitlink --repo gitlink-cli -s open -l 30`
|
||||
|
||||
### workflow +health — 仓库健康度评分
|
||||
输入仓库指标,用本地规则打分(活跃度 / CI / 文档 / Agent 就绪度)。
|
||||
- **参数**:
|
||||
- `--from` 从 JSON 文件读取指标
|
||||
- `--repository` 仓库名(`owner/repo`)
|
||||
- `--open-issues` / `--open-prs` 打开 Issue / PR 数(默认 0)
|
||||
- `--stale-issues` / `--stale-prs` 僵尸 Issue / PR 数(默认 0)
|
||||
- `--recent-activity-known` / `--recent-activity-days` 近期活动是否已知 / 距今天数
|
||||
- `--release-known` / `--has-recent-release` Release 状态是否已知 / 是否有近期 Release
|
||||
- `--ci-known` / `--ci-passing` CI 状态是否已知 / 是否通过
|
||||
- `--has-readme` / `--has-license` / `--has-contributing` 是否存在 README / LICENSE / CONTRIBUTING
|
||||
- `--agent-readiness-known` / `--agent-readiness-score` Agent 就绪度是否已知 / 0–10 评分
|
||||
- `--stale-days` 判定僵尸的天数阈值,默认 `30`
|
||||
- `--lang` 输出语言
|
||||
- **示例**:
|
||||
- `gitlink-cli workflow +health --repository Gitlink/gitlink-cli --open-issues 15 --open-prs 8 --ci-passing --has-readme --has-license --lang zh-CN`
|
||||
|
||||
### workflow +pr-summary — PR 只读评审摘要
|
||||
拉取指定 PR 的 diff/元数据,生成本地只读评审摘要。
|
||||
- **参数**:
|
||||
- `-n, --number` PR 编号(远端只读分析)
|
||||
- **示例**:
|
||||
- `gitlink-cli workflow +pr-summary --owner Gitlink --repo gitlink-cli -n 128`
|
||||
|
||||
### workflow +repo-report — 仓库工作流报告
|
||||
聚合仓库 Issue/PR/Release 等数据,生成本地只读工作流报告。
|
||||
- **示例**:
|
||||
- `gitlink-cli workflow +repo-report --owner Gitlink --repo gitlink-cli`
|
||||
|
||||
## 输出
|
||||
|
||||
支持 `--format json|table|yaml`。`json` 便于 Agent 后处理,`table` 便于人读。
|
||||
|
||||
## 设计要点
|
||||
|
||||
- **只读优先**:所有远端访问均为 GET,`triage` 的写操作受 `--dry-run` 控制。
|
||||
- **本地规则可复现**:评分/分拣逻辑在 `health_score.go` / `triage_rules.go`,无外部 AI 依赖,结果确定。
|
||||
- **与 Skill 互补**:`skills/gitlink-workflow/SKILL.md` 是给 Agent 的工作流剧本,本命令是其可复现的执行后端。
|
||||
|
|
@ -40,7 +40,7 @@ gitlink-cli/
|
|||
│ ├── common/
|
||||
│ │ ├── types.go # Shortcut / Flag / RuntimeContext 定义
|
||||
│ │ └── runner.go # CallAPI / PaginateAll / ResolveOwnerRepo
|
||||
│ ├── repo/ # repo +create / +clone / +fork / +list / +info
|
||||
│ ├── repo/ # repo +list / +info / +readme / +tree / +languages / +create ...
|
||||
│ ├── issue/ # issue +list / +create / +view / +close / +comment
|
||||
│ ├── pr/ # pr +list / +create / +view / +merge / +review
|
||||
│ ├── release/ # release +list / +create / +download
|
||||
|
|
@ -78,7 +78,7 @@ gitlink-cli/
|
|||
|
||||
| 领域 | Shortcuts | 数量 |
|
||||
|------|-----------|------|
|
||||
| repo | `+create` `+clone` `+fork` `+list` `+info` `+delete` `+settings` | 7 |
|
||||
| repo | `+list` `+info` `+readme` `+tree` `+languages` `+contributors` `+contributor-stats` `+code-stats` `+watchers` `+stargazers` `+follow` `+unfollow` `+like` `+unlike` `+create` `+fork` `+delete` | 17 |
|
||||
| issue | `+list` `+create` `+view` `+update` `+close` `+comment` `+assign` `+label` | 8 |
|
||||
| pr | `+list` `+create` `+view` `+merge` `+close` `+review` `+files` `+diff` | 8 |
|
||||
| release | `+list` `+create` `+view` `+delete` `+download` | 5 |
|
||||
|
|
|
|||
|
|
@ -1,43 +0,0 @@
|
|||
# 上游 PR 清单(竞赛·成果落地)
|
||||
|
||||
> 本文件汇总 jiangtx/gitlink-cli 小组向真正上游 `gitlink/gitlink-cli` 提交的全部 PR。
|
||||
> 竞赛评分:子赛题一「PR 已提交且通过 CI」为基础分,「被合并进主仓库」为高分;子赛题二「Skill 已提交收录申请」为基础分,「被官方收录」为高分。
|
||||
> 维护者 Review 通常 1–2 周;截止日后仍在 Review 的按"已提交且通过 CI / 已提交收录申请"计分。
|
||||
|
||||
每个 PR 都是**细粒度特性分支**(基于 `upstream/master`,只含该功能 = 功能代码 + 单元测试 + 命令帮助文档 + 变更说明),易被维护者 review 与合并。
|
||||
|
||||
## 子任务一·CLI 功能(5 个)
|
||||
|
||||
| # | 功能 | 分支 | 对比链接(开 PR) |
|
||||
|---|---|---|---|
|
||||
| 1 | export(issues/prs/contributors → CSV/JSON 导出,6 测试) | `pr-shortcut-export` | https://gitlink.org.cn/Gitlink/gitlink-cli/compare/master...jiangtx:pr-shortcut-export |
|
||||
| 2 | pm(项目管理:仪表盘/Sprint/周报等 6 命令) | `pr-shortcut-pm` | https://gitlink.org.cn/Gitlink/gitlink-cli/compare/master...jiangtx:pr-shortcut-pm |
|
||||
| 3 | notification(通知 list/read/read-all/watch,7 测试) | `pr-shortcut-notification` | https://gitlink.org.cn/Gitlink/gitlink-cli/compare/master...jiangtx:pr-shortcut-notification |
|
||||
| 4 | alias/browse/status(开发者体验命令) | `pr-cmd-dx-commands` | https://gitlink.org.cn/Gitlink/gitlink-cli/compare/master...jiangtx:pr-cmd-dx-commands |
|
||||
| 5 | capability + internal/capability(后端 API 能力探测) | `pr-shortcut-capability` | https://gitlink.org.cn/Gitlink/gitlink-cli/compare/master...jiangtx:pr-shortcut-capability |
|
||||
|
||||
## 子任务二·Skill 收录(2 个)
|
||||
|
||||
| # | Skill | 场景 | 分支 | 对比链接 |
|
||||
|---|---|---|---|---|
|
||||
| 6 | gitlink-release-notes | Release Notes 生成 | `pr-skill-release-notes` | https://gitlink.org.cn/Gitlink/gitlink-cli/compare/master...jiangtx:pr-skill-release-notes |
|
||||
| 7 | gitlink-compliance-check | 许可证合规检查 | `pr-skill-compliance-check` | https://gitlink.org.cn/Gitlink/gitlink-cli/compare/master...jiangtx:pr-skill-compliance-check |
|
||||
|
||||
## 子任务三·端到端编排 Skill(5 个)
|
||||
|
||||
| # | Skill | 串联场景 | 分支 | 对比链接 |
|
||||
|---|---|---|---|---|
|
||||
| 8 | gitlink-community-ops | 社区运营:Issue分类→分配→周报→Release | `pr-skill-community-ops` | https://gitlink.org.cn/Gitlink/gitlink-cli/compare/master...jiangtx:pr-skill-community-ops |
|
||||
| 9 | gitlink-pr-gate | 代码质量看门人:PR→Review→CI→合并 | `pr-skill-pr-gate` | https://gitlink.org.cn/Gitlink/gitlink-cli/compare/master...jiangtx:pr-skill-pr-gate |
|
||||
| 10 | gitlink-project-bootstrap | 项目一键初始化:建仓→README/LICENSE/CI→Issue | `pr-skill-project-bootstrap` | https://gitlink.org.cn/Gitlink/gitlink-cli/compare/master...jiangtx:pr-skill-project-bootstrap |
|
||||
| 11 | gitlink-multi-repo-ops | 多仓库协同:跨仓 Issue/PR/Release | `pr-skill-multi-repo-ops` | https://gitlink.org.cn/Gitlink/gitlink-cli/compare/master...jiangtx:pr-skill-multi-repo-ops |
|
||||
| 12 | gitlink-contributor-growth | 贡献者成长:活动→排行→徽章 | `pr-skill-contributor-growth` | https://gitlink.org.cn/Gitlink/gitlink-cli/compare/master...jiangtx:pr-skill-contributor-growth |
|
||||
|
||||
## 说明
|
||||
|
||||
- **细粒度**:每个 PR 一个特性分支,互不耦合,维护者可独立 review/合并。
|
||||
- **质量**:CLI PR 均含单元测试 + 帮助文档 + 变更说明;Skill PR 遵循 skills 规范(SKILL.md + frontmatter)。
|
||||
- **已核验不重复**:wiki/label/member/milestone/pipeline/webhook/compare/workflow/research-tracker 等上游已有,未重复提交。
|
||||
- **提交方式**:点对比链接 → 源分支 `jiangtx:<分支>` → 目标 `gitlink:master` → 粘贴标题与说明(见各 commit message)→ 创建合并请求。
|
||||
|
||||
> 提交手册与 PR 描述模板见 [UPSTREAM_PR_GUIDE.md](UPSTREAM_PR_GUIDE.md)。
|
||||
|
|
@ -1,111 +0,0 @@
|
|||
# 上游 PR 提交手册(子赛题一·成果落地)
|
||||
|
||||
> 目标:把本 fork 的成熟成果以 PR 形式提交到真正上游 `gitlink/gitlink-cli`,争取"已提交且通过 CI"基础分 → "被合并"高分。
|
||||
> 本手册由 Claude 生成,**push 与开 PR 需用你的 GitLink 账号执行**(我不能代操作)。
|
||||
|
||||
## 0. 一次性准备:配置 upstream 远程
|
||||
|
||||
```bash
|
||||
cd gitlink-cli
|
||||
git remote add upstream https://gitlink.org.cn/gitlink/gitlink-cli.git
|
||||
git fetch upstream
|
||||
git fetch origin
|
||||
# 确认
|
||||
git remote -v # 应看到 origin(jiangtx) + upstream(gitlink)
|
||||
```
|
||||
|
||||
## 1. 选哪些成果提(质量优先,建议 3–4 个 PR)
|
||||
|
||||
按"自带测试 + 自带文档 + 改动聚焦"挑选,下面的 commit 哈希均为本 fork 已验证的干净 feature 提交:
|
||||
|
||||
| PR | 主题 | 源 commit | 含测试 | 含文档 |
|
||||
|---|---|---|---|---|
|
||||
| **PR-1** | wiki 命令(5 条) | `6bcd8b6` | ✅ | ✅ |
|
||||
| **PR-2** | pm 命令(6 条) | `b4cfdb7` | ✅ | ✅ |
|
||||
| **PR-3** | export 命令(3 条) | `2cc0e5c` | ✅ | ✅ |
|
||||
| **PR-4** | alias/browse/status(开发者体验) | `a28a8ea` + `9474c1f`(测试) | ✅ | ✅ |
|
||||
| **PR-5** | pipeline 命令 | 见 `git log -- shortcuts/pipeline/` | ✅ | ✅ |
|
||||
| **PR-6** | webhook 命令 | 见 `git log -- shortcuts/webhook/` | ✅ | ✅ |
|
||||
| **PR-7(Skill 收录)** | gitlink-research-tracker Skill | 见 `git log -- skills/gitlink-research-tracker/` | — | ✅ |
|
||||
|
||||
> 建议**先提 PR-1 / PR-2 / PR-7**这三个最干净、最有代表性的。其余视维护者反馈再提。
|
||||
|
||||
## 2. 单个 PR 的标准流程(以 PR-1 wiki 为例)
|
||||
|
||||
```bash
|
||||
# 1) 基于上游 master 建干净分支
|
||||
git checkout -b pr-shortcut-wiki upstream/master
|
||||
|
||||
# 2) 挑入对应 commit(如只一个就用 cherry-pick;多个依次挑)
|
||||
git cherry-pick 6bcd8b6
|
||||
# 若 wiki 的帮助/变更文档在别的 commit,也一并 cherry-pick
|
||||
|
||||
# 3) 本地校验(需 Go;无 Go 则跳过,靠上游 CI)
|
||||
go build ./... && go vet ./... && go test ./shortcuts/wiki/... && gofmt -l .
|
||||
|
||||
# 4) 推到 fork
|
||||
git push origin pr-shortcut-wiki
|
||||
|
||||
# 5) 到 GitLink 网页:jiangtx/gitlink-cli → 合并请求 → 新建 → 源分支 pr-shortcut-wiki → 目标 gitlink/gitlink-cli:master
|
||||
```
|
||||
|
||||
> 若 cherry-pick 与上游冲突,通常是因为上游同期也改了 `register.go`。解决:手动把你的模块加进 `groups` map 与 `descriptions` map 即可。
|
||||
|
||||
## 3. PR 描述模板(直接复制填)
|
||||
|
||||
```markdown
|
||||
## 背景
|
||||
gitlink-cli 缺少 XXX 命令,需手工拼 Raw API。本 PR 新增 `xxx` 命令组。
|
||||
|
||||
## 改动
|
||||
- 新增 `shortcuts/xxx/`:N 条子命令(list/create/...)
|
||||
- 新增单元测试 `shortcuts/xxx/*_test.go`(覆盖 method/path/query/payload)
|
||||
- 新增帮助文档 `doc/commands/xxx.md` + 变更说明 `doc/changes/xxx-shortcut.md`
|
||||
|
||||
## 对应 OpenAPI
|
||||
- GET/POST `/v1/:owner/:repo/...`
|
||||
|
||||
## 测试方式
|
||||
\`\`\`bash
|
||||
go test ./shortcuts/xxx/... -race
|
||||
gofmt -l shortcuts/xxx/
|
||||
\`\`\`
|
||||
|
||||
## 截图
|
||||
(贴 1–2 张命令运行截图)
|
||||
|
||||
## 关联
|
||||
竞赛:GitLink 大赛 2026 子赛题一。fork:jiangtx/gitlink-cli。
|
||||
```
|
||||
|
||||
## 4. PR-7·Skill 收录(子赛题二)
|
||||
|
||||
```bash
|
||||
git checkout -b pr-skill-research-tracker upstream/master
|
||||
git cherry-pick <research-tracker 的 commit>
|
||||
git push origin pr-skill-research-tracker
|
||||
# 目标:gitlink/gitlink-cli:master,路径 skills/gitlink-research-tracker/
|
||||
```
|
||||
|
||||
PR 描述要点:
|
||||
- 说明这是**科研辅助场景**(技术调研:多关键词搜索→成熟度评分→趋势→选型),与现有 Skill 不重复
|
||||
- 附 `EXAMPLES.md` 里的 Agent 验证结果(调用次数/token/耗时/输出)
|
||||
- 注明兼容 Claude Code(已验证)/ OpenClaw / Cursor
|
||||
|
||||
## 5. 提交后:更新 README 的 PR 清单
|
||||
|
||||
每个 PR 开好后,回到本仓库 `README.md` 的「上游 PR 清单」表格,把 `_待提交_` 换成真实 PR 靶向链接与状态,并 commit。这是评委看到"成果落地"的直接证据。
|
||||
|
||||
## 6. 提交前自查清单(每个 PR 必须全绿)
|
||||
|
||||
- [ ] `go build ./...` 通过
|
||||
- [ ] `go test ./... -race` 通过
|
||||
- [ ] `gofmt -l .` 无输出(无未格式化文件)
|
||||
- [ ] 含单元测试
|
||||
- [ ] 含 `doc/commands/*.md` + `doc/changes/*.md`
|
||||
- [ ] PR 描述用模板
|
||||
- [ ] 不夹带无关改动(一个 PR 只做一件事)
|
||||
|
||||
## 7. 时间策略
|
||||
|
||||
今天(7/8)就提 PR-1、PR-2、PR-7。维护者 Review 1–2 周,越早提越可能合并。截止日仍在 Review 的,竞赛按"已提交且通过 CI"计基础分。
|
||||
|
|
@ -1,18 +0,0 @@
|
|||
# 课程实践报告
|
||||
|
||||
> 《软件演化与运维》进阶任务要求的三份报告,已纳入本仓库供评委查阅。
|
||||
> 源文件(含 docx 生成脚本)见小组工作区 `报告生成/`。
|
||||
|
||||
| 报告 | 文件 | 对应要求 |
|
||||
|---|---|---|
|
||||
| 软件分析及建模报告 | [软件分析及建模报告.docx](软件分析及建模报告.docx) | 子任务一:CLI 架构建模 |
|
||||
| 软件新需求构思报告 | [软件新需求构思报告.docx](软件新需求构思报告.docx) | 子任务一/二/三:新增命令与 Skill 的需求来源 |
|
||||
| 变更影响分析及测试报告 | [变更影响分析及测试报告.docx](变更影响分析及测试报告.docx) | 子任务一/二/三:测试与变更影响分析 |
|
||||
|
||||
## 报告与代码的对应关系
|
||||
|
||||
- **软件分析及建模报告**:建模对象为 `shortcuts/` 命令注册体系(`register.go` → 各模块 `Shortcuts()`)、`internal/capability` 能力探测、`gitlink-web` 科研服务架构。
|
||||
- **新需求构思报告**:覆盖新增的 12 个命令模块(pm/wiki/export/label/member/milestone/notification/pipeline/webhook/capability/compare/workflow)、26 个 Skill、端到端工作流、科研服务的设计动机与场景。
|
||||
- **变更影响分析及测试报告**:覆盖各模块单元测试(`*_test.go`)、DevOps 流水线(`.devops/`)、跨平台验证(`doc/commands/cross-platform.md`)、Agent 平台验证记录(各 Skill 的 `EXAMPLES.md` / `examples/`)。
|
||||
|
||||
> 三份报告的 docx 由 `报告生成/gen_*.js` 脚本生成,内容与本仓库代码同步。
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
|
@ -23,4 +23,4 @@
|
|||
6. Issue 摘要发布
|
||||
|
||||
该链路满足不少于 3 个 CLI 调用的要求,并形成从数据获取到结果发布的端到端闭环。
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -1,18 +1,18 @@
|
|||
# gitlink-cli Release Notes 草稿
|
||||
|
||||
- 统计窗口:近 7 天
|
||||
- 生成时间:2026-05-20 14:05:25 UTC
|
||||
|
||||
## 变更概览
|
||||
- 已合并 PR:8 个
|
||||
- 最近窗口内合并 PR:2 个
|
||||
|
||||
## 变更分类
|
||||
### feature
|
||||
- feat(pr): add pr +comment shortcut (2026-05-14)
|
||||
|
||||
### fix
|
||||
- fix(npm): improve missing binary diagnostics (2026-05-19)
|
||||
|
||||
## 发布说明
|
||||
- 存在 1 个超过 7 天未更新的开放 Issue,建议优先清理。
|
||||
# gitlink-cli Release Notes 草稿
|
||||
|
||||
- 统计窗口:近 7 天
|
||||
- 生成时间:2026-05-20 14:05:25 UTC
|
||||
|
||||
## 变更概览
|
||||
- 已合并 PR:8 个
|
||||
- 最近窗口内合并 PR:2 个
|
||||
|
||||
## 变更分类
|
||||
### feature
|
||||
- feat(pr): add pr +comment shortcut (2026-05-14)
|
||||
|
||||
### fix
|
||||
- fix(npm): improve missing binary diagnostics (2026-05-19)
|
||||
|
||||
## 发布说明
|
||||
- 存在 1 个超过 7 天未更新的开放 Issue,建议优先清理。
|
||||
|
|
|
|||
|
|
@ -1,32 +1,32 @@
|
|||
# gitlink-cli 自动化周报
|
||||
|
||||
- 统计窗口:近 7 天
|
||||
- 生成时间:2026-05-20 14:05:25 UTC
|
||||
|
||||
## 核心指标
|
||||
|
||||
| 指标 | 数值 |
|
||||
| --- | ---: |
|
||||
| Issues 总数 | 15 |
|
||||
| 打开 Issues | 5 |
|
||||
| 超窗 Issue | 1 |
|
||||
| PR 总数 | 20 |
|
||||
| 打开 PR | 5 |
|
||||
| 已合并 PR | 8 |
|
||||
| Release 数 | 11 |
|
||||
|
||||
## 热点标签
|
||||
- 无
|
||||
|
||||
## 最近合并 PR
|
||||
### fix
|
||||
- fix(npm): improve missing binary diagnostics (2026-05-19)
|
||||
### feature
|
||||
- feat(pr): add pr +comment shortcut (2026-05-14)
|
||||
|
||||
## 风险提示
|
||||
### 超窗 Issue
|
||||
- 2 gitlink-cli 使用讨论与反馈收集 (open) 2026-04-18
|
||||
|
||||
### 建议动作
|
||||
- 存在 1 个超过 7 天未更新的开放 Issue,建议优先清理。
|
||||
# gitlink-cli 自动化周报
|
||||
|
||||
- 统计窗口:近 7 天
|
||||
- 生成时间:2026-05-20 14:05:25 UTC
|
||||
|
||||
## 核心指标
|
||||
|
||||
| 指标 | 数值 |
|
||||
| --- | ---: |
|
||||
| Issues 总数 | 15 |
|
||||
| 打开 Issues | 5 |
|
||||
| 超窗 Issue | 1 |
|
||||
| PR 总数 | 20 |
|
||||
| 打开 PR | 5 |
|
||||
| 已合并 PR | 8 |
|
||||
| Release 数 | 11 |
|
||||
|
||||
## 热点标签
|
||||
- 无
|
||||
|
||||
## 最近合并 PR
|
||||
### fix
|
||||
- fix(npm): improve missing binary diagnostics (2026-05-19)
|
||||
### feature
|
||||
- feat(pr): add pr +comment shortcut (2026-05-14)
|
||||
|
||||
## 风险提示
|
||||
### 超窗 Issue
|
||||
- 2 gitlink-cli 使用讨论与反馈收集 (open) 2026-04-18
|
||||
|
||||
### 建议动作
|
||||
- 存在 1 个超过 7 天未更新的开放 Issue,建议优先清理。
|
||||
|
|
|
|||
|
|
@ -1,14 +1,14 @@
|
|||
# gitlink-cli Release Notes 草稿
|
||||
|
||||
- 统计窗口:近 7 天
|
||||
- 生成时间:2026-05-20 14:32:24 UTC
|
||||
|
||||
## 变更概览
|
||||
- 已合并 PR:0 个
|
||||
- 最近窗口内合并 PR:0 个
|
||||
|
||||
## 变更分类
|
||||
- 无
|
||||
|
||||
## 发布说明
|
||||
- 当前未采集到 Release 记录,建议补充发布说明或确认 Release 权限。
|
||||
# gitlink-cli Release Notes 草稿
|
||||
|
||||
- 统计窗口:近 7 天
|
||||
- 生成时间:2026-05-20 14:32:24 UTC
|
||||
|
||||
## 变更概览
|
||||
- 已合并 PR:0 个
|
||||
- 最近窗口内合并 PR:0 个
|
||||
|
||||
## 变更分类
|
||||
- 无
|
||||
|
||||
## 发布说明
|
||||
- 当前未采集到 Release 记录,建议补充发布说明或确认 Release 权限。
|
||||
|
|
|
|||
|
|
@ -1,26 +1,26 @@
|
|||
# gitlink-cli 自动化周报
|
||||
|
||||
- 统计窗口:近 7 天
|
||||
- 生成时间:2026-05-20 14:32:24 UTC
|
||||
|
||||
## 核心指标
|
||||
|
||||
| 指标 | 数值 |
|
||||
| --- | ---: |
|
||||
| Issues 总数 | 2 |
|
||||
| 打开 Issues | 2 |
|
||||
| 超窗 Issue | 0 |
|
||||
| PR 总数 | 0 |
|
||||
| 打开 PR | 0 |
|
||||
| 已合并 PR | 0 |
|
||||
| Release 数 | 0 |
|
||||
|
||||
## 热点标签
|
||||
- 无
|
||||
|
||||
## 最近合并 PR
|
||||
- 无
|
||||
|
||||
## 风险提示
|
||||
### 建议动作
|
||||
- 当前未采集到 Release 记录,建议补充发布说明或确认 Release 权限。
|
||||
# gitlink-cli 自动化周报
|
||||
|
||||
- 统计窗口:近 7 天
|
||||
- 生成时间:2026-05-20 14:32:24 UTC
|
||||
|
||||
## 核心指标
|
||||
|
||||
| 指标 | 数值 |
|
||||
| --- | ---: |
|
||||
| Issues 总数 | 2 |
|
||||
| 打开 Issues | 2 |
|
||||
| 超窗 Issue | 0 |
|
||||
| PR 总数 | 0 |
|
||||
| 打开 PR | 0 |
|
||||
| 已合并 PR | 0 |
|
||||
| Release 数 | 0 |
|
||||
|
||||
## 热点标签
|
||||
- 无
|
||||
|
||||
## 最近合并 PR
|
||||
- 无
|
||||
|
||||
## 风险提示
|
||||
### 建议动作
|
||||
- 当前未采集到 Release 记录,建议补充发布说明或确认 Release 权限。
|
||||
|
|
|
|||
|
|
@ -155,26 +155,11 @@ def extract_first_dict(payload: Any, keys: Iterable[str]) -> dict[str, Any]:
|
|||
return {}
|
||||
|
||||
|
||||
# 可由 --cli-bin 或 GITLINK_CLI_BIN 环境变量指定本仓库编译的二进制,
|
||||
# 从而让工作流调用本 fork 的 Go 命令(pm/wiki/export 等)而非上游 npm 包。
|
||||
_CLI_BIN: str | None = None
|
||||
|
||||
|
||||
def set_cli_bin(path: str | None) -> None:
|
||||
"""设置工作流要调用的 gitlink-cli 二进制路径。"""
|
||||
global _CLI_BIN
|
||||
_CLI_BIN = path
|
||||
|
||||
|
||||
def run_gitlink_cli(command: list[str], owner: str, repo: str, cwd: Path | None = None) -> Any:
|
||||
resolved = _CLI_BIN or os.environ.get("GITLINK_CLI_BIN") or shutil_which("gitlink-cli")
|
||||
if not resolved:
|
||||
raise WorkflowError(
|
||||
"未找到 gitlink-cli:请用 --cli-bin 指定本仓库编译的二进制,"
|
||||
"或设置 GITLINK_CLI_BIN 环境变量,或将其安装到 PATH"
|
||||
)
|
||||
if shutil_which("gitlink-cli") is None:
|
||||
raise WorkflowError("未找到 gitlink-cli,请先安装并确保它在 PATH 中")
|
||||
|
||||
cli_path = resolved
|
||||
cli_path = shutil_which("gitlink-cli") or "gitlink-cli"
|
||||
if cli_path.lower().endswith((".cmd", ".bat")):
|
||||
cmd = [
|
||||
"cmd",
|
||||
|
|
@ -788,7 +773,6 @@ def build_artifacts(
|
|||
|
||||
def main(argv: list[str] | None = None) -> int:
|
||||
args = parse_args(argv)
|
||||
set_cli_bin(args.cli_bin)
|
||||
config = load_json_file(args.config)
|
||||
|
||||
owner = args.owner or config.get("owner")
|
||||
|
|
|
|||
|
|
@ -5,45 +5,22 @@ param(
|
|||
[int]$WindowDays = 7,
|
||||
[string]$OutputDir = "outputs",
|
||||
[int]$PublishIssueId = 0,
|
||||
[switch]$SkipReleases,
|
||||
[string]$CliBin = "",
|
||||
[switch]$UseNpm
|
||||
[switch]$SkipReleases
|
||||
)
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
# 解析要调用的 gitlink-cli:优先 --cli-bin,其次本仓库编译的二进制,最后 npm 全局包
|
||||
if ($CliBin) {
|
||||
$env:GITLINK_CLI_BIN = $CliBin
|
||||
Write-Host ">> 使用指定二进制: $CliBin"
|
||||
} elseif (-not $UseNpm) {
|
||||
$repoRoot = Resolve-Path "$PSScriptRoot/../../.." | Select-Object -ExpandProperty Path
|
||||
$localExe = Join-Path $repoRoot "gitlink-cli.exe"
|
||||
$localExe2 = Join-Path $repoRoot "gitlink-cli"
|
||||
if (Test-Path $localExe) {
|
||||
$env:GITLINK_CLI_BIN = $localExe
|
||||
Write-Host ">> 使用本 fork 二进制: $localExe"
|
||||
} elseif (Test-Path $localExe2) {
|
||||
$env:GITLINK_CLI_BIN = $localExe2
|
||||
Write-Host ">> 使用本 fork 二进制: $localExe2"
|
||||
} else {
|
||||
Write-Host ">> 未找到本仓库二进制,回退到 npm 全局包(用 -UseNpm 跳过此提示)"
|
||||
$UseNpm = $true
|
||||
}
|
||||
$cliCandidates = npm.cmd exec --yes --package=@gitlink-ai/cli -- cmd /c where gitlink-cli 2>$null
|
||||
$cliPath = $cliCandidates | Where-Object { $_ -match 'gitlink-cli\.cmd$' } | Select-Object -First 1
|
||||
if (-not $cliPath) {
|
||||
$cliPath = $cliCandidates | Select-Object -First 1
|
||||
}
|
||||
if (-not $cliPath) {
|
||||
throw "未能通过 npm exec 找到 gitlink-cli"
|
||||
}
|
||||
|
||||
if ($UseNpm -or -not $env:GITLINK_CLI_BIN) {
|
||||
$cliCandidates = npm.cmd exec --yes --package=@gitlink-ai/cli -- cmd /c where gitlink-cli 2>$null
|
||||
$cliPath = $cliCandidates | Where-Object { $_ -match 'gitlink-cli\.cmd$' } | Select-Object -First 1
|
||||
if (-not $cliPath) {
|
||||
$cliPath = $cliCandidates | Select-Object -First 1
|
||||
}
|
||||
if (-not $cliPath) {
|
||||
throw "未能通过 npm exec 找到 gitlink-cli"
|
||||
}
|
||||
$cliDir = Split-Path -Parent $cliPath
|
||||
$env:PATH = "$cliDir;$env:PATH"
|
||||
}
|
||||
$cliDir = Split-Path -Parent $cliPath
|
||||
$env:PATH = "$cliDir;$env:PATH"
|
||||
|
||||
$args = @(
|
||||
"scripts\gitlink_workflow.py",
|
||||
|
|
|
|||
|
|
@ -1,73 +0,0 @@
|
|||
#!/usr/bin/env bash
|
||||
# community-ops-automation 跨平台运行入口(Linux / macOS / Git Bash)
|
||||
# 默认使用【本 fork 编译的 gitlink-cli 二进制】,从而工作流调用本仓库新增的 Go 命令。
|
||||
# 也可通过 --cli-bin 指定其它二进制,或退回上游 npm 包(--use-npm)。
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
WF_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" # community-ops-automation/
|
||||
REPO_ROOT="$(cd "$WF_ROOT/../../.." && pwd)" # gitlink-cli 仓库根
|
||||
|
||||
CONFIG="${WF_ROOT}/examples/demo_active_config.json"
|
||||
OWNER=""
|
||||
REPO_NAME=""
|
||||
WINDOW_DAYS=7
|
||||
OUTPUT_DIR="outputs"
|
||||
PUBLISH_ISSUE_ID=0
|
||||
SKIP_RELEASES=0
|
||||
USE_NPM=0
|
||||
CLI_BIN=""
|
||||
|
||||
usage() { sed -n '2,9p' "$0"; exit 1; }
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--config) CONFIG="$2"; shift 2 ;;
|
||||
--owner) OWNER="$2"; shift 2 ;;
|
||||
--repo) REPO_NAME="$2"; shift 2 ;;
|
||||
--window-days) WINDOW_DAYS="$2"; shift 2 ;;
|
||||
--output-dir) OUTPUT_DIR="$2"; shift 2 ;;
|
||||
--publish-issue-id) PUBLISH_ISSUE_ID="$2"; shift 2 ;;
|
||||
--skip-releases) SKIP_RELEASES=1; shift ;;
|
||||
--cli-bin) CLI_BIN="$2"; shift 2 ;;
|
||||
--use-npm) USE_NPM=1; shift ;;
|
||||
-h|--help) usage ;;
|
||||
*) echo "未知参数: $1"; usage ;;
|
||||
esac
|
||||
done
|
||||
|
||||
# 解析要调用的 gitlink-cli 二进制
|
||||
if [[ -n "$CLI_BIN" ]]; then
|
||||
BIN="$CLI_BIN"
|
||||
elif [[ "$USE_NPM" -eq 1 ]]; then
|
||||
BIN="" # 走 PATH 中的 npm 全局包
|
||||
echo ">> 使用 PATH 中的 gitlink-cli(npm 全局包)"
|
||||
else
|
||||
# 优先用仓库根已编译的二进制
|
||||
if [[ -x "$REPO_ROOT/gitlink-cli" ]]; then
|
||||
BIN="$REPO_ROOT/gitlink-cli"
|
||||
elif [[ -x "$REPO_ROOT/gitlink-cli.exe" ]]; then
|
||||
BIN="$REPO_ROOT/gitlink-cli.exe"
|
||||
else
|
||||
echo ">> 未找到已编译二进制,尝试 go build(需 Go 环境)..."
|
||||
if command -v go >/dev/null 2>&1; then
|
||||
(cd "$REPO_ROOT" && go build -o gitlink-cli .)
|
||||
BIN="$REPO_ROOT/gitlink-cli"
|
||||
else
|
||||
echo "!! 无 Go 环境,也未找到二进制。请先编译,或改用 --use-npm。" >&2
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
echo ">> 使用本 fork 二进制: $BIN"
|
||||
fi
|
||||
|
||||
[[ -n "$BIN" ]] && export GITLINK_CLI_BIN="$BIN"
|
||||
|
||||
ARGS=(scripts/gitlink_workflow.py --config "$CONFIG" --window-days "$WINDOW_DAYS" --output-dir "$OUTPUT_DIR")
|
||||
[[ -n "$OWNER" ]] && ARGS+=(--owner "$OWNER")
|
||||
[[ -n "$REPO_NAME" ]] && ARGS+=(--repo "$REPO_NAME")
|
||||
[[ "$PUBLISH_ISSUE_ID" -gt 0 ]] && ARGS+=(--publish-issue-id "$PUBLISH_ISSUE_ID")
|
||||
[[ "$SKIP_RELEASES" -eq 1 ]] && ARGS+=(--skip-releases)
|
||||
|
||||
cd "$WF_ROOT"
|
||||
python3 "${ARGS[@]}"
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
outputs/
|
||||
__pycache__/
|
||||
*.pyc
|
||||
|
|
@ -0,0 +1,69 @@
|
|||
# PR 质量门禁工作流(pr-quality-gatekeeper)
|
||||
|
||||
把已收录的 [`gitlink-gatekeeper` Skill](../../../skills/gitlink-gatekeeper/SKILL.md)(Policy-as-Code 合并门禁)包成**可直接运行的端到端工作流**:
|
||||
|
||||
> **采集 → 路由 → 裁决 → 回写/善后**:读取一个真实 PR 的元信息/变更文件/commits/CI,按变更路径建议 reviewer,依 `gatekeeper.yaml` 策略算出**确定性 0–100 评分卡**与**三态裁决**(PASS / REQUEST_CHANGES / COMMENT),并(仅在 `--apply` 时)把评分卡评论、裁决标签、tracking issue 真实回写到 GitLink。
|
||||
|
||||
与仓库内已有能力的关系:`label` 命令(裁决标签)→ `gitlink-gatekeeper` Skill(裁决知识)→ **本工作流(可复现闭环)**,三层共用同一套策略文件,互为支撑而非重复。
|
||||
|
||||
## 交付物
|
||||
|
||||
- `scripts/gatekeeper_workflow.py`:单 PR 门禁闭环(纯标准库,Python ≥3.9,零第三方依赖)
|
||||
- `scripts/gatekeeper_sweep.py`:**仓库级批量体检**——对全部 open PR 逐个 dry-run,产出治理报告
|
||||
- `owner-rules.example.yaml`:变更路径 → reviewer 的路由表样例
|
||||
- `config.example.yaml`:工作流配置样例(命令行参数可覆盖)
|
||||
- `findings.example.json`:AI/人工审查发现注入样例(**来自对真实 PR diff 的真实审查**,行号可复核)
|
||||
- `docs/architecture.md` · `docs/quickstart.md` · `docs/runbook.md` · `docs/verification.md`
|
||||
- `ci-example/`:Gitea Actions 接入示例(PR 触发自动门禁,退出码 2 = REQUEST_CHANGES)
|
||||
- `examples/demo-outputs/`:真实平台运行产物(PASS 90 评分卡 / 注入发现后的 55 分评分卡 / 113 个 open PR 的全仓体检报告)
|
||||
- `tests/test_scoring.py`:确定性回归护栏(同输入 → 同分 → 同裁决)
|
||||
|
||||
## 快速运行(默认 dry-run,不写远端)
|
||||
|
||||
```bash
|
||||
npm install -g @gitlink-ai/cli # ≥0.2.0,自带 label 命令与 gitlink-gatekeeper Skill
|
||||
gitlink-cli auth login
|
||||
|
||||
python3 scripts/gatekeeper_workflow.py \
|
||||
--owner <owner> --repo <repo> --pr <PR号> \
|
||||
--policy ../../../skills/gitlink-gatekeeper/examples/gatekeeper.yaml \
|
||||
--owner-rules owner-rules.example.yaml \
|
||||
--output-dir outputs
|
||||
```
|
||||
|
||||
- 注入审查发现得到含扣分的评分卡:加 `--findings findings.example.json`
|
||||
- 真实回写(评论 + 标签 + tracking issue):加 `--apply`(请先在自有仓库演练)
|
||||
- 全仓批量体检(只读,零写入):
|
||||
|
||||
```bash
|
||||
python3 scripts/gatekeeper_sweep.py \
|
||||
--owner <owner> --repo <repo> \
|
||||
--policy ../../../skills/gitlink-gatekeeper/examples/gatekeeper.yaml \
|
||||
--owner-rules owner-rules.example.yaml \
|
||||
--output-dir sweep-out --date-label $(date +%F)
|
||||
```
|
||||
|
||||
更多见 [`docs/quickstart.md`](docs/quickstart.md) 与 [`docs/runbook.md`](docs/runbook.md)。
|
||||
|
||||
## 已在真实平台验证
|
||||
|
||||
全部证据见 [`docs/verification.md`](docs/verification.md),要点:
|
||||
|
||||
| 验证 | 对象 | 结果 |
|
||||
|------|------|------|
|
||||
| dry-run | 本仓库真实 PR(pull_request_id 15222) | ✅ PASS 90/100,8 个变更文件路由正确 |
|
||||
| 注入真实审查发现 | 同一 PR + `findings.example.json` | ❌ REQUEST_CHANGES 55/100(裁决翻转,确定性可复算) |
|
||||
| `--apply` 真实回写 | 自有 fork 的演练 PR | 评分卡评论 + tracking issue + 裁决标签全部由 API 回执确认 |
|
||||
| **全仓批量体检** | 本仓库**全部 113 个 open PR** | 113/113 成功:PASS 105 / COMMENT 6 / REQUEST_CHANGES 2,均分 88.5;96% 未关联 issue |
|
||||
| 单测 | `tests/test_scoring.py` | 全绿(锁定四个权威裁决案例的分值与裁决) |
|
||||
|
||||
## 设计要点
|
||||
|
||||
- **确定性评分**:AI 只负责产出「发现列表」(可选注入),扣分与裁决由纯函数完成——同策略 + 同 PR → 同裁决,可逐位手算复现、可审计。
|
||||
- **安全默认**:默认 dry-run 什么都不写;即便策略开了 `auto_merge`,也必须 `verdict == PASS` 且显式 `--apply` 才会合并;强语义的 approve/reject 始终留给人,自动裁决只以建议性 `common` 评论 + 标签呈现。
|
||||
- **原生适配 GitLink**:PR 标题/描述取自 `pr +view` 的 `issue.subject/description`;标签挂载走「`label +list` 查 id → Raw API `POST /:owner/:repo/issues/<issue_id>`」;尊重 `common/approved/rejected` 三态 review。
|
||||
- **零依赖、零常驻**:纯标准库脚本 + `gitlink-cli`,无需部署 webhook 服务或数据库,CI 一条 step 即可接入(见 `ci-example/`);确定性意味着**大规模治理零 AI 成本**。
|
||||
|
||||
## 许可证
|
||||
|
||||
随仓库 [MulanPSL-2.0](../../../LICENSE)。
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
# CI 集成示例 —— 门禁接 CI
|
||||
|
||||
本目录演示如何把 **gitlink-gatekeeper** 的 PR 看门人门禁接到 CI 上,让裁决直接挡住不达标的 PR。
|
||||
|
||||
> 这是**示例**,不是开箱即用的生产配置;`gitlink-cli` 的安装方式、PR 编号字段名需按你的 runner 实际情况调整。
|
||||
|
||||
## 文件
|
||||
|
||||
- [`gatekeeper.gitea.yml`](gatekeeper.gitea.yml):Gitea Actions 工作流(GitLink 基于 Gitea,语法与 GitHub Actions 兼容)。
|
||||
|
||||
## 用法
|
||||
|
||||
1. 把 `gatekeeper.gitea.yml` 复制到目标仓库的 `.gitea/workflows/` 目录。
|
||||
2. 在仓库 **Settings → Actions → Secrets** 新增 `GITLINK_TOKEN`,值为有权读取该仓库 PR 的访问令牌(供 `gitlink-cli` 认证)。**Token 切勿写进仓库或日志。**
|
||||
3. 提一个 PR 触发工作流即可。
|
||||
|
||||
## 工作原理
|
||||
|
||||
- 触发:PR 的 `opened` / `synchronize` / `reopened` 事件。
|
||||
- 步骤:检出 → 准备 Python 3.9(脚本纯标准库,无需装依赖)→ 装 `gitlink-cli` → 跑 `scripts/gatekeeper_workflow.py` 采集本次 PR 上下文并评分裁决。
|
||||
- **退出码即门禁**:
|
||||
- `0` = PASS / COMMENT → job 通过,放行。
|
||||
- `2` = REQUEST_CHANGES → 工作流把它转成 job 失败,挡住该 PR。
|
||||
- `1` = 可预期错误(缺参数 / 未装 `gitlink-cli` 等)→ 同样失败。
|
||||
- 产物:评分卡与 `summary.json` 落在 `outputs/`,工作流用 `upload-artifact` 上传,便于在 CI 页面查看裁决依据。
|
||||
|
||||
调门禁松紧只需改 `--policy` 指向的 `gatekeeper.yaml`(策略字段说明见 [`gitlink-gatekeeper` Skill REFERENCE](../../../../skills/gitlink-gatekeeper/REFERENCE.md))。
|
||||
|
|
@ -0,0 +1,75 @@
|
|||
# gitlink-gatekeeper —— Gitea Actions CI 示例(GitLink 平台用)
|
||||
#
|
||||
# ⚠️ 这是一个「门禁接 CI」的演示示例,不是开箱即用的生产配置。
|
||||
# - GitLink 基于 Gitea,其 Actions 语法与 GitHub Actions 兼容,工作流放在
|
||||
# 仓库的 .gitea/workflows/ 目录下。把本文件复制过去并按需调整即可启用。
|
||||
# - 需要在仓库 Settings → Actions → Secrets 配置一个 GITLINK_TOKEN secret
|
||||
# (供 gitlink-cli 认证、采集目标 PR 的上下文)。Token 切勿写进仓库。
|
||||
# - runner 需能访问 GitLink API;Python 3.9+ 与 gitlink-cli 的安装方式按实际
|
||||
# runner 镜像调整(下面 install 步骤仅为占位示意)。
|
||||
#
|
||||
# 触发:对 PR 的 open / 同步事件运行门禁,脚本返回码 2(REQUEST_CHANGES)
|
||||
# 会让本 job 失败,从而在 CI 上挡住该 PR(PASS/COMMENT 返回 0 即通过)。
|
||||
|
||||
name: gatekeeper
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened, synchronize, reopened]
|
||||
|
||||
jobs:
|
||||
gatekeeper:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: 检出代码
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: 准备 Python(纯标准库,无需装依赖)
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: "3.9"
|
||||
|
||||
# 安装 gitlink-cli(示意:按 runner 实际情况替换为正确的安装/分发方式)
|
||||
- name: 安装 gitlink-cli
|
||||
run: |
|
||||
# 例如从发布物下载或用包管理器安装,确保 PATH 里有 gitlink-cli
|
||||
gitlink-cli --version
|
||||
|
||||
# 跑门禁:采集本次 PR 上下文 → 评分 → 裁决。
|
||||
# REQUEST_CHANGES 时脚本退出码为 2;下面用 if/exit 把它转成 job 失败。
|
||||
- name: 运行 PR 看门人门禁
|
||||
env:
|
||||
# gitlink-cli 通过该环境变量认证(对应仓库配置的 secret)
|
||||
GITLINK_TOKEN: ${{ secrets.GITLINK_TOKEN }}
|
||||
# Gitea 注入的 PR 编号;不同 runner 字段名可能不同,按实际调整
|
||||
PR_NUMBER: ${{ github.event.pull_request.number }}
|
||||
run: |
|
||||
set -o pipefail
|
||||
python3 examples/workflows/pr-quality-gatekeeper/scripts/gatekeeper_workflow.py \
|
||||
--owner "${{ github.repository_owner }}" \
|
||||
--repo "${{ github.event.repository.name }}" \
|
||||
--pr "${PR_NUMBER}" \
|
||||
--policy skills/gitlink-gatekeeper/examples/gatekeeper.yaml \
|
||||
--owner-rules workflow/owner-rules.yaml \
|
||||
--cli-bin gitlink-cli
|
||||
code=$?
|
||||
# 退出码:0 = PASS/COMMENT(放行);2 = REQUEST_CHANGES(挡住,让 job 失败);
|
||||
# 1 = 可预期错误(缺参数 / 未装 gitlink-cli 等),同样视为失败。
|
||||
if [ "$code" -eq 0 ]; then
|
||||
echo "门禁通过(PASS/COMMENT)"
|
||||
exit 0
|
||||
elif [ "$code" -eq 2 ]; then
|
||||
echo "::error::门禁裁决 REQUEST_CHANGES,阻止合并"
|
||||
exit 1
|
||||
else
|
||||
echo "::error::门禁执行出错(退出码 $code)"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 上传评分卡 / summary 产物,便于在 CI 页面查看裁决依据
|
||||
- name: 上传门禁产物
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: gatekeeper-scorecard
|
||||
path: outputs/
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
# config.example.yaml — PR 看门人闭环的工作流配置(gatekeeper_workflow.py --config 用)
|
||||
#
|
||||
# 命令行参数(--owner/--repo/--pr/--policy/--owner-rules/--findings)会覆盖这里的同名字段。
|
||||
# 相对路径以本配置文件所在目录为基准解析。
|
||||
|
||||
owner: Gitlink # 目标仓库 owner(GitLink 主分支为 master)
|
||||
repo: gitlink-cli # 目标仓库名
|
||||
pr: 1 # 目标 PR 编号(用 --pr 覆盖以复用同一配置跑不同 PR)
|
||||
|
||||
# 策略文件(Policy-as-Code)。缺省 / 文件不存在时回退脚本内置默认策略(SSOT 第 2 节)。
|
||||
policy: ../skills/gitlink-gatekeeper/examples/gatekeeper.yaml
|
||||
|
||||
# 文件路径 → reviewer 路由表(工作流步骤 1)
|
||||
owner_rules: owner-rules.yaml
|
||||
|
||||
# 可选:AI 审查发现 JSON(注入 review_findings 维度)。
|
||||
# 缺省为空 → 评分仍确定性可复现(同策略 + 同 PR 上下文 → 同裁决)。
|
||||
# 这里默认指向随附的示例(1 major + 2 minor),开箱即可跑通;用 --findings 覆盖为你自己的产物。
|
||||
findings: findings.sample.json
|
||||
|
|
@ -0,0 +1,66 @@
|
|||
# 架构与数据流 — PR 看门人闭环
|
||||
|
||||
本工作流采用「**采集 → 路由 → 裁决 → 回写/善后**」四段式流水线,把 `gitlink-gatekeeper` 的 Policy-as-Code 门禁包成一条可复现闭环。所有数值/字段/算法以已收录的 [`gitlink-gatekeeper` Skill REFERENCE](../../../../skills/gitlink-gatekeeper/REFERENCE.md) 为准。
|
||||
|
||||
## 设计目标
|
||||
|
||||
- **可复现**:同策略 + 同 PR 上下文 → 同评分卡 + 同裁决(确定性算法,SSOT 第 3–5 节)。
|
||||
- **可审计**:评分卡逐维列分 + 备注,硬门禁逐条列出命中原因,裁决可追溯到具体规则与 `gatekeeper.yaml` 版本。
|
||||
- **安全默认**:默认 dry-run,写操作需显式 `--apply`;绝不默认自动合并(SSOT 第 8 节)。
|
||||
- **低门槛**:仅依赖 `gitlink-cli` 与 Python 标准库(含内置 YAML 子集解析器,无第三方包)。
|
||||
- **边界清晰**:采集、路由、裁决、回写四段各自独立,便于单测与替换(如换一套评分维度只动裁决段)。
|
||||
|
||||
## ASCII 流程图
|
||||
|
||||
> 下面是数据流占位图:左侧为 `gitlink-cli` 采集,中间为本脚本的确定性处理,右侧为回写/善后的写操作(仅 `--apply` 时执行)。
|
||||
|
||||
```
|
||||
┌──────────────────────────── gatekeeper_workflow.py ────────────────────────────┐
|
||||
│ │
|
||||
gitlink-cli (读) │ step 1: 路由 step 2: 裁决 step 3: 回写 + 善后 │ gitlink-cli (写, 仅 --apply)
|
||||
───────────────────── │ ─────────────────── ────────────────── ───────────────────────── │ ─────────────────────────────
|
||||
│ │
|
||||
pr +view ──┐ │ ┌─ review_findings(40) ─┐ │
|
||||
pr +files ──┼──▶ 采集 │ changed_files │ test_coverage (20) │ verdict │ pr +comment ─▶ 评分卡评论
|
||||
ci +builds │ 归一化 │ │ │ pr_hygiene (15) │──▶ ── PASS ───────┐ │
|
||||
api .../ │ │ ▼ │ commit_quality(15) │ ── COMMENT ───┐ │ │ label +create ─▶ 裁决标签
|
||||
commits ──┘ │ owner-rules.yaml │ ci_status (10) │ ── REQ_CHG ─┐ │ │ │ (+ 挂 issue_tag_ids
|
||||
▲ │ (glob → reviewer) └───────────┬───────────┘ │ │ │ │ via Raw API POST
|
||||
│ │ │ ▼ │ │ │ │ /:owner/:repo/issues/:id)
|
||||
gatekeeper.yaml ─────┼─────┼──────────────▶ hard_gates 判定 ─┴─▶ total 0..100 ─▶ 阈值 │ │ │ │
|
||||
(Policy-as-Code) │ ▼ (SSOT 第4节) (SSOT 第3节) (第5节) │ │ │ │ issue +create ─▶ tracking issue
|
||||
│ │ suggested_reviewers ──────────────────────────────────────────┘ │ │ │ (仅 REQUEST_CHANGES)
|
||||
findings.json ───────┼──▶ review_findings 注入 │ │ │
|
||||
(可选, AI 审查) │ │ │ │ pr +merge ─▶ 合并 (受限:
|
||||
│ ┌── outputs/*_scorecard.md ◀────────────┘ │ │ PASS + auto_merge + --apply)
|
||||
│ 本地产物落盘 (总是) ───────┤ │ │
|
||||
│ └── outputs/*_summary.json ◀──────────────┘ │
|
||||
└─────────────────────────────────────────────────────────────────────────────────┘
|
||||
|
||||
dry-run(默认):右侧写操作仅打印「将要执行的命令」,不实际调用 → 安全。
|
||||
--apply :右侧写操作真正执行;其中合并需同时满足 PASS + 策略 auto_merge=true + --apply。
|
||||
```
|
||||
|
||||
## 四段职责
|
||||
|
||||
### ① 采集(collect_pr_context)
|
||||
调只读 `gitlink-cli` 命令拿到 PR 元信息、变更文件、CI 状态、commits(端点未开放时降级,不阻断)。输出统一归一化为内部结构,兼容 GitLink Envelope 的多种字段名。
|
||||
|
||||
### ② 路由(route_reviewers)
|
||||
读 `owner-rules.yaml`,对每个变更文件按 glob 顺序匹配(首个命中生效,顺序即优先级),产出 `reviewer → 文件清单`;未命中文件归 `default_reviewers`。结果写进评分卡的「Suggested reviewers」分区。**只产出建议,不调用任何写操作**——是否真正分配由维护者决定。
|
||||
|
||||
### ③ 裁决(score_dimensions / evaluate_hard_gates / decide_verdict)
|
||||
- 五维加权评分(权重和=100,SSOT 第 3 节),可选注入 AI findings 影响 `review_findings`。
|
||||
- 硬门禁逐项判定(SSOT 第 4 节),任一命中即 `hard_gate_failed`。
|
||||
- 裁决判定树(SSOT 第 5 节):硬门禁失败 → REQUEST_CHANGES;否则按总分与 `pass`/`request_changes` 阈值落三态。
|
||||
- 渲染评分卡(SSOT 第 6 节模板)。
|
||||
|
||||
### ④ 回写 + 善后(build_*_command + execute_write)
|
||||
按裁决构造写操作计划:评分卡评论、裁决标签、(REQUEST_CHANGES 时)tracking issue、(受限)合并。dry-run 只打印计划;`--apply` 才逐条执行并记录结果到 `summary.json`。
|
||||
|
||||
## 为什么选这条链路
|
||||
|
||||
子赛题三要求用现有命令 / Skill 组合形成完整解决方案,且串联不少于 3 步。本链路:
|
||||
1. 串联了 **4 个只读采集命令** + **最多 4 个写命令**,远超 3 步下限。
|
||||
2. 形成从「数据获取」到「治理动作落地」的端到端闭环,并能接入 CI(REQUEST_CHANGES 返回码 2)。
|
||||
3. 复用本作品自研的 `label` 命令组(子赛题一)与 gatekeeper 策略(子赛题二),三个子赛题在同一作品内闭环,相互增强。
|
||||
|
|
@ -0,0 +1,48 @@
|
|||
# 最短复现路径(3 步)
|
||||
|
||||
## 1. 安装与认证
|
||||
|
||||
```bash
|
||||
npm install -g @gitlink-ai/cli # ≥0.2.0(label 命令与 gitlink-gatekeeper Skill 已内置)
|
||||
gitlink-cli auth login # 或 export GITLINK_TOKEN=<私人令牌>
|
||||
gitlink-cli auth status # 确认已登录
|
||||
```
|
||||
|
||||
## 2. 对任意真实 PR 出评分卡(dry-run,零写入)
|
||||
|
||||
在本目录(`examples/workflows/pr-quality-gatekeeper/`)下:
|
||||
|
||||
```bash
|
||||
python3 scripts/gatekeeper_workflow.py \
|
||||
--owner Gitlink --repo gitlink-cli --pr <PR号> \
|
||||
--policy ../../../skills/gitlink-gatekeeper/examples/gatekeeper.yaml \
|
||||
--owner-rules owner-rules.example.yaml \
|
||||
--output-dir outputs
|
||||
```
|
||||
|
||||
产物:`outputs/<owner>_<repo>_pr<id>_scorecard.md`(评分卡)+ `_summary.json`(结构化摘要)。
|
||||
退出码:`0` = PASS/COMMENT,`2` = REQUEST_CHANGES(可直接当 CI 门禁用),`1` = 运行错误。
|
||||
|
||||
不带 `--policy` 也能跑(脚本内置同值默认策略);想看含扣分的评分卡,加 `--findings findings.example.json`。
|
||||
|
||||
## 3. 可选进阶
|
||||
|
||||
- **真实回写**(评论 + 裁决标签 + tracking issue):加 `--apply`。请先在自有 fork 演练;自动裁决只用建议性 `common` 评论,绝不替人 approve/reject,绝不自动合并。
|
||||
- **全仓体检**(只读批扫全部 open PR,出治理报告):
|
||||
|
||||
```bash
|
||||
python3 scripts/gatekeeper_sweep.py \
|
||||
--owner Gitlink --repo gitlink-cli \
|
||||
--policy ../../../skills/gitlink-gatekeeper/examples/gatekeeper.yaml \
|
||||
--owner-rules owner-rules.example.yaml \
|
||||
--output-dir sweep-out --date-label $(date +%F)
|
||||
```
|
||||
|
||||
- **CI 接入**:见 [`../ci-example/`](../ci-example/)(Gitea Actions,PR 触发自动门禁)。
|
||||
- **改门禁松紧**:复制一份 `gatekeeper.yaml` 改 `weights/hard_gates/thresholds`,字段说明见 [Skill REFERENCE](../../../../skills/gitlink-gatekeeper/REFERENCE.md)。
|
||||
|
||||
## 验证自己改动没破坏确定性
|
||||
|
||||
```bash
|
||||
python3 tests/test_scoring.py # 同输入 → 同分 → 同裁决 的回归护栏
|
||||
```
|
||||
|
|
@ -0,0 +1,106 @@
|
|||
# 运行手册 — PR 看门人闭环
|
||||
|
||||
本手册覆盖 `scripts/gatekeeper_workflow.py` 的前置条件、运行步骤、参数、预期输出与回滚。数值/字段以已收录的 [`gitlink-gatekeeper` Skill REFERENCE](../../../../skills/gitlink-gatekeeper/REFERENCE.md) 为准。
|
||||
|
||||
## 1. 前置条件
|
||||
|
||||
- 已安装 `gitlink-cli` 且在 `PATH` 中(或用 `--cli-bin` 指定路径)。
|
||||
- 已完成登录:`gitlink-cli auth login`(Token 有效期 7 天,过期重新登录;详见 [gitlink-shared](../../../../skills/gitlink-shared/SKILL.md))。
|
||||
- 对目标仓库有读权限;要 `--apply` 回写评论/标签/建 issue 时需写权限。
|
||||
- Python 3.8+(脚本纯标准库,无需 `pip install`)。
|
||||
|
||||
验证登录态:
|
||||
|
||||
```bash
|
||||
gitlink-cli auth status
|
||||
gitlink-cli pr +view -i <pr_id> --owner <owner> --repo <repo> --format json # 确认目标 PR 可读
|
||||
```
|
||||
|
||||
## 2. 配置
|
||||
|
||||
编辑 [`../config.example.yaml`](../config.example.yaml)(或复制一份),填好 `owner`/`repo`/`pr` 与策略、路由表路径。命令行参数会覆盖配置同名字段,相对路径以配置文件所在目录为基准。
|
||||
|
||||
按需调整 [`../owner-rules.example.yaml`](../owner-rules.example.yaml):把占位 reviewer 用户名替换成本仓库维护者,按「具体规则在前」排列 glob。
|
||||
|
||||
按需选择策略预设(均在 [`../../../../skills/gitlink-gatekeeper/examples/`](../../../../skills/gitlink-gatekeeper/examples/)):
|
||||
- `gatekeeper.yaml`:均衡基线(= SSOT 内置默认)。
|
||||
- `gatekeeper.strict.yaml`:严格预设。
|
||||
- `gatekeeper.lenient.yaml`:宽松预设。
|
||||
- 不指定 `--policy` 且配置无 `policy` 字段时,回退脚本内置默认策略(与 `gatekeeper.yaml` 等价)。
|
||||
|
||||
## 3. 运行步骤
|
||||
|
||||
### 步骤 A:dry-run 预览(安全默认,必做)
|
||||
|
||||
```bash
|
||||
python3 scripts/gatekeeper_workflow.py --config config.example.yaml --pr <pr_id>
|
||||
```
|
||||
|
||||
此模式**不写任何东西**,只采集 + 评分 + 打印将要执行的写命令 + 落盘本地产物。先看评分卡与计划是否符合预期。
|
||||
|
||||
### 步骤 B:注入 AI 审查发现(可选)
|
||||
|
||||
`review_findings` 维度默认按 0 发现计分。若已有 AI 代码审查(如 `gitlink-code-review` Skill)产出,整理成 JSON 注入:
|
||||
|
||||
```json
|
||||
{ "findings": [
|
||||
{ "severity": "blocker", "message": "硬编码密钥", "file": "internal/auth/refresh.go", "line": 12 },
|
||||
{ "severity": "minor", "message": "缺超时上下文", "file": "internal/auth/handler.go", "line": 40 }
|
||||
] }
|
||||
```
|
||||
|
||||
```bash
|
||||
python3 scripts/gatekeeper_workflow.py --config config.example.yaml --pr <pr_id> --findings findings.json
|
||||
```
|
||||
|
||||
`severity` 取值:`blocker` / `major` / `minor` / `nit`(其余忽略)。
|
||||
|
||||
### 步骤 C:apply 执行写操作
|
||||
|
||||
确认 dry-run 计划无误后,加 `--apply`:
|
||||
|
||||
```bash
|
||||
python3 scripts/gatekeeper_workflow.py --config config.example.yaml --pr <pr_id> --apply
|
||||
```
|
||||
|
||||
将依次执行(按裁决):回写评分卡评论 → 确保裁决标签存在 →(仅 REQUEST_CHANGES)创建 tracking issue。
|
||||
**合并不会自动发生**:仅当策略 `behavior.auto_merge: true` 且裁决为 `PASS` 且本次带 `--apply` 时,才追加 `pr +merge`。默认 `auto_merge: false`。
|
||||
|
||||
## 4. 参数速查
|
||||
|
||||
| 参数 | 说明 | 默认 |
|
||||
|------|------|------|
|
||||
| `--config` | 工作流配置 YAML(owner/repo/pr/policy/owner_rules/findings) | 无 |
|
||||
| `--owner` / `--repo` / `--pr` | 覆盖配置中的目标 | 取自 config |
|
||||
| `--policy` | `gatekeeper.yaml` 路径 | 内置默认策略 |
|
||||
| `--owner-rules` | `owner-rules.yaml` 路径 | 取自 config |
|
||||
| `--findings` | AI 审查发现 JSON | 空(0 发现) |
|
||||
| `--cli-bin` | `gitlink-cli` 可执行路径 | `gitlink-cli` |
|
||||
| `--skip-ci` | 跳过 CI 采集(`ci_status` 记 `unknown`) | 否 |
|
||||
| `--output-dir` | 本地产物目录 | `outputs` |
|
||||
| `--apply` | **执行写操作**;不传则仅预览 | 否(dry-run) |
|
||||
|
||||
## 5. 预期输出
|
||||
|
||||
- 终端:三段进度(路由 / 裁决 / 回写)+ 评分概览 + 计划或执行结果 + 最终裁决。
|
||||
- 文件:
|
||||
- `outputs/<owner>_<repo>_pr<id>_scorecard.md` — 评分卡(SSOT 第 6 节模板)。
|
||||
- `outputs/<owner>_<repo>_pr<id>_summary.json` — 结构化摘要(路由、各维得分、硬门禁、裁决、`planned_writes`、`executed`、产物路径)。
|
||||
- 退出码:`PASS`/`COMMENT` → `0`;`REQUEST_CHANGES` → `2`(可作 CI 门禁);可预期错误(缺配置 / 未登录 / CLI 缺失)→ `1`。
|
||||
|
||||
样例评分卡见 [`../../../../skills/gitlink-gatekeeper/examples/scorecard-sample.md`](../../../../skills/gitlink-gatekeeper/examples/scorecard-sample.md)。
|
||||
|
||||
## 6. 回滚
|
||||
|
||||
dry-run 不产生任何远端副作用,无需回滚(本地产物可直接删 `outputs/`)。
|
||||
|
||||
`--apply` 后如需撤销:
|
||||
|
||||
| 已做的写操作 | 回滚方式 |
|
||||
|--------------|----------|
|
||||
| 回写的评分卡评论 | 评论走 issue journals,在 PR 页面手动删除该评论即可;脚本不提供删除命令(避免误删他人评论) |
|
||||
| 创建的裁决标签定义 | `gitlink-cli label +delete -i <label_id> --owner <o> --repo <r>`(先 `label +list` 查 id) |
|
||||
| 创建的 tracking issue | `gitlink-cli issue +close -n <number> --owner <o> --repo <r>`(关闭而非删除,保留审计痕迹) |
|
||||
| 已合并的 PR | **不可自动回滚**。这也是默认 `auto_merge: false` 的原因;合并前务必人工确认。如确需撤销,按仓库常规流程 revert commit |
|
||||
|
||||
> 安全提示:任何 `--apply` 写操作前,脚本会在 dry-run 计划里完整复述将执行的命令。生产仓库建议先 dry-run,再 `--apply`。
|
||||
|
|
@ -0,0 +1,55 @@
|
|||
# 真实平台验证记录
|
||||
|
||||
> 全部针对 **GitLink 线上真实平台** 运行(`gitlink-cli` + Token 认证),非 mock。
|
||||
> 他人仓库一律 dry-run(只读零写入);写操作只在自有 fork 演练。
|
||||
> 运行环境:macOS(Apple Silicon)· Python 3.9 · `@gitlink-ai/cli` 0.2.0(npm 官方发布版,零本地构建)。
|
||||
|
||||
## A. dry-run:真实 PR → PASS 90/100
|
||||
|
||||
对本仓库真实 PR(`pull_request_id 15222`,feat/org-team-projects,8 个变更文件):
|
||||
|
||||
- 路由正确分流:README/docs/skill → doc-maintainer;`org.go` → go-reviewer;`org_test.go` → qa-reviewer
|
||||
- 评分(确定性,可手算复现):review 40/40 · test 20/20(1 src/1 test)· hygiene 10/15(desc✓/issue✗/size✓)· commit 15/15 · ci 5/10(unknown)= **90 → PASS**
|
||||
- CI 取不到构建记录 → `unknown`:按策略**不触发**硬门禁(仅显式 `failing` 触发),只在 CI 维记半分
|
||||
- 产物:[`../examples/demo-outputs/scorecard-pass-90.md`](../examples/demo-outputs/scorecard-pass-90.md)
|
||||
|
||||
## B. 注入真实审查发现 → REQUEST_CHANGES 55/100
|
||||
|
||||
同一 PR,注入 [`../findings.example.json`](../findings.example.json) 重跑:review_findings 40/40 → 5/40(1 major + 2 minor),总分 90 → 55,**裁决翻转为 REQUEST_CHANGES**。
|
||||
|
||||
**发现是真的,不是编的**——三条均来自对该 PR 真实 diff(head `bcc27bf`)的代码审查,标注 `shortcuts/org/org.go` 真实行号,任何人拉取该分支可逐条复核。其中 major:新增的 `parseBool` 只认字面 `"true"`,`--dry-run=1` 会被静默当 false,而该 flag 守护的是「批量移除团队全部项目」这一破坏性操作。
|
||||
|
||||
产物:[`../examples/demo-outputs/scorecard-findings-55.md`](../examples/demo-outputs/scorecard-findings-55.md)
|
||||
|
||||
## C. `--apply` 真实回写(自有 fork 演练)
|
||||
|
||||
在自有 fork 的演练 PR(故意「改源码不带测试」)上执行 `--apply`:
|
||||
|
||||
- 触发硬门禁 `require_tests_for_src_changes` → REQUEST_CHANGES 40/100
|
||||
- GitLink API 回执确认三件写操作全部落地:
|
||||
1. 评分卡评论回写到 PR(comment id `472741`)
|
||||
2. 自动创建 tracking issue(id `143217`),汇总硬门禁 + 必修项 + 建议 reviewer,与 PR 双向回链
|
||||
3. 裁决标签挂载到 PR 背后 issue(`label +list` 查 id → Raw API `POST /:owner/:repo/issues/<issue_id>`)——依赖本仓库的 `label` 命令(0.2.0 起官方发布版自带)
|
||||
|
||||
## D. 全仓批量体检:113 个 open PR
|
||||
|
||||
`gatekeeper_sweep.py` 对本仓库**全部 113 个 open PR** 逐个 dry-run(只读、零写入、零 AI 成本),113/113 成功:
|
||||
|
||||
- 裁决分布:**PASS 105 · COMMENT 6 · REQUEST_CHANGES 2**;分数 min 70 / 中位 90 / 均值 88.5 / max 95
|
||||
- 治理洞察:**96% 的 open PR 未关联 issue**;2 个 PR 触发 `require_tests_for_src_changes`(改源码不带测试)
|
||||
- 完整报告(含全量明细表):[`../examples/demo-outputs/sweep-report-2026-06-10.md`](../examples/demo-outputs/sweep-report-2026-06-10.md)
|
||||
- 诚实口径:批扫不注入审查发现(review_findings 维未评、按满分计),CI 统一 `--skip-ci`(unknown 半分)——总分代表「除人工/AI 审查外的工程卫生分」,偏乐观
|
||||
|
||||
## E. 单元测试(确定性回归护栏)
|
||||
|
||||
```bash
|
||||
$ python3 tests/test_scoring.py
|
||||
OK
|
||||
```
|
||||
|
||||
锁定四个权威裁决案例(PASS / REQUEST_CHANGES / COMMENT / 硬门禁直拒)的**总分与裁决**与 Skill 文档逐位一致;任何改动若破坏「同输入 → 同分 → 同裁决」,测试立即变红。
|
||||
|
||||
## 真实运行当场暴露过的问题(透明记录)
|
||||
|
||||
- GitLink 的 PR 标题/描述在 `pr +view` 返回的 `issue.subject/description`,而非 `pull_request` 子对象——离线 mock 测不到,真实平台运行才暴露并修复。
|
||||
- npm 0.1.18 时代 `--apply` 的打标签步骤会报 `unknown command "label"`(彼时 `label` 命令尚未发布);0.2.0 起官方发布版自带,整条闭环零本地构建跑通。
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
## 🛡️ Gatekeeper Report — PR #15222 feat(org): add team project binding shortcuts
|
||||
|
||||
**Verdict: ❌ REQUEST_CHANGES** · Score: 55/100 · policy: gatekeeper.yaml@v1
|
||||
|
||||
| Dimension | Weight | Score | Notes |
|
||||
|-----------|:------:|:-----:|-------|
|
||||
| Review findings | 40 | 5/40 | 0 blocker / 1 major / 2 minor / 0 nit |
|
||||
| Test coverage | 20 | 20/20 | 1 src / 1 test files |
|
||||
| PR hygiene | 15 | 10/15 | desc ✓ / linked issue ✗ / size ✓ |
|
||||
| Commit quality | 15 | 15/15 | 0/0 conventional |
|
||||
| CI status | 10 | 5/10 | unknown |
|
||||
|
||||
### 👥 Suggested reviewers (3)
|
||||
- @doc-maintainer — 6 file(s): README.md, README.zh-CN.md, doc/changes/org-team-projects.md …
|
||||
- @go-reviewer — 2 file(s): shortcuts/org/org.go, shortcuts/org/org_test.go
|
||||
- @qa-reviewer — 1 file(s): shortcuts/org/org_test.go
|
||||
|
||||
### 🔴 Must fix (1)
|
||||
- [major] parseBool 只把字面 "true" 当真:用户传 --dry-run=1 / t / TRUE 以外写法会被静默解析为 false。该 flag 守护的是 team-projects-remove-all(批量移除团队全部项目)这类破坏性操作——预览意图被静默降级为真实执行。建议改用 strconv.ParseBool(与 shortcuts/common/runner.go:52 解析 flag 默认值的行为一致),无法识别的值应报错而非吞掉 — shortcuts/org/org.go:156
|
||||
|
||||
### 🟡 Should fix (2)
|
||||
- [minor] team-projects-remove-all 一条命令清空团队全部项目绑定,除 --dry-run 外无确认机制;建议在 Description/help 标注危险性,或要求显式 --yes 二次确认 — shortcuts/org/org.go:103
|
||||
- [minor] dry-run 预览 payload 中 action(add_all_team_projects/remove_all_team_projects)与实际请求 path 段(create_all/destroy_all,见 :134)是两套词汇,排查问题时易误导;建议统一或在 payload 同时给出两者映射 — shortcuts/org/org.go:142
|
||||
|
||||
### Next steps
|
||||
1. 评分低于阈值,按上方 Must/Should fix 修复后重新触发 gatekeeper
|
||||
---
|
||||
*Generated by gitlink-gatekeeper · policy-as-code PR gate · re-run after changes*
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
## 🛡️ Gatekeeper Report — PR #15222 feat(org): add team project binding shortcuts
|
||||
|
||||
**Verdict: ✅ PASS** · Score: 90/100 · policy: gatekeeper.yaml@v1
|
||||
|
||||
| Dimension | Weight | Score | Notes |
|
||||
|-----------|:------:|:-----:|-------|
|
||||
| Review findings | 40 | 40/40 | 0 blocker / 0 major / 0 minor / 0 nit |
|
||||
| Test coverage | 20 | 20/20 | 1 src / 1 test files |
|
||||
| PR hygiene | 15 | 10/15 | desc ✓ / linked issue ✗ / size ✓ |
|
||||
| Commit quality | 15 | 15/15 | 0/0 conventional |
|
||||
| CI status | 10 | 5/10 | unknown |
|
||||
|
||||
### 👥 Suggested reviewers (3)
|
||||
- @doc-maintainer — 6 file(s): README.md, README.zh-CN.md, doc/changes/org-team-projects.md …
|
||||
- @go-reviewer — 2 file(s): shortcuts/org/org.go, shortcuts/org/org_test.go
|
||||
- @qa-reviewer — 1 file(s): shortcuts/org/org_test.go
|
||||
|
||||
### Next steps
|
||||
1. 满足合并门禁;如策略开启 auto_merge 且操作者带 --apply,可执行合并
|
||||
---
|
||||
*Generated by gitlink-gatekeeper · policy-as-code PR gate · re-run after changes*
|
||||
|
|
@ -0,0 +1,138 @@
|
|||
# gatekeeper 仓库体检报告 —— Gitlink/gitlink-cli(2026-06-10)
|
||||
|
||||
> 对 **113 个 open PR** 全量 dry-run(**只读,零写入**)· 策略 `gatekeeper.yaml` · 成功 113 / 失败 0
|
||||
>
|
||||
> **诚实口径**:批扫未注入 AI 审查发现,review_findings 维按 0 发现计满分(**该维度未评**);CI 维按 `--skip-ci` 统一记 unknown(半分)。其余维度为真实采集。因此**总分代表「除人工/AI 审查外的工程卫生分」,偏乐观**;裁决分布同理。
|
||||
|
||||
## 总览
|
||||
|
||||
- 裁决分布:COMMENT **6** · PASS **105** · REQUEST_CHANGES **2**
|
||||
- 分数:min 70 / 中位 90 / 均值 88.5 / max 95
|
||||
- **0%** 的 PR 测试覆盖维 0 分(改动不带任何测试)
|
||||
- **96%** 的 PR 未关联 issue
|
||||
- **2%** 的 PR 触发 REQUEST_CHANGES(硬门禁或低分)
|
||||
|
||||
硬门禁命中:`require_tests_for_src_changes` × 2
|
||||
|
||||
## 全量明细(按分数降序)
|
||||
|
||||
| PR | 标题 | 作者 | 总分 | 裁决 | 硬门禁失败 | 卫生(描述/关联/体量) |
|
||||
|----|------|------|-----:|------|-----------|---------------------|
|
||||
| [#145](https://www.gitlink.org.cn/Gitlink/gitlink-cli/pulls/145) | fix(issue): preserve metadata during batch close | dtwdtw | 95 | PASS | — | ✓/✓/✓ |
|
||||
| [#218](https://www.gitlink.org.cn/Gitlink/gitlink-cli/pulls/218) | feat(skills): 新增 科研Fork影响力分析 的skill : gitlink-re | yangsai | 90 | PASS | — | ✓/✗/✓ |
|
||||
| [#177](https://www.gitlink.org.cn/Gitlink/gitlink-cli/pulls/177) | feat(wiki): add wiki management shortcuts | wangyue111 | 90 | PASS | — | ✓/✗/✓ |
|
||||
| [#217](https://www.gitlink.org.cn/Gitlink/gitlink-cli/pulls/217) | feat(commands): add command catalog export | wangyue111 | 90 | PASS | — | ✓/✗/✓ |
|
||||
| [#216](https://www.gitlink.org.cn/Gitlink/gitlink-cli/pulls/216) | feat(api): support saved variables in batch plan | wangyue111 | 90 | PASS | — | ✓/✗/✓ |
|
||||
| [#214](https://www.gitlink.org.cn/Gitlink/gitlink-cli/pulls/214) | feat(pr): add conversation comment shortcuts | wangyue111 | 90 | PASS | — | ✓/✗/✓ |
|
||||
| [#213](https://www.gitlink.org.cn/Gitlink/gitlink-cli/pulls/213) | feat(repo): add mirror sync shortcut | wangyue111 | 90 | PASS | — | ✓/✗/✓ |
|
||||
| [#212](https://www.gitlink.org.cn/Gitlink/gitlink-cli/pulls/212) | feat(feedback): add feedback shortcut | wangyue111 | 90 | PASS | — | ✓/✗/✓ |
|
||||
| [#211](https://www.gitlink.org.cn/Gitlink/gitlink-cli/pulls/211) | feat(repo): add profile view shortcuts | wangyue111 | 90 | PASS | — | ✓/✗/✓ |
|
||||
| [#210](https://www.gitlink.org.cn/Gitlink/gitlink-cli/pulls/210) | feat(skills): 新增维护者交接与分支治理 Skills | Mengz | 90 | PASS | — | ✓/✗/✓ |
|
||||
| [#208](https://www.gitlink.org.cn/Gitlink/gitlink-cli/pulls/208) | feat(user): add pinned project shortcuts | wangyue111 | 90 | PASS | — | ✓/✗/✓ |
|
||||
| [#207](https://www.gitlink.org.cn/Gitlink/gitlink-cli/pulls/207) | feat(user): add statistics shortcuts | wangyue111 | 90 | PASS | — | ✓/✗/✓ |
|
||||
| [#206](https://www.gitlink.org.cn/Gitlink/gitlink-cli/pulls/206) | feat(commit): add commit inspection shortcuts | wangyue111 | 90 | PASS | — | ✓/✗/✓ |
|
||||
| [#204](https://www.gitlink.org.cn/Gitlink/gitlink-cli/pulls/204) | feat(org): 增强组织团队与成员管理快捷命令 | Mengz | 90 | PASS | — | ✓/✗/✓ |
|
||||
| [#203](https://www.gitlink.org.cn/Gitlink/gitlink-cli/pulls/203) | feat(user): 增加用户画像分析快捷命令 | Mengz | 90 | PASS | — | ✓/✗/✓ |
|
||||
| [#202](https://www.gitlink.org.cn/Gitlink/gitlink-cli/pulls/202) | feat(ignore): add ignore template shortcuts | wangyue111 | 90 | PASS | — | ✓/✗/✓ |
|
||||
| [#201](https://www.gitlink.org.cn/Gitlink/gitlink-cli/pulls/201) | feat(account): add account auth shortcuts | wangyue111 | 90 | PASS | — | ✓/✗/✓ |
|
||||
| [#200](https://www.gitlink.org.cn/Gitlink/gitlink-cli/pulls/200) | feat(pr): add review journal shortcuts | wangyue111 | 90 | PASS | — | ✓/✗/✓ |
|
||||
| [#199](https://www.gitlink.org.cn/Gitlink/gitlink-cli/pulls/199) | feat(code): add read-only code browsing shortcut | wangyue111 | 90 | PASS | — | ✓/✗/✓ |
|
||||
| [#198](https://www.gitlink.org.cn/Gitlink/gitlink-cli/pulls/198) | feat(message): 增加消息中心快捷命令 | Mengz | 90 | PASS | — | ✓/✗/✓ |
|
||||
| [#197](https://www.gitlink.org.cn/Gitlink/gitlink-cli/pulls/197) | feat(message-settings): 增加消息通知设置快捷命令 | Mengz | 90 | PASS | — | ✓/✗/✓ |
|
||||
| [#194](https://www.gitlink.org.cn/Gitlink/gitlink-cli/pulls/194) | fix(pr): 补齐 pr +view 的合并与关闭时间字段 | Mengz | 90 | PASS | — | ✓/✗/✓ |
|
||||
| [#193](https://www.gitlink.org.cn/Gitlink/gitlink-cli/pulls/193) | feat(shortcut): add shortcuts/wiki | co63oc | 90 | PASS | — | ✓/✗/✓ |
|
||||
| [#192](https://www.gitlink.org.cn/Gitlink/gitlink-cli/pulls/192) | feat(repo): add navigation unit shortcuts | wangyue111 | 90 | PASS | — | ✓/✗/✓ |
|
||||
| [#191](https://www.gitlink.org.cn/Gitlink/gitlink-cli/pulls/191) | feat(user): add profile shortcuts | wangyue111 | 90 | PASS | — | ✓/✗/✓ |
|
||||
| [#187](https://www.gitlink.org.cn/Gitlink/gitlink-cli/pulls/187) | feat(org): add team project bulk shortcuts | wangyue111 | 90 | PASS | — | ✓/✗/✓ |
|
||||
| [#186](https://www.gitlink.org.cn/Gitlink/gitlink-cli/pulls/186) | feat(ref): add branch and tag shortcuts | wangyue111 | 90 | PASS | — | ✓/✗/✓ |
|
||||
| [#185](https://www.gitlink.org.cn/Gitlink/gitlink-cli/pulls/185) | Add workflow pull request review queue | Mengz | 90 | PASS | — | ✓/✗/✓ |
|
||||
| [#184](https://www.gitlink.org.cn/Gitlink/gitlink-cli/pulls/184) | Add workflow release notes generator | Mengz | 90 | PASS | — | ✓/✗/✓ |
|
||||
| [#183](https://www.gitlink.org.cn/Gitlink/gitlink-cli/pulls/183) | feat(project): add lifecycle flow shortcuts | wangyue111 | 90 | PASS | — | ✓/✗/✓ |
|
||||
| [#182](https://www.gitlink.org.cn/Gitlink/gitlink-cli/pulls/182) | feat(issue): add journal maintenance shortcuts | wangyue111 | 90 | PASS | — | ✓/✗/✓ |
|
||||
| [#181](https://www.gitlink.org.cn/Gitlink/gitlink-cli/pulls/181) | feat(topic): add project topic shortcuts | wangyue111 | 90 | PASS | — | ✓/✗/✓ |
|
||||
| [#180](https://www.gitlink.org.cn/Gitlink/gitlink-cli/pulls/180) | feat(template): add project template shortcuts | wangyue111 | 90 | PASS | — | ✓/✗/✓ |
|
||||
| [#179](https://www.gitlink.org.cn/Gitlink/gitlink-cli/pulls/179) | feat(dataset): add research dataset shortcuts | wangyue111 | 90 | PASS | — | ✓/✗/✓ |
|
||||
| [#178](https://www.gitlink.org.cn/Gitlink/gitlink-cli/pulls/178) | feat(contents): add repository content shortcuts | wangyue111 | 90 | PASS | — | ✓/✗/✓ |
|
||||
| [#176](https://www.gitlink.org.cn/Gitlink/gitlink-cli/pulls/176) | feat(user): add dashboard shortcuts | wangyue111 | 90 | PASS | — | ✓/✗/✓ |
|
||||
| [#175](https://www.gitlink.org.cn/Gitlink/gitlink-cli/pulls/175) | feat(notification): add message and setting shor | wangyue111 | 90 | PASS | — | ✓/✗/✓ |
|
||||
| [#174](https://www.gitlink.org.cn/Gitlink/gitlink-cli/pulls/174) | feat(public-key): add SSH key shortcuts | wangyue111 | 90 | PASS | — | ✓/✗/✓ |
|
||||
| [#173](https://www.gitlink.org.cn/Gitlink/gitlink-cli/pulls/173) | feat(account): add cancellation shortcuts | wangyue111 | 90 | PASS | — | ✓/✗/✓ |
|
||||
| [#172](https://www.gitlink.org.cn/Gitlink/gitlink-cli/pulls/172) | feat(account): add security shortcuts | wangyue111 | 90 | PASS | — | ✓/✗/✓ |
|
||||
| [#171](https://www.gitlink.org.cn/Gitlink/gitlink-cli/pulls/171) | feat(oauth): add token shortcuts | wangyue111 | 90 | PASS | — | ✓/✗/✓ |
|
||||
| [#170](https://www.gitlink.org.cn/Gitlink/gitlink-cli/pulls/170) | Add repository file search and batch commit shor | Mengz | 90 | PASS | — | ✓/✗/✓ |
|
||||
| [#167](https://www.gitlink.org.cn/Gitlink/gitlink-cli/pulls/167) | feat(account): add email verification shortcuts | wangyue111 | 90 | PASS | — | ✓/✗/✓ |
|
||||
| [#164](https://www.gitlink.org.cn/Gitlink/gitlink-cli/pulls/164) | Add pull request review comment management short | Mengz | 90 | PASS | — | ✓/✗/✓ |
|
||||
| [#163](https://www.gitlink.org.cn/Gitlink/gitlink-cli/pulls/163) | Add complete issue comment management shortcuts | Mengz | 90 | PASS | — | ✓/✗/✓ |
|
||||
| [#160](https://www.gitlink.org.cn/Gitlink/gitlink-cli/pulls/160) | Add GitLink feedback submission shortcut | Mengz | 90 | PASS | — | ✓/✗/✓ |
|
||||
| [#158](https://www.gitlink.org.cn/Gitlink/gitlink-cli/pulls/158) | Add code trace analysis shortcuts | Mengz | 90 | PASS | — | ✓/✗/✓ |
|
||||
| [#153](https://www.gitlink.org.cn/Gitlink/gitlink-cli/pulls/153) | feat(shortcut): add shortcuts/ignore | co63oc | 90 | PASS | — | ✓/✗/✓ |
|
||||
| [#151](https://www.gitlink.org.cn/Gitlink/gitlink-cli/pulls/151) | feat(transfer): add transfer request shortcuts | wangyue111 | 90 | PASS | — | ✓/✗/✓ |
|
||||
| [#135](https://www.gitlink.org.cn/Gitlink/gitlink-cli/pulls/135) | feat(dev): add developer resource shortcuts | wangyue111 | 90 | PASS | — | ✓/✗/✓ |
|
||||
| [#118](https://www.gitlink.org.cn/Gitlink/gitlink-cli/pulls/118) | feat(access): add project access shortcuts | wangyue111 | 90 | PASS | — | ✓/✗/✓ |
|
||||
| [#114](https://www.gitlink.org.cn/Gitlink/gitlink-cli/pulls/114) | feat(mirror): add mirror repository shortcuts | wangyue111 | 90 | PASS | — | ✓/✗/✓ |
|
||||
| [#113](https://www.gitlink.org.cn/Gitlink/gitlink-cli/pulls/113) | feat(todo): add request approval shortcuts | wangyue111 | 90 | PASS | — | ✓/✗/✓ |
|
||||
| [#107](https://www.gitlink.org.cn/Gitlink/gitlink-cli/pulls/107) | feat(star): add starred project shortcuts | wangyue111 | 90 | PASS | — | ✓/✗/✓ |
|
||||
| [#83](https://www.gitlink.org.cn/Gitlink/gitlink-cli/pulls/83) | feat(org): add team project binding shortcuts | wangyue111 | 90 | PASS | — | ✓/✗/✓ |
|
||||
| [#82](https://www.gitlink.org.cn/Gitlink/gitlink-cli/pulls/82) | feat(meta): add attachment and metadata shortcut | wangyue111 | 90 | PASS | — | ✓/✗/✓ |
|
||||
| [#78](https://www.gitlink.org.cn/Gitlink/gitlink-cli/pulls/78) | feat(branch): complete OpenAPI shortcuts | wangyue111 | 90 | PASS | — | ✓/✗/✓ |
|
||||
| [#76](https://www.gitlink.org.cn/Gitlink/gitlink-cli/pulls/76) | feat(notification): add OpenAPI shortcuts | wangyue111 | 90 | PASS | — | ✓/✗/✓ |
|
||||
| [#72](https://www.gitlink.org.cn/Gitlink/gitlink-cli/pulls/72) | feat(template): add project template shortcuts | wangyue111 | 90 | PASS | — | ✓/✗/✓ |
|
||||
| [#70](https://www.gitlink.org.cn/Gitlink/gitlink-cli/pulls/70) | feat(user): add account and stats shortcuts | wangyue111 | 90 | PASS | — | ✓/✗/✓ |
|
||||
| [#65](https://www.gitlink.org.cn/Gitlink/gitlink-cli/pulls/65) | feat(wiki): add OpenAPI shortcuts | wangyue111 | 90 | PASS | — | ✓/✗/✓ |
|
||||
| [#64](https://www.gitlink.org.cn/Gitlink/gitlink-cli/pulls/64) | feat(dataset): add OpenAPI shortcuts | wangyue111 | 90 | PASS | — | ✓/✗/✓ |
|
||||
| [#63](https://www.gitlink.org.cn/Gitlink/gitlink-cli/pulls/63) | feat(code): add repository code OpenAPI shortcut | wangyue111 | 90 | PASS | — | ✓/✗/✓ |
|
||||
| [#152](https://www.gitlink.org.cn/Gitlink/gitlink-cli/pulls/152) | chore(doc): fix README.md | co63oc | 90 | PASS | — | ✓/✗/✓ |
|
||||
| [#137](https://www.gitlink.org.cn/Gitlink/gitlink-cli/pulls/137) | feat(skills): 增强 7 个 Agent Skill + 新增 2 个 Skill( | whale | 90 | PASS | — | ✓/✗/✓ |
|
||||
| [#149](https://www.gitlink.org.cn/Gitlink/gitlink-cli/pulls/149) | feat(skills): 新增 学者/团队科研画像生成 的skill : gitlink-sc | yangsai | 90 | PASS | — | ✓/✗/✓ |
|
||||
| [#148](https://www.gitlink.org.cn/Gitlink/gitlink-cli/pulls/148) | feat(skills): 新增 科研热点追踪与知识图谱构建 的skill : gitlink- | yangsai | 90 | PASS | — | ✓/✗/✓ |
|
||||
| [#144](https://www.gitlink.org.cn/Gitlink/gitlink-cli/pulls/144) | feat(skills): 新增 3 个 Agent Skill — wiki-builder, | whale | 90 | PASS | — | ✓/✗/✓ |
|
||||
| [#134](https://www.gitlink.org.cn/Gitlink/gitlink-cli/pulls/134) | 新增 shell 自动补全命令 | Mengz | 90 | PASS | — | ✓/✗/✓ |
|
||||
| [#99](https://www.gitlink.org.cn/Gitlink/gitlink-cli/pulls/99) | 新增 5 个仓库检查快捷命令 (languages/contributors/files/tag | jiangtx | 90 | PASS | — | ✓/✗/✓ |
|
||||
| [#86](https://www.gitlink.org.cn/Gitlink/gitlink-cli/pulls/86) | fix: preserve issue metadata on update | dtwdtw | 90 | PASS | — | ✓/✗/✓ |
|
||||
| [#73](https://www.gitlink.org.cn/Gitlink/gitlink-cli/pulls/73) | feat(user): add SSH key shortcuts | Mengz | 90 | PASS | — | ✓/✗/✓ |
|
||||
| [#67](https://www.gitlink.org.cn/Gitlink/gitlink-cli/pulls/67) | feat(repo): add repository units shortcuts | Mengz | 90 | PASS | — | ✓/✗/✓ |
|
||||
| [#60](https://www.gitlink.org.cn/Gitlink/gitlink-cli/pulls/60) | feat: add notification shortcuts | Mengz | 90 | PASS | — | ✓/✗/✓ |
|
||||
| [#58](https://www.gitlink.org.cn/Gitlink/gitlink-cli/pulls/58) | feat: add repository reaction shortcuts | Mengz | 90 | PASS | — | ✓/✗/✓ |
|
||||
| [#126](https://www.gitlink.org.cn/Gitlink/gitlink-cli/pulls/126) | feat(skills): 新增 gitlink-scaffold 社区健康文件体检 Skill | Ct201314 | 90 | PASS | — | ✓/✗/✓ |
|
||||
| [#56](https://www.gitlink.org.cn/Gitlink/gitlink-cli/pulls/56) | feat: add git tag shortcut group | Mengz | 90 | PASS | — | ✓/✗/✓ |
|
||||
| [#125](https://www.gitlink.org.cn/Gitlink/gitlink-cli/pulls/125) | feat(skills): 新增 gitlink-newcomer 新人引导 Skill | Ct201314 | 90 | PASS | — | ✓/✗/✓ |
|
||||
| [#127](https://www.gitlink.org.cn/Gitlink/gitlink-cli/pulls/127) | feat(skills): 新增 gitlink-deps 依赖追踪 Skill | Ct201314 | 90 | PASS | — | ✓/✗/✓ |
|
||||
| [#128](https://www.gitlink.org.cn/Gitlink/gitlink-cli/pulls/128) | feat(skills): 新增 gitlink-contributor 贡献者致谢与成长 Sk | Ct201314 | 90 | PASS | — | ✓/✗/✓ |
|
||||
| [#129](https://www.gitlink.org.cn/Gitlink/gitlink-cli/pulls/129) | feat(skills): 新增 gitlink-kb 知识库问答 Skill | Ct201314 | 90 | PASS | — | ✓/✗/✓ |
|
||||
| [#115](https://www.gitlink.org.cn/Gitlink/gitlink-cli/pulls/115) | feat: add catalog template shortcuts | Mengz | 90 | PASS | — | ✓/✗/✓ |
|
||||
| [#116](https://www.gitlink.org.cn/Gitlink/gitlink-cli/pulls/116) | 新增仓库洞察快捷命令 | Mengz | 90 | PASS | — | ✓/✗/✓ |
|
||||
| [#119](https://www.gitlink.org.cn/Gitlink/gitlink-cli/pulls/119) | 新增仓库转移快捷命令 | Mengz | 90 | PASS | — | ✓/✗/✓ |
|
||||
| [#122](https://www.gitlink.org.cn/Gitlink/gitlink-cli/pulls/122) | 完善仓库 README 快捷命令 | Mengz | 90 | PASS | — | ✓/✗/✓ |
|
||||
| [#50](https://www.gitlink.org.cn/Gitlink/gitlink-cli/pulls/50) | feat: add wiki shortcut group | Mengz | 90 | PASS | — | ✓/✗/✓ |
|
||||
| [#54](https://www.gitlink.org.cn/Gitlink/gitlink-cli/pulls/54) | gitlink-growth 开源贡献者成长系统 Skill 贡献 | yingjie | 90 | PASS | — | ✓/✗/✓ |
|
||||
| [#23](https://www.gitlink.org.cn/Gitlink/gitlink-cli/pulls/23) | feat: support fork metadata in pr create | Mengz | 90 | PASS | — | ✓/✗/✓ |
|
||||
| [#196](https://www.gitlink.org.cn/Gitlink/gitlink-cli/pulls/196) | feat(release): 增加发布资产管理快捷命令 | Mengz | 88 | PASS | — | ✓/✗/✓ |
|
||||
| [#215](https://www.gitlink.org.cn/Gitlink/gitlink-cli/pulls/215) | fix(client): improve API robustness | wangyue111 | 87 | PASS | — | ✓/✗/✓ |
|
||||
| [#147](https://www.gitlink.org.cn/Gitlink/gitlink-cli/pulls/147) | feat(shortcuts): 新增 wiki/commit/file/star/watch | chroe | 86 | PASS | — | ✓/✗/✓ |
|
||||
| [#209](https://www.gitlink.org.cn/Gitlink/gitlink-cli/pulls/209) | feat(milestone): 增加里程碑进度分析快捷命令 | Mengz | 85 | PASS | — | ✓/✗/✓ |
|
||||
| [#205](https://www.gitlink.org.cn/Gitlink/gitlink-cli/pulls/205) | fix(issue): 修复详情缺失并保护更新元数据 | Mengz | 85 | PASS | — | ✓/✗/✓ |
|
||||
| [#195](https://www.gitlink.org.cn/Gitlink/gitlink-cli/pulls/195) | feat(compare): 新增 compare 汇总与提交筛选能力 | Mengz | 85 | PASS | — | ✓/✗/✓ |
|
||||
| [#190](https://www.gitlink.org.cn/Gitlink/gitlink-cli/pulls/190) | Add workflow release readiness gate | Mengz | 85 | PASS | — | ✓/✗/✓ |
|
||||
| [#189](https://www.gitlink.org.cn/Gitlink/gitlink-cli/pulls/189) | Add workflow duplicate issue detection | Mengz | 85 | PASS | — | ✓/✗/✓ |
|
||||
| [#188](https://www.gitlink.org.cn/Gitlink/gitlink-cli/pulls/188) | Add workflow dependency risk audit | Mengz | 85 | PASS | — | ✓/✗/✓ |
|
||||
| [#165](https://www.gitlink.org.cn/Gitlink/gitlink-cli/pulls/165) | feat(issue): add batch maintenance shortcuts | wangyue111 | 85 | PASS | — | ✓/✗/✓ |
|
||||
| [#159](https://www.gitlink.org.cn/Gitlink/gitlink-cli/pulls/159) | Add member application workflow shortcuts | Mengz | 85 | PASS | — | ✓/✗/✓ |
|
||||
| [#77](https://www.gitlink.org.cn/Gitlink/gitlink-cli/pulls/77) | feat(journal): add issue and PR comment shortcut | wangyue111 | 85 | PASS | — | ✓/✗/✓ |
|
||||
| [#150](https://www.gitlink.org.cn/Gitlink/gitlink-cli/pulls/150) | 新增 Issue 批量导出命令 | Mengz | 85 | PASS | — | ✓/✗/✓ |
|
||||
| [#142](https://www.gitlink.org.cn/Gitlink/gitlink-cli/pulls/142) | 新增 PR 本地检出命令 | Mengz | 85 | PASS | — | ✓/✗/✓ |
|
||||
| [#100](https://www.gitlink.org.cn/Gitlink/gitlink-cli/pulls/100) | 查看指定时间范围的开发统计 | jiangtx | 85 | PASS | — | ✗/✗/✓ |
|
||||
| [#101](https://www.gitlink.org.cn/Gitlink/gitlink-cli/pulls/101) | 查看用户项目动态 | jiangtx | 85 | PASS | — | ✗/✗/✓ |
|
||||
| [#21](https://www.gitlink.org.cn/Gitlink/gitlink-cli/pulls/21) | feat: add attachment shortcut group | Mengz | 85 | PASS | — | ✓/✗/✓ |
|
||||
| [#139](https://www.gitlink.org.cn/Gitlink/gitlink-cli/pulls/139) | feat(wiki): 新增 Wiki 页面与目录管理 Shortcuts | whale | 82 | COMMENT | — | ✓/✗/✓ |
|
||||
| [#130](https://www.gitlink.org.cn/Gitlink/gitlink-cli/pulls/130) | feat(workflows): 新增 gitlink-flow 社区运营自动化端到端工作流 | Ct201314 | 82 | COMMENT | — | ✓/✗/✓ |
|
||||
| [#97](https://www.gitlink.org.cn/Gitlink/gitlink-cli/pulls/97) | 基础设施修复 | jiangtx | 81 | COMMENT | — | ✗/✗/✓ |
|
||||
| [#123](https://www.gitlink.org.cn/Gitlink/gitlink-cli/pulls/123) | 新增 Release 资产下载命令 | Mengz | 80 | COMMENT | — | ✗/✗/✓ |
|
||||
| [#103](https://www.gitlink.org.cn/Gitlink/gitlink-cli/pulls/103) | feat: 新建 pm 模块,添加 6 条项目管理命令 | wyxttn | 78 | COMMENT | — | — |
|
||||
| [#131](https://www.gitlink.org.cn/Gitlink/gitlink-cli/pulls/131) | 子赛题三 - Java-Gatekeeper 端到端自动化质量门禁工作流 | xxxx12 | 75 | REQUEST_CHANGES | require_tests_for_src_changes | ✓/✓/✓ |
|
||||
| [#30](https://www.gitlink.org.cn/Gitlink/gitlink-cli/pulls/30) | 增加wiki管理的shortcut | camelliamc | 74 | COMMENT | — | — |
|
||||
| [#146](https://www.gitlink.org.cn/Gitlink/gitlink-cli/pulls/146) | feat: 新增 Showcase Dashboard 交互式展示页 | chroe | 70 | REQUEST_CHANGES | require_tests_for_src_changes | ✓/✗/✓ |
|
||||
|
||||
## 这份报告说明了什么
|
||||
|
||||
- 同一份 `gatekeeper.yaml` 策略可以**无人值守地体检一个真实活跃仓库的全部积压**——确定性评分意味着大规模治理零 AI 成本,AI 只在需要语义判断(review_findings)时按需介入。
|
||||
- 任何人重跑本报告(`python3 scripts/gatekeeper_sweep.py`)会对同一组 PR 得到同样的分数与裁决。
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
{
|
||||
"_comment": "对 Gitlink/gitlink-cli PR #15222(feat/org-team-projects,head bcc27bf)真实 diff 的人工+AI 代码审查发现。每条均可在该 PR 的 shortcuts/org/org.go 对应行号复核——非样例数据。",
|
||||
"findings": [
|
||||
{
|
||||
"severity": "major",
|
||||
"file": "shortcuts/org/org.go",
|
||||
"line": 156,
|
||||
"message": "parseBool 只把字面 \"true\" 当真:用户传 --dry-run=1 / t / TRUE 以外写法会被静默解析为 false。该 flag 守护的是 team-projects-remove-all(批量移除团队全部项目)这类破坏性操作——预览意图被静默降级为真实执行。建议改用 strconv.ParseBool(与 shortcuts/common/runner.go:52 解析 flag 默认值的行为一致),无法识别的值应报错而非吞掉"
|
||||
},
|
||||
{
|
||||
"severity": "minor",
|
||||
"file": "shortcuts/org/org.go",
|
||||
"line": 103,
|
||||
"message": "team-projects-remove-all 一条命令清空团队全部项目绑定,除 --dry-run 外无确认机制;建议在 Description/help 标注危险性,或要求显式 --yes 二次确认"
|
||||
},
|
||||
{
|
||||
"severity": "minor",
|
||||
"file": "shortcuts/org/org.go",
|
||||
"line": 142,
|
||||
"message": "dry-run 预览 payload 中 action(add_all_team_projects/remove_all_team_projects)与实际请求 path 段(create_all/destroy_all,见 :134)是两套词汇,排查问题时易误导;建议统一或在 payload 同时给出两者映射"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -0,0 +1,62 @@
|
|||
# owner-rules.yaml — 文件路径 → reviewer 路由表(gitlink-gatekeeper 工作流步骤 1)
|
||||
#
|
||||
# 作用:PR 看门人闭环的第一步「路由」。脚本拉取 PR 变更文件后,按下面的
|
||||
# glob 规则把每个文件映射到建议 reviewer,写进评分卡的「Suggested
|
||||
# reviewers」分区(REQUEST_CHANGES 时也写进 tracking issue)。
|
||||
#
|
||||
# 语义(见 scripts/gatekeeper_workflow.py route_reviewers):
|
||||
# - rules 按顺序匹配,**首个命中的规则生效**(顺序即优先级,把更具体的放前面)。
|
||||
# - glob 用 Python fnmatch 语法(* 不跨目录段时也会匹配 /,与 fnmatch 行为一致)。
|
||||
# - 一个文件命中后不再继续匹配后续规则;多个 reviewer 写在同一规则的 reviewers 列表里。
|
||||
# - 未命中任何规则的文件归到 default_reviewers(兜底)。
|
||||
#
|
||||
# 排序示例(首个命中即生效、顺序即优先级,请按需排序):
|
||||
# 下面把目录 glob `skills/**` 放在语言 glob `**/*.go` 之前,于是 `skills/foo.go`
|
||||
# 会先命中 `skills/**` → 路由给 skill-owner,而不会落到 go-reviewer。若你希望
|
||||
# skills 下的 Go 文件仍由 go-reviewer 审,就把语言规则提到目录规则之前
|
||||
# (或在目录规则里收窄 glob,如 `skills/**/*.md`)。
|
||||
#
|
||||
# 注意:本表只产出「建议」,不调用任何写操作;真正分配 reviewer 由维护者在
|
||||
# PR 页面决定。gatekeeper 不替人点提交(SSOT 第 8 节安全规则)。
|
||||
#
|
||||
# reviewers 填 GitLink 用户名(login)。下方为占位示例,真实使用时替换为本仓库的维护者。
|
||||
|
||||
rules:
|
||||
# —— 文档:只改文档走文档维护者,避免占用代码 reviewer ——
|
||||
- glob: "docs/**"
|
||||
reviewers: ["doc-maintainer"]
|
||||
- glob: "**/*.md"
|
||||
reviewers: ["doc-maintainer"]
|
||||
|
||||
# —— 工作流 / 脚本本体 ——
|
||||
- glob: "workflow/**"
|
||||
reviewers: ["workflow-owner"]
|
||||
- glob: "skills/**"
|
||||
reviewers: ["skill-owner"]
|
||||
|
||||
# —— 按语言路由到对应方向的 reviewer ——
|
||||
- glob: "**/*_test.go"
|
||||
reviewers: ["go-reviewer", "qa-reviewer"]
|
||||
- glob: "**/*.go"
|
||||
reviewers: ["go-reviewer"]
|
||||
- glob: "test_*.py"
|
||||
reviewers: ["py-reviewer", "qa-reviewer"]
|
||||
- glob: "**/*.py"
|
||||
reviewers: ["py-reviewer"]
|
||||
- glob: "**/*.ts"
|
||||
reviewers: ["fe-reviewer"]
|
||||
- glob: "**/*.js"
|
||||
reviewers: ["fe-reviewer"]
|
||||
|
||||
# —— 高敏感区:CI / 依赖 / 安全配置,强制资深 reviewer ——
|
||||
- glob: ".gitea/**"
|
||||
reviewers: ["ci-owner", "security-reviewer"]
|
||||
- glob: "**/Dockerfile"
|
||||
reviewers: ["ci-owner"]
|
||||
- glob: "go.mod"
|
||||
reviewers: ["security-reviewer"]
|
||||
- glob: "go.sum"
|
||||
reviewers: ["security-reviewer"]
|
||||
|
||||
# 未命中上面任何规则的文件,兜底分配给这些人
|
||||
default_reviewers: ["maintainer"]
|
||||
|
|
@ -0,0 +1,227 @@
|
|||
# SPDX-License-Identifier: MulanPSL-2.0
|
||||
"""gatekeeper_sweep —— 对一个仓库的全部 open PR 批量跑门禁(只读 dry-run),出治理报告。
|
||||
|
||||
把单 PR 的「策略 → 评分卡 → 裁决」升级为仓库级体检:
|
||||
1. 翻页拉取 PR 列表,筛出 open;
|
||||
2. 逐个调用 gatekeeper_workflow.py(强制 dry-run,绝不 --apply,对远端零写入);
|
||||
3. 汇总每个 PR 的 summary.json → 聚合统计 + 全量明细表 → sweep-report.md / sweep-summary.json。
|
||||
|
||||
诚实口径:批扫不注入 AI 审查发现(--findings),review_findings 维按 0 发现计满分,
|
||||
报告中明确标注「该维度未评」;其余 4 维(测试/卫生/commit/CI)为真实采集结果。
|
||||
纯标准库,无第三方依赖。
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
import urllib.request
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
API_BASE = "https://www.gitlink.org.cn/api"
|
||||
HYGIENE_RE = re.compile(r"desc (✓|✗) / linked issue (✓|✗) / size (✓|✗)")
|
||||
|
||||
|
||||
def fetch_open_prs(owner: str, repo: str, limit_pages: int = 20) -> list[dict[str, Any]]:
|
||||
"""翻页拉取 PR 列表并筛出 open(列表接口的 status 参数不可靠,按字段过滤)。"""
|
||||
items: list[dict[str, Any]] = []
|
||||
page = 1
|
||||
while page <= limit_pages:
|
||||
url = f"{API_BASE}/{owner}/{repo}/pulls.json?page={page}&limit=50"
|
||||
with urllib.request.urlopen(url, timeout=30) as resp:
|
||||
data = json.loads(resp.read().decode("utf-8"))
|
||||
batch = data.get("issues") or []
|
||||
if not batch:
|
||||
break
|
||||
items.extend(batch)
|
||||
if len(items) >= int(data.get("search_count") or 0):
|
||||
break
|
||||
page += 1
|
||||
return [it for it in items if it.get("pull_request_staus") == "open"]
|
||||
|
||||
|
||||
def run_one(
|
||||
workflow_script: Path,
|
||||
owner: str,
|
||||
repo: str,
|
||||
number: int,
|
||||
policy: Path,
|
||||
owner_rules: Path,
|
||||
cli_bin: str,
|
||||
out_dir: Path,
|
||||
) -> dict[str, Any]:
|
||||
"""对单个 PR 跑一次 dry-run 门禁,返回解析后的行记录(失败不抛,记 error)。"""
|
||||
cmd = [
|
||||
sys.executable,
|
||||
str(workflow_script),
|
||||
"--owner", owner,
|
||||
"--repo", repo,
|
||||
"--pr", str(number),
|
||||
"--policy", str(policy),
|
||||
"--owner-rules", str(owner_rules),
|
||||
"--cli-bin", cli_bin,
|
||||
"--skip-ci",
|
||||
"--output-dir", str(out_dir),
|
||||
]
|
||||
proc = subprocess.run(cmd, capture_output=True, text=True, timeout=180)
|
||||
slug = f"{owner}_{repo}_pr{number}".replace("/", "_")
|
||||
summary_path = out_dir / f"{slug}_summary.json"
|
||||
if proc.returncode == 1 or not summary_path.exists():
|
||||
return {"number": number, "error": (proc.stderr or proc.stdout)[-200:].strip()}
|
||||
summary = json.loads(summary_path.read_text(encoding="utf-8"))
|
||||
hygiene = ""
|
||||
scorecard_path = out_dir / f"{slug}_scorecard.md"
|
||||
if scorecard_path.exists():
|
||||
m = HYGIENE_RE.search(scorecard_path.read_text(encoding="utf-8"))
|
||||
if m:
|
||||
hygiene = "/".join(m.groups()) # 例如 "✓/✗/✓":描述/关联issue/体量
|
||||
return {
|
||||
"number": number,
|
||||
"verdict": summary.get("verdict"),
|
||||
"total": summary.get("total"),
|
||||
"scores": summary.get("scores", {}),
|
||||
"hard_gate_failures": [f.get("gate") if isinstance(f, dict) else f
|
||||
for f in summary.get("hard_gate_failures", [])],
|
||||
"hygiene": hygiene,
|
||||
"suggested_reviewers": summary.get("routing", {}).get("suggested_reviewers", []),
|
||||
}
|
||||
|
||||
|
||||
def aggregate(rows: list[dict[str, Any]]) -> dict[str, Any]:
|
||||
ok = [r for r in rows if "error" not in r]
|
||||
totals = sorted(r["total"] for r in ok)
|
||||
verdicts: dict[str, int] = {}
|
||||
gate_hits: dict[str, int] = {}
|
||||
for r in ok:
|
||||
verdicts[r["verdict"]] = verdicts.get(r["verdict"], 0) + 1
|
||||
for g in r["hard_gate_failures"]:
|
||||
gate_hits[str(g)] = gate_hits.get(str(g), 0) + 1
|
||||
def pct(n: int) -> str:
|
||||
return f"{100 * n / len(ok):.0f}%" if ok else "0%"
|
||||
no_linked = sum(1 for r in ok if r["hygiene"] and r["hygiene"].split("/")[1] == "✗")
|
||||
zero_cov = sum(1 for r in ok if r["scores"].get("test_coverage") == 0)
|
||||
return {
|
||||
"scanned": len(rows),
|
||||
"succeeded": len(ok),
|
||||
"failed": len(rows) - len(ok),
|
||||
"verdicts": verdicts,
|
||||
"score_min": totals[0] if totals else None,
|
||||
"score_median": totals[len(totals) // 2] if totals else None,
|
||||
"score_avg": round(sum(totals) / len(totals), 1) if totals else None,
|
||||
"score_max": totals[-1] if totals else None,
|
||||
"hard_gate_hits": gate_hits,
|
||||
"pct_zero_test_coverage": pct(zero_cov),
|
||||
"pct_no_linked_issue": pct(no_linked),
|
||||
"pct_request_changes": pct(verdicts.get("REQUEST_CHANGES", 0)),
|
||||
}
|
||||
|
||||
|
||||
def render_report(
|
||||
owner: str, repo: str, policy_label: str, date_label: str,
|
||||
rows: list[dict[str, Any]], agg: dict[str, Any],
|
||||
pr_meta: dict[int, dict[str, Any]],
|
||||
) -> str:
|
||||
ok = [r for r in rows if "error" not in r]
|
||||
lines = [
|
||||
f"# gatekeeper 仓库体检报告 —— {owner}/{repo}({date_label})",
|
||||
"",
|
||||
f"> 对 **{agg['scanned']} 个 open PR** 全量 dry-run(**只读,零写入**)· 策略 `{policy_label}` · "
|
||||
f"成功 {agg['succeeded']} / 失败 {agg['failed']}",
|
||||
">",
|
||||
"> **诚实口径**:批扫未注入 AI 审查发现,review_findings 维按 0 发现计满分(**该维度未评**);"
|
||||
"CI 维按 `--skip-ci` 统一记 unknown(半分)。其余维度为真实采集。"
|
||||
"因此**总分代表「除人工/AI 审查外的工程卫生分」,偏乐观**;裁决分布同理。",
|
||||
"",
|
||||
"## 总览",
|
||||
"",
|
||||
f"- 裁决分布:{' · '.join(f'{k} **{v}**' for k, v in sorted(agg['verdicts'].items()))}",
|
||||
f"- 分数:min {agg['score_min']} / 中位 {agg['score_median']} / 均值 {agg['score_avg']} / max {agg['score_max']}",
|
||||
f"- **{agg['pct_zero_test_coverage']}** 的 PR 测试覆盖维 0 分(改动不带任何测试)",
|
||||
f"- **{agg['pct_no_linked_issue']}** 的 PR 未关联 issue",
|
||||
f"- **{agg['pct_request_changes']}** 的 PR 触发 REQUEST_CHANGES(硬门禁或低分)",
|
||||
"",
|
||||
"硬门禁命中:" + (";".join(f"`{g}` × {n}" for g, n in sorted(agg["hard_gate_hits"].items(), key=lambda x: -x[1])) or "无"),
|
||||
"",
|
||||
"## 全量明细(按分数降序)",
|
||||
"",
|
||||
"| PR | 标题 | 作者 | 总分 | 裁决 | 硬门禁失败 | 卫生(描述/关联/体量) |",
|
||||
"|----|------|------|-----:|------|-----------|---------------------|",
|
||||
]
|
||||
for r in sorted(ok, key=lambda x: -x["total"]):
|
||||
meta = pr_meta.get(r["number"], {})
|
||||
title = str(meta.get("name", ""))[:48].replace("|", "\\|")
|
||||
gates = ", ".join(str(g) for g in r["hard_gate_failures"]) or "—"
|
||||
lines.append(
|
||||
f"| [#{r['number']}](https://www.gitlink.org.cn/{owner}/{repo}/pulls/{r['number']}) "
|
||||
f"| {title} | {meta.get('author_name', '?')} | {r['total']} | {r['verdict']} | {gates} | {r['hygiene'] or '—'} |"
|
||||
)
|
||||
errs = [r for r in rows if "error" in r]
|
||||
if errs:
|
||||
lines += ["", "## 跑失败的 PR", ""]
|
||||
lines += [f"- #{r['number']}:`{r['error']}`" for r in errs]
|
||||
lines += [
|
||||
"",
|
||||
"## 这份报告说明了什么",
|
||||
"",
|
||||
"- 同一份 `gatekeeper.yaml` 策略可以**无人值守地体检一个真实活跃仓库的全部积压**——"
|
||||
"确定性评分意味着大规模治理零 AI 成本,AI 只在需要语义判断(review_findings)时按需介入。",
|
||||
"- 任何人重跑本报告(`python3 scripts/gatekeeper_sweep.py`)会对同一组 PR 得到同样的分数与裁决。",
|
||||
"",
|
||||
]
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
def main(argv: list[str] | None = None) -> int:
|
||||
parser = argparse.ArgumentParser(description="对全部 open PR 批量 dry-run 出治理报告")
|
||||
parser.add_argument("--owner", default="Gitlink")
|
||||
parser.add_argument("--repo", default="gitlink-cli")
|
||||
parser.add_argument("--policy", type=Path, required=True)
|
||||
parser.add_argument("--owner-rules", dest="owner_rules", type=Path, required=True)
|
||||
parser.add_argument("--cli-bin", default="gitlink-cli")
|
||||
parser.add_argument("--output-dir", type=Path, default=Path("sweep-outputs"))
|
||||
parser.add_argument("--date-label", default="sweep", help="报告日期标签(可复现:不取系统时间)")
|
||||
parser.add_argument("--max", type=int, default=0, help="只跑前 N 个(0=全量),用于试跑")
|
||||
parser.add_argument("--sleep", type=float, default=0.2, help="相邻 PR 间隔秒数(对平台礼貌)")
|
||||
args = parser.parse_args(argv)
|
||||
|
||||
workflow_script = Path(__file__).with_name("gatekeeper_workflow.py")
|
||||
runs_dir = args.output_dir / "runs"
|
||||
runs_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
prs = fetch_open_prs(args.owner, args.repo)
|
||||
if args.max:
|
||||
prs = prs[: args.max]
|
||||
pr_meta = {int(p["pull_request_number"]): p for p in prs}
|
||||
print(f"open PR 共 {len(prs)} 个,开始批扫(dry-run,零写入)…", flush=True)
|
||||
|
||||
rows: list[dict[str, Any]] = []
|
||||
for i, p in enumerate(prs, 1):
|
||||
number = int(p["pull_request_number"])
|
||||
row = run_one(workflow_script, args.owner, args.repo, number,
|
||||
args.policy, args.owner_rules, args.cli_bin, runs_dir)
|
||||
rows.append(row)
|
||||
tag = row.get("verdict", "ERROR")
|
||||
print(f"[{i}/{len(prs)}] PR #{number} → {tag} {row.get('total', '')}", flush=True)
|
||||
time.sleep(args.sleep)
|
||||
|
||||
agg = aggregate(rows)
|
||||
policy_label = args.policy.name
|
||||
report = render_report(args.owner, args.repo, policy_label, args.date_label, rows, agg, pr_meta)
|
||||
(args.output_dir / "sweep-report.md").write_text(report, encoding="utf-8")
|
||||
(args.output_dir / "sweep-summary.json").write_text(
|
||||
json.dumps({"aggregate": agg, "rows": rows}, ensure_ascii=False, indent=2),
|
||||
encoding="utf-8",
|
||||
)
|
||||
print(f"\n报告:{args.output_dir / 'sweep-report.md'}")
|
||||
print(f"汇总:{args.output_dir / 'sweep-summary.json'}")
|
||||
print(f"裁决分布:{agg['verdicts']} · 均分 {agg['score_avg']}")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,248 @@
|
|||
#!/usr/bin/env python3
|
||||
"""可复现评分单测 —— 把「同输入 → 同分 → 同裁决」从口号变成可验证事实。
|
||||
|
||||
纯标准库 unittest(Python 3.9 兼容)。直接 import `scripts/gatekeeper_workflow.py`
|
||||
的确定性算法(score_dimensions / evaluate_hard_gates / decide_verdict),对四个
|
||||
权威裁决案例(skills/gitlink-gatekeeper/examples/decision-*.md 与
|
||||
scorecard-sample.md)构造等价的 ScoreInput,断言**总分**与**三态裁决**与文档逐位一致。
|
||||
|
||||
任意一处算法改动若改变了这四个案例的分值,本测试立即变红——即为「确定性」的回归护栏。
|
||||
|
||||
运行:
|
||||
python3 workflow/tests/test_scoring.py
|
||||
或:
|
||||
python3 -m unittest workflow.tests.test_scoring # 在仓库根目录
|
||||
|
||||
数值来源(默认策略 gatekeeper.yaml,与脚本内置 DEFAULT_POLICY 一致):
|
||||
权重 40/20/15/15/10;severity_penalty blocker=100/major=25/minor=5/nit=1;
|
||||
thresholds pass=85 / request_changes=60;max_changed_files=80。
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import importlib.util
|
||||
import sys
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# 以绝对路径加载被测脚本(它在 scripts/ 下、非包,按文件直接载入最稳)
|
||||
#
|
||||
# 注意:必须先把模块塞进 sys.modules 再 exec —— 被测脚本用了
|
||||
# `from __future__ import annotations`,Python 3.9 的 @dataclass 在解析字符串
|
||||
# 注解时会回查 sys.modules[cls.__module__],未注册会取到 None 而报
|
||||
# AttributeError('NoneType' object has no attribute '__dict__')。
|
||||
# --------------------------------------------------------------------------- #
|
||||
_SCRIPT = (
|
||||
Path(__file__).resolve().parent.parent / "scripts" / "gatekeeper_workflow.py"
|
||||
)
|
||||
_spec = importlib.util.spec_from_file_location("gatekeeper_workflow", _SCRIPT)
|
||||
assert _spec and _spec.loader, f"无法定位被测脚本:{_SCRIPT}"
|
||||
gw = importlib.util.module_from_spec(_spec)
|
||||
sys.modules["gatekeeper_workflow"] = gw
|
||||
_spec.loader.exec_module(gw) # type: ignore[union-attr]
|
||||
|
||||
ScoreInput = gw.ScoreInput
|
||||
Finding = gw.Finding
|
||||
score_dimensions = gw.score_dimensions
|
||||
evaluate_hard_gates = gw.evaluate_hard_gates
|
||||
decide_verdict = gw.decide_verdict
|
||||
# 默认策略(深拷贝一份,避免任何用例意外改到共享 dict)
|
||||
import json as _json # noqa: E402
|
||||
|
||||
DEFAULT_POLICY = _json.loads(_json.dumps(gw.DEFAULT_POLICY))
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# 构造辅助:把「严重度计数 / 文件数 / commit 计数」翻译成 ScoreInput 字段
|
||||
# --------------------------------------------------------------------------- #
|
||||
|
||||
def _findings(blocker: int = 0, major: int = 0, minor: int = 0, nit: int = 0):
|
||||
"""按严重度计数生成 Finding 列表(message/file/line 对评分无影响,仅 severity 计 penalty)。"""
|
||||
out = []
|
||||
for sev, n in (("blocker", blocker), ("major", major), ("minor", minor), ("nit", nit)):
|
||||
for i in range(n):
|
||||
out.append(Finding(severity=sev, message=f"{sev} #{i}", file="f.go", line=i + 1))
|
||||
return out
|
||||
|
||||
|
||||
def _commits(conforming: int, total: int):
|
||||
"""生成 total 条 commit message,其中 conforming 条符合 Conventional Commits。"""
|
||||
assert conforming <= total
|
||||
msgs = [f"feat(mod{i}): conforming change {i}" for i in range(conforming)]
|
||||
msgs += [f"wip update {i}" for i in range(total - conforming)] # 'wip ...' 不匹配规约
|
||||
return msgs
|
||||
|
||||
|
||||
def _files(n: int):
|
||||
"""生成 n 个占位变更文件路径(仅用于 size 维度计 len,src/test 计数由字段直接给定)。"""
|
||||
return [f"path/file_{i}.go" for i in range(n)]
|
||||
|
||||
|
||||
def _build(
|
||||
*,
|
||||
pr_id: str,
|
||||
title: str,
|
||||
desc_len: int,
|
||||
linked_issue: bool,
|
||||
n_files: int,
|
||||
src: int,
|
||||
tests: int,
|
||||
commits: tuple, # (conforming, total)
|
||||
ci: str,
|
||||
findings_counts: dict,
|
||||
) -> ScoreInput:
|
||||
description = "x" * desc_len if desc_len else ""
|
||||
return ScoreInput(
|
||||
pr_id=pr_id,
|
||||
title=title,
|
||||
description=description,
|
||||
changed_files=_files(n_files),
|
||||
changed_src=src,
|
||||
changed_tests=tests,
|
||||
commits=_commits(*commits),
|
||||
ci_status=ci,
|
||||
linked_issue=linked_issue,
|
||||
findings=_findings(**findings_counts),
|
||||
)
|
||||
|
||||
|
||||
def _run(inp: ScoreInput):
|
||||
"""跑完整确定性链路,返回 (total, verdict)。"""
|
||||
dims = score_dimensions(inp, DEFAULT_POLICY)
|
||||
failures = evaluate_hard_gates(inp, DEFAULT_POLICY)
|
||||
verdict = decide_verdict(dims["total"], bool(failures), DEFAULT_POLICY)
|
||||
return dims, failures, verdict
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# 四个权威案例
|
||||
# --------------------------------------------------------------------------- #
|
||||
|
||||
class TestAuthoritativeCases(unittest.TestCase):
|
||||
"""对照 examples/ 下四个裁决记录,断言总分与裁决。"""
|
||||
|
||||
def test_decision_pass(self):
|
||||
# decision-pass.md:3 src / 2 test、desc 142(含#198)、4/4 commit、CI passing、
|
||||
# 0/0/1/2 findings → 33+17+15+15+10 = 90 → PASS
|
||||
inp = _build(
|
||||
pr_id="214",
|
||||
title="feat(search): validate pagination params",
|
||||
desc_len=142,
|
||||
linked_issue=True,
|
||||
n_files=5,
|
||||
src=3,
|
||||
tests=2,
|
||||
commits=(4, 4),
|
||||
ci="passing",
|
||||
findings_counts={"minor": 1, "nit": 2},
|
||||
)
|
||||
dims, failures, verdict = _run(inp)
|
||||
self.assertEqual(dims["review_findings"]["score"], 33)
|
||||
self.assertEqual(dims["test_coverage"]["score"], 17)
|
||||
self.assertEqual(dims["pr_hygiene"]["score"], 15)
|
||||
self.assertEqual(dims["commit_quality"]["score"], 15)
|
||||
self.assertEqual(dims["ci_status"]["score"], 10)
|
||||
self.assertEqual(failures, [])
|
||||
self.assertEqual(dims["total"], 90)
|
||||
self.assertEqual(verdict, "PASS")
|
||||
|
||||
def test_decision_request_changes(self):
|
||||
# decision-request-changes.md:4 src / 0 test(触发硬门禁
|
||||
# require_tests_for_src_changes)、desc 88 无关联、2/3 commit、CI passing、
|
||||
# 0/1/1/2 findings → 8+0+10+10+10 = 38 → REQUEST_CHANGES
|
||||
inp = _build(
|
||||
pr_id="305",
|
||||
title="refactor(billing): rework settlement pipeline",
|
||||
desc_len=88,
|
||||
linked_issue=False,
|
||||
n_files=4,
|
||||
src=4,
|
||||
tests=0,
|
||||
commits=(2, 3),
|
||||
ci="passing",
|
||||
findings_counts={"major": 1, "minor": 1, "nit": 2},
|
||||
)
|
||||
dims, failures, verdict = _run(inp)
|
||||
self.assertEqual(dims["review_findings"]["score"], 8)
|
||||
self.assertEqual(dims["test_coverage"]["score"], 0)
|
||||
self.assertEqual(dims["pr_hygiene"]["score"], 10)
|
||||
self.assertEqual(dims["commit_quality"]["score"], 10)
|
||||
self.assertEqual(dims["ci_status"]["score"], 10)
|
||||
gate_names = {f["gate"] for f in failures}
|
||||
self.assertIn("require_tests_for_src_changes", gate_names)
|
||||
self.assertEqual(dims["total"], 38)
|
||||
self.assertEqual(verdict, "REQUEST_CHANGES")
|
||||
|
||||
def test_decision_comment(self):
|
||||
# decision-comment.md:2 src / 1 test、desc 52 无关联、2/3 commit、CI passing、
|
||||
# 0/0/3/2 findings → 23+15+10+10+10 = 68 ∈ [60,85) 且无硬门禁 → COMMENT
|
||||
inp = _build(
|
||||
pr_id="277",
|
||||
title="feat(config): merge defaults on load",
|
||||
desc_len=52,
|
||||
linked_issue=False,
|
||||
n_files=2,
|
||||
src=2,
|
||||
tests=1,
|
||||
commits=(2, 3),
|
||||
ci="passing",
|
||||
findings_counts={"minor": 3, "nit": 2},
|
||||
)
|
||||
dims, failures, verdict = _run(inp)
|
||||
self.assertEqual(dims["review_findings"]["score"], 23)
|
||||
self.assertEqual(dims["test_coverage"]["score"], 15)
|
||||
self.assertEqual(dims["pr_hygiene"]["score"], 10)
|
||||
self.assertEqual(dims["commit_quality"]["score"], 10)
|
||||
self.assertEqual(dims["ci_status"]["score"], 10)
|
||||
self.assertEqual(failures, [])
|
||||
self.assertEqual(dims["total"], 68)
|
||||
self.assertEqual(verdict, "COMMENT")
|
||||
|
||||
def test_scorecard_sample(self):
|
||||
# scorecard-sample.md:4 src / 0 test(触发硬门禁)、desc 64 无关联、3/4 commit、
|
||||
# CI passing、0/1/2/1 findings → 4+0+10+11+10 = 35 → REQUEST_CHANGES
|
||||
inp = _build(
|
||||
pr_id="128",
|
||||
title="feat(auth): add refresh-token rotation",
|
||||
desc_len=64,
|
||||
linked_issue=False,
|
||||
n_files=6,
|
||||
src=4,
|
||||
tests=0,
|
||||
commits=(3, 4),
|
||||
ci="passing",
|
||||
findings_counts={"major": 1, "minor": 2, "nit": 1},
|
||||
)
|
||||
dims, failures, verdict = _run(inp)
|
||||
self.assertEqual(dims["review_findings"]["score"], 4)
|
||||
self.assertEqual(dims["test_coverage"]["score"], 0)
|
||||
self.assertEqual(dims["pr_hygiene"]["score"], 10)
|
||||
self.assertEqual(dims["commit_quality"]["score"], 11)
|
||||
self.assertEqual(dims["ci_status"]["score"], 10)
|
||||
gate_names = {f["gate"] for f in failures}
|
||||
self.assertIn("require_tests_for_src_changes", gate_names)
|
||||
self.assertEqual(dims["total"], 35)
|
||||
self.assertEqual(verdict, "REQUEST_CHANGES")
|
||||
|
||||
|
||||
class TestVerdictBoundaries(unittest.TestCase):
|
||||
"""裁决判定树(decide_verdict)边界:与 thresholds pass=85 / request_changes=60 一致。"""
|
||||
|
||||
def test_pass_threshold_inclusive(self):
|
||||
self.assertEqual(decide_verdict(85, False, DEFAULT_POLICY), "PASS")
|
||||
|
||||
def test_comment_band(self):
|
||||
self.assertEqual(decide_verdict(60, False, DEFAULT_POLICY), "COMMENT")
|
||||
self.assertEqual(decide_verdict(84, False, DEFAULT_POLICY), "COMMENT")
|
||||
|
||||
def test_request_changes_below_band(self):
|
||||
self.assertEqual(decide_verdict(59, False, DEFAULT_POLICY), "REQUEST_CHANGES")
|
||||
|
||||
def test_hard_gate_short_circuits_high_score(self):
|
||||
# 即便满分,硬门禁失败也直接 REQUEST_CHANGES
|
||||
self.assertEqual(decide_verdict(100, True, DEFAULT_POLICY), "REQUEST_CHANGES")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main(verbosity=2)
|
||||
|
|
@ -0,0 +1 @@
|
|||
outputs/
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue