Merge PR #300: 新增批量重开、标签、指派、评论、导出、导入命令
# Conflicts: # README.md # README.zh-CN.md # shortcuts/issue/batch.go # shortcuts/issue/issue.go # skills/gitlink-issue/SKILL.md
This commit is contained in:
commit
527b8c2e63
529
README.md
529
README.md
|
|
@ -5,16 +5,93 @@
|
|||
[](https://golang.org)
|
||||
[](https://www.npmjs.com/package/@gitlink-ai/cli)
|
||||
|
||||
The official [GitLink](https://www.gitlink.org.cn) CLI tool — built for humans and AI Agents. Supports **macOS, Linux, and Windows**. Covers repository management, issue tracking, pull requests, CI/CD, and AI-powered workflows, with 40+ commands and 13 AI Agent [Skills](./skills/).
|
||||
The official [GitLink](https://www.gitlink.org.cn) CLI tool — built for humans and AI Agents. Supports **macOS, Linux, and Windows**. Covers repository management, issue tracking, pull requests, webhooks, member collaboration, CI/CD, and AI-powered workflows, with 40+ commands and AI Agent [Skills](./skills/README.md).
|
||||
|
||||
**[中文文档](./README.zh-CN.md)**
|
||||
|
||||
[Install](#installation--quick-start) · [AI Agent Skills](#ai-agent-skills) · [Auth](#configure--use) · [Commands](#usage-examples) · [Contributing](#related-projects)
|
||||
|
||||
## Contributors
|
||||
|
||||
<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>
|
||||
|
||||
## Why gitlink-cli?
|
||||
|
||||
- **Agent-Native Design** — 13 structured [Skills](./skills/) out of the box, compatible with Claude Code, OpenClaw, and other AI platforms — Agents can operate GitLink with zero extra setup
|
||||
- **Wide Coverage** — Repository, Issue, PR, Branch, Release, CI, Org, Search, User — all core domains covered
|
||||
- **Agent-Native Design** — Structured [Skills](./skills/README.md) out of the box, compatible with Claude Code, OpenClaw, and other AI platforms — Agents can operate GitLink with zero extra setup
|
||||
- **Wide Coverage** — Repository, Issue, PR, Webhook, Member, Branch, Release, CI, Pipeline, Org, Search, and User workflows are covered by high-level commands
|
||||
- **AI-Friendly & Optimized** — Every command is tested with real Agents, featuring concise parameters, smart defaults, and structured output
|
||||
- **Cross-Platform** — Runs on macOS, Linux, and Windows (x64/arm64), install via `npm install -g @gitlink-ai/cli` in one command, binary auto-downloaded
|
||||
- **Open Source, Zero Barriers** — MulanPSL-2.0 license, ready to use, just `npm install`
|
||||
|
|
@ -26,15 +103,21 @@ The official [GitLink](https://www.gitlink.org.cn) CLI tool — built for humans
|
|||
|
||||
| Category | Capabilities |
|
||||
|----------|-------------|
|
||||
| 📦 Repo | List, create, fork, delete repositories, view repo info |
|
||||
| 🐛 Issue | Create, update, close, batch close, comment on issues |
|
||||
| 📦 Repo | List, create, fork, delete repositories, view repo info, insights, and interactions |
|
||||
| 🐛 Issue | Create, update, close, batch close/update/delete/reopen/label/assign/comment/export/import, comment on issues |
|
||||
| 🔖 Label | Create, list, update, delete issue labels |
|
||||
| 🔀 PR | Create, merge, review pull requests, view changed files |
|
||||
| 👥 Member | List, add, remove repository members, change roles, create and accept invite links |
|
||||
| 🌿 Branch | Create, delete, list, protect, unprotect branches |
|
||||
| 🏷️ Release | Create, view, delete releases |
|
||||
| 🏷️ Release | Create, edit, update, view, delete releases |
|
||||
| 🏢 Org | Manage organizations, members, teams |
|
||||
| 🔧 CI | View builds, logs, CI/CD operations |
|
||||
| ⚙️ Pipeline | Run, inspect, enable, disable, delete pipeline workflows and logs |
|
||||
| 🔔 Webhook | Manage repo webhooks and test deliveries |
|
||||
| 🔍 Search | Search repositories, users |
|
||||
| 📊 Dataset | Query research datasets by project |
|
||||
| 👤 User | View user profiles and info |
|
||||
| 📊 Profile | User ability, role, major, activity, and contribution statistics |
|
||||
| 📋 PM | Sprint management, kanban boards, weekly reports |
|
||||
| 🤖 Workflow | AI-powered issue triage, PR review, release notes |
|
||||
|
||||
|
|
@ -55,7 +138,7 @@ The official [GitLink](https://www.gitlink.org.cn) CLI tool — built for humans
|
|||
**From npm (recommended):**
|
||||
|
||||
```bash
|
||||
# One command: installs CLI binary + all 13 AI Agent Skills
|
||||
# One command: installs CLI binary + AI Agent Skills
|
||||
npm install -g @gitlink-ai/cli
|
||||
```
|
||||
|
||||
|
|
@ -136,50 +219,81 @@ gitlink-cli repo +list
|
|||
# View repository info
|
||||
gitlink-cli repo +info --owner Gitlink --repo forgeplus
|
||||
|
||||
# Read repository README
|
||||
gitlink-cli repo +readme --owner Gitlink --repo forgeplus --ref master
|
||||
|
||||
# List repository files at root or a directory
|
||||
gitlink-cli repo +tree --owner Gitlink --repo forgeplus --ref master
|
||||
gitlink-cli repo +tree --owner Gitlink --repo forgeplus --path src --ref main
|
||||
|
||||
# Show language breakdown
|
||||
gitlink-cli repo +languages --owner Gitlink --repo forgeplus
|
||||
|
||||
# List contributors
|
||||
gitlink-cli repo +contributors --owner Gitlink --repo forgeplus
|
||||
|
||||
# Show contributor code-line stats for a branch, tag, or commit
|
||||
gitlink-cli repo +contributor-stats --owner Gitlink --repo forgeplus --ref master --pass-year 1
|
||||
|
||||
# Show repository code stats
|
||||
gitlink-cli repo +code-stats --owner Gitlink --repo forgeplus --ref master
|
||||
|
||||
# List watchers and stargazers in a time range
|
||||
gitlink-cli repo +watchers --owner Gitlink --repo forgeplus --start-at 1714521600 --end-at 1717200000
|
||||
gitlink-cli repo +stargazers --owner Gitlink --repo forgeplus --start-at 1714521600 --end-at 1717200000
|
||||
|
||||
# Preview and apply repository interaction actions
|
||||
gitlink-cli repo +follow --owner Gitlink --repo forgeplus --dry-run
|
||||
gitlink-cli repo +follow --owner Gitlink --repo forgeplus
|
||||
gitlink-cli repo +unfollow --owner Gitlink --repo forgeplus --project-id 123
|
||||
gitlink-cli repo +like --owner Gitlink --repo forgeplus
|
||||
gitlink-cli repo +unlike --owner Gitlink --repo forgeplus --project-id 123
|
||||
|
||||
# Create a repository
|
||||
gitlink-cli repo +create -n my-project -d "Project description"
|
||||
|
||||
# Fork a repository
|
||||
gitlink-cli repo +fork --owner Gitlink --repo forgeplus
|
||||
|
||||
# View repository details
|
||||
gitlink-cli repo +detail --owner Gitlink --repo forgeplus
|
||||
|
||||
# View repository simple info
|
||||
gitlink-cli repo +simple --owner Gitlink --repo forgeplus
|
||||
|
||||
# View repository settings
|
||||
gitlink-cli repo +settings --owner Gitlink --repo forgeplus
|
||||
|
||||
# List navigation units
|
||||
gitlink-cli repo +units --owner Gitlink --repo forgeplus
|
||||
|
||||
# Update navigation units
|
||||
gitlink-cli repo +units-update --owner Gitlink --repo forgeplus --unit-types code,issues,pulls --dry-run
|
||||
gitlink-cli repo +units-update --owner Gitlink --repo forgeplus --unit-types code,issues,pulls
|
||||
|
||||
# List project topics
|
||||
gitlink-cli repo +topics --owner Gitlink --repo forgeplus
|
||||
|
||||
# Add a project topic
|
||||
gitlink-cli repo +topic-add --owner Gitlink --repo forgeplus --name "machine-learning" --dry-run
|
||||
gitlink-cli repo +topic-add --owner Gitlink --repo forgeplus --name "machine-learning"
|
||||
|
||||
# Delete a project topic
|
||||
gitlink-cli repo +topic-delete --owner Gitlink --repo forgeplus --id 123 --dry-run
|
||||
gitlink-cli repo +topic-delete --owner Gitlink --repo forgeplus --id 123
|
||||
|
||||
# List organizations available for transfer
|
||||
gitlink-cli repo +transfer-orgs --owner Gitlink --repo forgeplus
|
||||
|
||||
# Apply for repository transfer
|
||||
gitlink-cli repo +transfer --owner Gitlink --repo forgeplus --org-id 456 --dry-run
|
||||
gitlink-cli repo +transfer --owner Gitlink --repo forgeplus --org-id 456
|
||||
|
||||
# Cancel transfer application
|
||||
gitlink-cli repo +transfer-cancel --owner Gitlink --repo forgeplus --dry-run
|
||||
gitlink-cli repo +transfer-cancel --owner Gitlink --repo forgeplus
|
||||
```
|
||||
gitlink-cli repo +fork --owner Gitlink --repo forgeplus
|
||||
```
|
||||
|
||||
### Webhook Management
|
||||
|
||||
```bash
|
||||
# List webhooks
|
||||
gitlink-cli webhook +list --owner Gitlink --repo forgeplus
|
||||
|
||||
# Create a webhook
|
||||
gitlink-cli webhook +create --owner Gitlink --repo forgeplus \
|
||||
--url https://example.com/hook --events push,create
|
||||
|
||||
# Test a webhook
|
||||
gitlink-cli webhook +test --owner Gitlink --repo forgeplus --id 68
|
||||
|
||||
# View webhook delivery tasks
|
||||
gitlink-cli webhook +tasks --owner Gitlink --repo forgeplus --id 68
|
||||
```
|
||||
|
||||
### Member Management
|
||||
|
||||
```bash
|
||||
# List repository members
|
||||
gitlink-cli member +list --owner Gitlink --repo forgeplus
|
||||
|
||||
# Add a member
|
||||
gitlink-cli member +add --owner Gitlink --repo forgeplus --user-id 101
|
||||
|
||||
# Preview batch add without changing data
|
||||
gitlink-cli member +batch-add --owner Gitlink --repo forgeplus --user-ids 101,102 --dry-run
|
||||
|
||||
# Batch add members from a CSV file
|
||||
gitlink-cli member +batch-add --owner Gitlink --repo forgeplus --from members.csv
|
||||
|
||||
# Change a member role
|
||||
gitlink-cli member +role --owner Gitlink --repo forgeplus --user-id 101 --role Developer
|
||||
|
||||
# Create an invite link
|
||||
gitlink-cli member +invite-link --owner Gitlink --repo forgeplus --role developer --apply true
|
||||
```
|
||||
|
||||
### Issue Management
|
||||
|
||||
|
|
@ -190,9 +304,15 @@ gitlink-cli issue +list --owner Gitlink --repo forgeplus
|
|||
# Create an issue
|
||||
gitlink-cli issue +create --owner Gitlink --repo forgeplus -t "Bug: Login failed" -b "Steps to reproduce..."
|
||||
|
||||
# Create an issue with metadata
|
||||
gitlink-cli issue +create --owner Gitlink --repo forgeplus -t "Bug: Login failed" --priority-id 3 --tag-ids 4,5 --assigner-ids 7
|
||||
|
||||
# View an issue
|
||||
gitlink-cli issue +view --owner Gitlink --repo forgeplus -i 123
|
||||
|
||||
# Update issue metadata
|
||||
gitlink-cli issue +update --owner Gitlink --repo forgeplus --number 123 --priority-id 4 --branch bugfix/login --due-date 2026-06-15
|
||||
|
||||
# Close an issue
|
||||
gitlink-cli issue +close --owner Gitlink --repo forgeplus -i 123
|
||||
|
||||
|
|
@ -202,8 +322,79 @@ gitlink-cli issue +batch-close --owner Gitlink --repo forgeplus --numbers 123,12
|
|||
# Batch close issues from a CSV file
|
||||
gitlink-cli issue +batch-close --owner Gitlink --repo forgeplus --from issues.csv
|
||||
|
||||
# Preview batch metadata update by API issue IDs
|
||||
# Note: --ids uses API issue IDs, not web URL issue numbers.
|
||||
gitlink-cli issue +batch-update --owner Gitlink --repo forgeplus --ids 101,102 --status-id 3 --priority-id 2 --dry-run
|
||||
|
||||
# Destructive batch delete requires both dry-run first and --yes for real execution
|
||||
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
|
||||
|
||||
# Batch reopen closed issues by issue numbers
|
||||
gitlink-cli issue +batch-reopen --owner Gitlink --repo forgeplus --numbers 123,124 --dry-run
|
||||
gitlink-cli issue +batch-reopen --owner Gitlink --repo forgeplus --from issues.csv
|
||||
|
||||
# Batch add/remove labels by API issue IDs (requires numeric tag IDs)
|
||||
gitlink-cli issue +batch-label --owner Gitlink --repo forgeplus --ids 101,102 --add 1,2 --dry-run
|
||||
gitlink-cli issue +batch-label --owner Gitlink --repo forgeplus --ids 101,102 --remove 3
|
||||
|
||||
# Batch assign/unassign users by API issue IDs (requires numeric user IDs)
|
||||
gitlink-cli issue +batch-assign --owner Gitlink --repo forgeplus --ids 101,102 --add 5,6 --dry-run
|
||||
gitlink-cli issue +batch-assign --owner Gitlink --repo forgeplus --ids 101,102 --remove 7
|
||||
|
||||
# Batch add comments to multiple issues by issue numbers
|
||||
gitlink-cli issue +batch-comment --owner Gitlink --repo forgeplus --numbers 123,124 --message "Batch update notice" --dry-run
|
||||
gitlink-cli issue +batch-comment --owner Gitlink --repo forgeplus --from issues.csv --message "Processed"
|
||||
|
||||
# Export issues to CSV or JSON with filters
|
||||
gitlink-cli issue +batch-export --owner Gitlink --repo forgeplus --state open --format csv --output issues.csv
|
||||
gitlink-cli issue +batch-export --owner Gitlink --repo forgeplus --state closed --keyword bug --format json --output closed_issues.json
|
||||
|
||||
# Import issues from a CSV file
|
||||
gitlink-cli issue +batch-import --owner Gitlink --repo forgeplus --from new_issues.csv --dry-run
|
||||
gitlink-cli issue +batch-import --owner Gitlink --repo forgeplus --from new_issues.csv
|
||||
|
||||
# Add a comment
|
||||
gitlink-cli issue +comment --owner Gitlink --repo forgeplus -i 123 -b "Fixed"
|
||||
|
||||
# List issue assigners
|
||||
gitlink-cli issue +assigners --owner Gitlink --repo forgeplus
|
||||
|
||||
# List issue authors
|
||||
gitlink-cli issue +authors --owner Gitlink --repo forgeplus
|
||||
|
||||
# List issue priorities
|
||||
gitlink-cli issue +priorities --owner Gitlink --repo forgeplus
|
||||
|
||||
# List issue tags
|
||||
gitlink-cli issue +tags --owner Gitlink --repo forgeplus --only-name
|
||||
|
||||
# List issue statuses
|
||||
gitlink-cli issue +statuses --owner Gitlink --repo forgeplus
|
||||
```
|
||||
|
||||
`issue +view`, `issue +update`, `issue +close`, and `issue +comment` prefer
|
||||
`--number` / `-n` for the issue number shown in the web URL. `--id` / `-i`
|
||||
is accepted as a compatibility alias for the same web issue number, not the
|
||||
global database ID.
|
||||
|
||||
### Label Management
|
||||
|
||||
```bash
|
||||
# List issue labels
|
||||
gitlink-cli label +list --owner Gitlink --repo forgeplus
|
||||
|
||||
# Filter labels by keyword
|
||||
gitlink-cli label +list --owner Gitlink --repo forgeplus -k bug
|
||||
|
||||
# Create a label (color defaults to #1E90FF)
|
||||
gitlink-cli label +create --owner Gitlink --repo forgeplus -n bug -d "Something is broken" -c "#FF0000"
|
||||
|
||||
# Update a label (unspecified fields are preserved)
|
||||
gitlink-cli label +update --owner Gitlink --repo forgeplus -i 42 -c "#00FF00"
|
||||
|
||||
# Delete a label
|
||||
gitlink-cli label +delete --owner Gitlink --repo forgeplus -i 42
|
||||
```
|
||||
|
||||
### Pull Requests
|
||||
|
|
@ -218,19 +409,30 @@ gitlink-cli pr +create --owner Gitlink --repo forgeplus -t "feat: Search feature
|
|||
# Create a PR (from a fork)
|
||||
gitlink-cli pr +create --owner Gitlink --repo forgeplus -t "feat: New feature" --head your_username/forgeplus:feature/my-feature --base master
|
||||
|
||||
# List PR branch candidates and check merge readiness
|
||||
gitlink-cli pr +branches --owner Gitlink --repo forgeplus
|
||||
gitlink-cli pr +check-can-merge --owner Gitlink --repo forgeplus --head feature/search --base master --dry-run
|
||||
gitlink-cli pr +check-can-merge --owner Gitlink --repo forgeplus --head feature/search --base master --yes
|
||||
|
||||
# View a PR
|
||||
gitlink-cli pr +view --owner Gitlink --repo forgeplus -i 42
|
||||
|
||||
# Merge a PR
|
||||
gitlink-cli pr +merge --owner Gitlink --repo forgeplus -i 42
|
||||
|
||||
# Reopen a closed PR
|
||||
gitlink-cli pr +reopen --owner Gitlink --repo forgeplus -i 42
|
||||
|
||||
# View changed files
|
||||
gitlink-cli pr +files --owner Gitlink --repo forgeplus -i 42
|
||||
|
||||
# List PR patchset versions
|
||||
gitlink-cli pr +versions --owner Gitlink --repo forgeplus -i 42
|
||||
|
||||
# View a patchset version diff
|
||||
gitlink-cli pr +version-diff --owner Gitlink --repo forgeplus -i 42 --version-id 16040
|
||||
|
||||
# List PR reviews
|
||||
gitlink-cli pr +reviews --owner Gitlink --repo forgeplus -i 42
|
||||
|
||||
# Create a PR review (with dry-run preview)
|
||||
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"
|
||||
```
|
||||
|
||||
### Branch Management
|
||||
|
|
@ -258,11 +460,18 @@ gitlink-cli branch +unprotect --name main
|
|||
# List releases
|
||||
gitlink-cli release +list --owner Gitlink --repo forgeplus
|
||||
|
||||
# Create a release
|
||||
gitlink-cli release +create --owner Gitlink --repo forgeplus -t v1.0.0 -n "v1.0.0 Stable" -b "Changelog..."
|
||||
# Create a release with release notes and optional assets
|
||||
gitlink-cli release +create --owner Gitlink --repo forgeplus -t v1.0.0 -n "v1.0.0 Stable" -b "Changelog..." --attachment-ids 12,34
|
||||
|
||||
# View a release
|
||||
gitlink-cli release +view --owner Gitlink --repo forgeplus -i <version_id>
|
||||
|
||||
# Get edit data and update while preserving unspecified fields
|
||||
gitlink-cli release +edit --owner Gitlink --repo forgeplus -i <version_id>
|
||||
gitlink-cli release +update --owner Gitlink --repo forgeplus -i <version_id> -b "Updated changelog" --dry-run
|
||||
|
||||
# Preview release deletion before executing it
|
||||
gitlink-cli release +delete --owner Gitlink --repo forgeplus -i <version_id> --dry-run
|
||||
```
|
||||
|
||||
### CI/CD Operations
|
||||
|
|
@ -278,6 +487,38 @@ gitlink-cli ci +log --owner Gitlink --repo forgeplus -i <build_id>
|
|||
gitlink-cli ci +restart --owner Gitlink --repo forgeplus -i <build_id>
|
||||
```
|
||||
|
||||
### Pipeline Operations
|
||||
|
||||
```bash
|
||||
# List platform pipelines
|
||||
gitlink-cli pipeline +list --owner-id 123 --page 1 --limit 20
|
||||
|
||||
# List repository pipeline runs
|
||||
gitlink-cli pipeline +runs --owner Gitlink --repo forgeplus --ref master --workflow build.yml
|
||||
|
||||
# Start a pipeline workflow, previewing the request first
|
||||
gitlink-cli pipeline +run --owner Gitlink --repo forgeplus --ref master --workflow build.yml --dry-run
|
||||
|
||||
# Inspect pipeline details and logs
|
||||
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
|
||||
|
||||
# Toggle or delete pipeline workflows, previewing destructive writes first
|
||||
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
|
||||
```
|
||||
|
||||
### Ignore File Templates
|
||||
|
||||
```bash
|
||||
# List all available .gitignore templates
|
||||
gitlink-cli ignore +list
|
||||
|
||||
# Filter templates by name
|
||||
gitlink-cli ignore +list --name Go
|
||||
```
|
||||
|
||||
### Search
|
||||
|
||||
```bash
|
||||
|
|
@ -288,6 +529,154 @@ gitlink-cli search +repos -k "machine learning"
|
|||
gitlink-cli search +users -k "zhangsan"
|
||||
```
|
||||
|
||||
### User Profile
|
||||
|
||||
`profile` surfaces GitLink's native user statistics (ability, role, major, activity,
|
||||
contribution). When `--user` is omitted it defaults to the authenticated user.
|
||||
|
||||
```bash
|
||||
# Development ability scores + language breakdown
|
||||
gitlink-cli profile +ability --user zhangsan
|
||||
|
||||
# Role positioning / major (discipline) categories
|
||||
gitlink-cli profile +role --user zhangsan
|
||||
gitlink-cli profile +major --user zhangsan
|
||||
|
||||
# Ability within a time window (Unix timestamps)
|
||||
gitlink-cli profile +ability --user zhangsan --start-time 1704067200 --end-time 1735689600
|
||||
|
||||
# Recent activity (issues / PRs / commits per day) for the current user
|
||||
gitlink-cli profile +activity
|
||||
|
||||
# Contribution heatmap for a given year
|
||||
gitlink-cli profile +contribution --user zhangsan --year 2025
|
||||
```
|
||||
|
||||
### Workflow Agent Commands
|
||||
|
||||
`workflow` provides rule-based repository analysis for maintainers and AI Agents. It currently supports:
|
||||
|
||||
- `workflow +triage`
|
||||
- `workflow +health`
|
||||
- `workflow +pr-summary`
|
||||
- `workflow +repo-report`
|
||||
|
||||
`workflow +pr-summary` defaults to `table` when `--format` is omitted.
|
||||
`workflow +repo-report` defaults to `markdown` when `--format` is omitted.
|
||||
|
||||
Examples:
|
||||
|
||||
```bash
|
||||
# Triage with local parameters
|
||||
gitlink-cli workflow +triage --title "Install failed on Windows" --body "go install failed with error" --format table
|
||||
|
||||
# Triage with JSON output
|
||||
gitlink-cli workflow +triage --title "Token leaked in logs" --body "The access token appears in command output" --format json
|
||||
|
||||
# Triage with Chinese markdown output
|
||||
gitlink-cli workflow +triage \
|
||||
--title "安装失败,无法登录" \
|
||||
--body "运行命令时报错" \
|
||||
--lang zh-CN \
|
||||
--format markdown
|
||||
|
||||
# Triage from a local JSON file
|
||||
gitlink-cli workflow +triage --from shortcuts/workflow/testdata/issue_bug.json --format json
|
||||
|
||||
# Triage by read-only GitLink fetch
|
||||
gitlink-cli workflow +triage --owner Gitlink --repo gitlink-cli --state open --limit 5 --format table
|
||||
|
||||
# Health for a healthy repository
|
||||
gitlink-cli workflow +health \
|
||||
--repository Gitlink/gitlink-cli \
|
||||
--open-issues 3 \
|
||||
--open-prs 1 \
|
||||
--has-readme \
|
||||
--has-license \
|
||||
--has-contributing \
|
||||
--agent-readiness-known \
|
||||
--agent-readiness-score 9 \
|
||||
--format table
|
||||
|
||||
# Health for a risky repository
|
||||
gitlink-cli workflow +health \
|
||||
--repository demo/repo \
|
||||
--open-issues 60 \
|
||||
--stale-issues 25 \
|
||||
--open-prs 12 \
|
||||
--stale-prs 6 \
|
||||
--recent-activity-known \
|
||||
--recent-activity-days 120 \
|
||||
--release-known=false \
|
||||
--format json
|
||||
|
||||
# Health with Chinese markdown output
|
||||
gitlink-cli workflow +health \
|
||||
--repository Gitlink/gitlink-cli \
|
||||
--open-issues 3 \
|
||||
--open-prs 1 \
|
||||
--has-readme \
|
||||
--has-license \
|
||||
--has-contributing \
|
||||
--lang zh-CN \
|
||||
--format markdown
|
||||
|
||||
# Health by read-only GitLink fetch
|
||||
gitlink-cli workflow +health --owner Gitlink --repo gitlink-cli --stale-days 30 --format table
|
||||
|
||||
# PR review summary by read-only GitLink fetch
|
||||
gitlink-cli workflow +pr-summary --owner Gitlink --repo gitlink-cli --number 1 --format markdown
|
||||
|
||||
# PR review summary from a local JSON file
|
||||
gitlink-cli workflow +pr-summary --from shortcuts/workflow/testdata/pr_summary.json --format json
|
||||
|
||||
# Repository workflow report by read-only GitLink fetch
|
||||
gitlink-cli workflow +repo-report --owner Gitlink --repo gitlink-cli --format markdown
|
||||
|
||||
# Repository workflow report from a local JSON file
|
||||
gitlink-cli workflow +repo-report --from shortcuts/workflow/testdata/repo_report.json --format json
|
||||
```
|
||||
|
||||
Output formats:
|
||||
|
||||
- `json` for scripts and AI Agents
|
||||
- `table` for terminal review
|
||||
- `markdown` for Issue comments, PR comments, release notes, and competition write-ups
|
||||
|
||||
Safety:
|
||||
|
||||
- Current workflow commands use local analysis by default and can also read GitLink data in read-only fetch mode.
|
||||
- They do not modify remote GitLink data.
|
||||
- They do not depend on LLM APIs.
|
||||
- `workflow +pr-summary` does not comment, approve, reject, or merge pull requests.
|
||||
- `workflow +repo-report` aggregates health, issue triage, and PR review summary signals without remote writes.
|
||||
|
||||
### Dataset
|
||||
|
||||
`dataset` manages and queries GitLink research datasets (title, description,
|
||||
paper content, license, owning project).
|
||||
|
||||
```bash
|
||||
# List datasets for one or more projects (by numeric project ID)
|
||||
gitlink-cli dataset +list --ids 5988
|
||||
|
||||
# View a repository's dataset and attachments
|
||||
gitlink-cli dataset +view --owner Gitlink --repo forgeplus
|
||||
|
||||
# Create / update a repository's dataset (preview first with --dry-run)
|
||||
gitlink-cli dataset +create --owner me --repo proj -t "My dataset" -d "..." --license-id 359 --dry-run
|
||||
gitlink-cli dataset +update --owner me --repo proj -t "My dataset" -d "updated"
|
||||
|
||||
# Delete a dataset attachment (destructive: preview, then confirm with --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
|
||||
```
|
||||
|
||||
> Note: `dataset +list` (platform dataset query) is verified on production
|
||||
> gitlink.org.cn. The per-repo `+view`/`+create`/`+update` routes follow the
|
||||
> published OpenAPI contract but are not yet deployed on production (they return
|
||||
> 404 there); they will work once the platform enables them.
|
||||
|
||||
### Raw API
|
||||
|
||||
For endpoints not covered by shortcuts, use the Raw API directly:
|
||||
|
|
@ -299,6 +688,12 @@ gitlink-cli api GET /users/me
|
|||
# POST request
|
||||
gitlink-cli api POST /Gitlink/forgeplus/issues --body '{"subject":"test","description":"..."}'
|
||||
|
||||
# POST request with body from a file
|
||||
gitlink-cli api POST /Gitlink/forgeplus/issues --body-file issue.json
|
||||
|
||||
# POST request with body from stdin
|
||||
Get-Content issue.json | gitlink-cli api POST /Gitlink/forgeplus/issues --body-stdin
|
||||
|
||||
# With query parameters
|
||||
gitlink-cli api GET /Gitlink/forgeplus/commits --query 'page=1&limit=5'
|
||||
```
|
||||
|
|
@ -309,7 +704,7 @@ gitlink-cli api GET /Gitlink/forgeplus/commits --query 'page=1&limit=5'
|
|||
|-----------|-------------|---------|
|
||||
| `--owner` | Repository owner | `--owner Gitlink` |
|
||||
| `--repo` | Repository name | `--repo forgeplus` |
|
||||
| `--format` | Output format (json/table/yaml) | `--format json` |
|
||||
| `--format` | Output format (json/table/yaml; workflow also supports markdown) | `--format json` |
|
||||
| `--debug` | Enable debug output | `--debug` |
|
||||
|
||||
**Automatic context resolution:** When running inside a git repository, `--owner` and `--repo` are automatically resolved from `git remote origin`.
|
||||
|
|
@ -336,25 +731,27 @@ git push gitlink
|
|||
|
||||
## AI Agent Skills
|
||||
|
||||
The `skills/` directory contains 13 Agent Skill files for AI-automated GitLink operations.
|
||||
The `skills/` directory contains Agent Skill files for AI-automated GitLink operations.
|
||||
|
||||
See [skills/README.md](skills/README.md) for details.
|
||||
See [skills/README.md](./skills/README.md) for details.
|
||||
|
||||
| Skill | Description |
|
||||
|-------|-------------|
|
||||
| `gitlink-shared` | Authentication, global parameters, safety rules, API notes |
|
||||
| `gitlink-repo` | Repository operations (create, view, delete, fork, etc.) |
|
||||
| `gitlink-issue` | Issue operations (create, update, close, comment, etc.) |
|
||||
| `gitlink-repo` | Repository operations (create, view, delete, fork, insights, etc.) |
|
||||
| `gitlink-issue` | Issue operations (create, update, close, batch update/delete, comment, etc.) |
|
||||
| `gitlink-pr` | Pull request operations (create, merge, review, etc.) |
|
||||
| `gitlink-member` | Repository member and invite link management |
|
||||
| `gitlink-branch` | Branch management (create, delete, list, protect, unprotect) |
|
||||
| `gitlink-release` | Release management (create, view, delete, etc.) |
|
||||
| `gitlink-release` | Release management (create, edit, update, view, delete, etc.) |
|
||||
| `gitlink-ci` | CI/CD operations (builds, logs, etc.) |
|
||||
| `gitlink-pipeline` | Pipeline workflow operations (runs, logs, enable, disable, delete, etc.) |
|
||||
| `gitlink-search` | Search (repositories, users, etc.) |
|
||||
| `gitlink-org` | Organization management (members, teams, etc.) |
|
||||
| `gitlink-user` | User management (profile info, etc.) |
|
||||
| `gitlink-pm` | Project management (sprints, kanban, weekly reports, etc.) |
|
||||
| `gitlink-workflow` | AI-powered workflows (issue triage, PR review, release notes, etc.) |
|
||||
| `gitlink-maintainer-copilot` | Maintainer dashboard, evidence pack, governance playbooks, and confirmed governance Issue creation |
|
||||
| `gitlink-health` | Project health analysis (PR/Issue metrics aggregation, health reports) |
|
||||
|
||||
## Project Structure
|
||||
|
||||
|
|
@ -377,10 +774,12 @@ gitlink-cli/
|
|||
│ ├── repo/ # Repository shortcuts
|
||||
│ ├── issue/ # Issue shortcuts
|
||||
│ ├── pr/ # PR shortcuts
|
||||
│ ├── member/ # Repository member shortcuts
|
||||
│ ├── branch/ # Branch shortcuts
|
||||
│ ├── release/ # Release shortcuts
|
||||
│ ├── org/ # Organization shortcuts
|
||||
│ ├── ci/ # CI shortcuts
|
||||
│ ├── pipeline/ # Pipeline shortcuts
|
||||
│ ├── search/ # Search shortcuts
|
||||
│ ├── user/ # User shortcuts
|
||||
│ └── register.go # Registration entry point
|
||||
|
|
@ -404,7 +803,7 @@ gitlink-cli/
|
|||
|
||||
## Documentation
|
||||
|
||||
- [Skills Guide](skills/README.md) — AI Agent Skills detailed documentation
|
||||
- [Skills Guide](./skills/README.md) — AI Agent Skills detailed documentation
|
||||
- [Design Document](doc/design.md) — Architecture design and development plan
|
||||
|
||||
## FAQ
|
||||
|
|
@ -459,7 +858,9 @@ Reinstall first:
|
|||
npm install -g @gitlink-ai/cli
|
||||
```
|
||||
|
||||
If the error persists, check whether the release page contains the asset for your platform, for example `gitlink-cli_<version>_windows_amd64.zip` on Windows x64. You can also download the binary manually from the release page or build from source with `go install .`.
|
||||
If the error persists, check whether the release page contains the asset for your platform,
|
||||
for example `gitlink-cli_<version>_windows_amd64.zip` on Windows x64.
|
||||
You can also download the binary manually from the release page or build from source with `go install .`.
|
||||
|
||||
### Q: Where are credentials stored on Windows?
|
||||
|
||||
|
|
@ -467,7 +868,7 @@ gitlink-cli uses Windows Credential Manager for secure token storage. If Credent
|
|||
|
||||
### Q: Where can I find the full API reference?
|
||||
|
||||
See [skills/gitlink-shared/references/api-reference.md](skills/gitlink-shared/references/api-reference.md).
|
||||
See [skills/gitlink-shared/references/api-reference.md](./skills/gitlink-shared/references/api-reference.md).
|
||||
|
||||
## License
|
||||
|
||||
|
|
|
|||
414
README.zh-CN.md
414
README.zh-CN.md
|
|
@ -5,16 +5,93 @@
|
|||
[](https://golang.org)
|
||||
[](https://www.npmjs.com/package/@gitlink-ai/cli)
|
||||
|
||||
[GitLink(确实开源)](https://www.gitlink.org.cn) 官方 CLI 工具 — 为人类和 AI Agent 双重设计。支持 **macOS、Linux、Windows**,覆盖仓库管理、Issue 追踪、Pull Request、CI/CD 和 AI 自动化工作流,包含 40+ 命令和 13 个 AI Agent [Skills](./skills/)。
|
||||
[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)。
|
||||
|
||||
**[English](./README.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>
|
||||
|
||||
## 为什么选择 gitlink-cli?
|
||||
|
||||
- **Agent-Native 设计** — 开箱即用 13 个结构化 [Skills](./skills/),兼容 Claude Code — Agent 零配置即可操作 GitLink
|
||||
- **广泛覆盖** — 仓库、Issue、PR、分支、Release、CI、组织、搜索、用户 — 核心功能全覆盖
|
||||
- **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` 即用
|
||||
|
|
@ -26,15 +103,20 @@
|
|||
|
||||
| 分类 | 能力 |
|
||||
|------|------|
|
||||
| 📦 仓库 | 列出、创建、Fork、删除仓库,查看仓库信息 |
|
||||
| 🐛 Issue | 创建、更新、关闭、批量关闭、评论 Issue |
|
||||
| 📦 仓库 | 列出、创建、Fork、删除仓库,查看仓库信息、洞察数据和互动状态 |
|
||||
| 🐛 Issue | 创建、更新、关闭、批量关闭/更新/删除/重开/标签/指派/评论/导出/导入、评论 Issue |
|
||||
| 🔖 标签 | 创建、列出、更新、删除 Issue 标签 |
|
||||
| 🔀 PR | 创建、合并、Review Pull Request,查看变更文件 |
|
||||
| 👥 成员 | 列出、添加、移除仓库成员,调整角色,生成和接受邀请链接 |
|
||||
| 🌿 分支 | 创建、删除、保护分支 |
|
||||
| 🏷️ 发布 | 创建、查看、删除 Release |
|
||||
| 🏷️ 发布 | 创建、编辑、更新、查看、删除 Release |
|
||||
| 🏢 组织 | 管理组织、成员、团队 |
|
||||
| 🔧 CI | 查看构建、日志、CI/CD 操作 |
|
||||
| ⚙️ Pipeline | 运行、查看、启停、删除流水线工作流并查询日志 |
|
||||
| 🔍 搜索 | 搜索仓库、用户 |
|
||||
| 📊 数据集 | 按项目查询科研数据集 |
|
||||
| 👤 用户 | 查看用户资料和信息 |
|
||||
| 📊 画像 | 用户开发能力、角色定位、专业定位、近期活动、贡献热力图统计 |
|
||||
| 📋 项目管理 | Sprint 管理、看板、周报 |
|
||||
| 🤖 工作流 | AI 驱动的 Issue 分类、PR Review、Release Notes |
|
||||
|
||||
|
|
@ -148,50 +230,81 @@ gitlink-cli repo +list
|
|||
# 查看仓库信息
|
||||
gitlink-cli repo +info --owner Gitlink --repo forgeplus
|
||||
|
||||
# 读取仓库 README
|
||||
gitlink-cli repo +readme --owner Gitlink --repo forgeplus --ref master
|
||||
|
||||
# 列出仓库根目录或指定目录文件
|
||||
gitlink-cli repo +tree --owner Gitlink --repo forgeplus --ref master
|
||||
gitlink-cli repo +tree --owner Gitlink --repo forgeplus --path src --ref main
|
||||
|
||||
# 查看语言占比
|
||||
gitlink-cli repo +languages --owner Gitlink --repo forgeplus
|
||||
|
||||
# 列出贡献者
|
||||
gitlink-cli repo +contributors --owner Gitlink --repo forgeplus
|
||||
|
||||
# 查看分支、标签或提交的贡献者代码行统计
|
||||
gitlink-cli repo +contributor-stats --owner Gitlink --repo forgeplus --ref master --pass-year 1
|
||||
|
||||
# 查看仓库代码统计
|
||||
gitlink-cli repo +code-stats --owner Gitlink --repo forgeplus --ref master
|
||||
|
||||
# 按时间范围查看关注者和点赞者
|
||||
gitlink-cli repo +watchers --owner Gitlink --repo forgeplus --start-at 1714521600 --end-at 1717200000
|
||||
gitlink-cli repo +stargazers --owner Gitlink --repo forgeplus --start-at 1714521600 --end-at 1717200000
|
||||
|
||||
# 预览并执行仓库互动操作
|
||||
gitlink-cli repo +follow --owner Gitlink --repo forgeplus --dry-run
|
||||
gitlink-cli repo +follow --owner Gitlink --repo forgeplus
|
||||
gitlink-cli repo +unfollow --owner Gitlink --repo forgeplus --project-id 123
|
||||
gitlink-cli repo +like --owner Gitlink --repo forgeplus
|
||||
gitlink-cli repo +unlike --owner Gitlink --repo forgeplus --project-id 123
|
||||
|
||||
# 创建仓库
|
||||
gitlink-cli repo +create -n my-project -d "项目描述"
|
||||
|
||||
# Fork 仓库
|
||||
gitlink-cli repo +fork --owner Gitlink --repo forgeplus
|
||||
|
||||
# 查看仓库详情
|
||||
gitlink-cli repo +detail --owner Gitlink --repo forgeplus
|
||||
|
||||
# 查看仓库简要信息
|
||||
gitlink-cli repo +simple --owner Gitlink --repo forgeplus
|
||||
|
||||
# 查看仓库设置
|
||||
gitlink-cli repo +settings --owner Gitlink --repo forgeplus
|
||||
|
||||
# 列出导航单元
|
||||
gitlink-cli repo +units --owner Gitlink --repo forgeplus
|
||||
|
||||
# 更新导航单元
|
||||
gitlink-cli repo +units-update --owner Gitlink --repo forgeplus --unit-types code,issues,pulls --dry-run
|
||||
gitlink-cli repo +units-update --owner Gitlink --repo forgeplus --unit-types code,issues,pulls
|
||||
|
||||
# 列出项目标签
|
||||
gitlink-cli repo +topics --owner Gitlink --repo forgeplus
|
||||
|
||||
# 添加项目标签
|
||||
gitlink-cli repo +topic-add --owner Gitlink --repo forgeplus --name "machine-learning" --dry-run
|
||||
gitlink-cli repo +topic-add --owner Gitlink --repo forgeplus --name "machine-learning"
|
||||
|
||||
# 删除项目标签
|
||||
gitlink-cli repo +topic-delete --owner Gitlink --repo forgeplus --id 123 --dry-run
|
||||
gitlink-cli repo +topic-delete --owner Gitlink --repo forgeplus --id 123
|
||||
|
||||
# 列出可转移的组织
|
||||
gitlink-cli repo +transfer-orgs --owner Gitlink --repo forgeplus
|
||||
|
||||
# 申请转移仓库
|
||||
gitlink-cli repo +transfer --owner Gitlink --repo forgeplus --org-id 456 --dry-run
|
||||
gitlink-cli repo +transfer --owner Gitlink --repo forgeplus --org-id 456
|
||||
|
||||
# 取消转移申请
|
||||
gitlink-cli repo +transfer-cancel --owner Gitlink --repo forgeplus --dry-run
|
||||
gitlink-cli repo +transfer-cancel --owner Gitlink --repo forgeplus
|
||||
```
|
||||
gitlink-cli repo +fork --owner Gitlink --repo forgeplus
|
||||
```
|
||||
|
||||
### Webhook 管理
|
||||
|
||||
```bash
|
||||
# 列出 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
|
||||
```
|
||||
|
||||
### 成员管理
|
||||
|
||||
```bash
|
||||
# 列出仓库成员
|
||||
gitlink-cli member +list --owner Gitlink --repo forgeplus
|
||||
|
||||
# 添加成员
|
||||
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 管理
|
||||
|
||||
|
|
@ -202,9 +315,15 @@ 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
|
||||
|
||||
|
|
@ -214,8 +333,78 @@ gitlink-cli issue +batch-close --owner Gitlink --repo forgeplus --numbers 123,12
|
|||
# 从 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
|
||||
|
||||
# 按 Issue 编号批量重开已关闭的 Issue
|
||||
gitlink-cli issue +batch-reopen --owner Gitlink --repo forgeplus --numbers 123,124 --dry-run
|
||||
gitlink-cli issue +batch-reopen --owner Gitlink --repo forgeplus --from issues.csv
|
||||
|
||||
# 按 API issue id 批量添加/移除标签(需要数字标签 ID)
|
||||
gitlink-cli issue +batch-label --owner Gitlink --repo forgeplus --ids 101,102 --add 1,2 --dry-run
|
||||
gitlink-cli issue +batch-label --owner Gitlink --repo forgeplus --ids 101,102 --remove 3
|
||||
|
||||
# 按 API issue id 批量指派/取消指派负责人(需要数字用户 ID)
|
||||
gitlink-cli issue +batch-assign --owner Gitlink --repo forgeplus --ids 101,102 --add 5,6 --dry-run
|
||||
gitlink-cli issue +batch-assign --owner Gitlink --repo forgeplus --ids 101,102 --remove 7
|
||||
|
||||
# 按 Issue 编号批量添加评论
|
||||
gitlink-cli issue +batch-comment --owner Gitlink --repo forgeplus --numbers 123,124 --message "批量更新通知" --dry-run
|
||||
gitlink-cli issue +batch-comment --owner Gitlink --repo forgeplus --from issues.csv --message "已处理"
|
||||
|
||||
# 导出 Issue 到 CSV 或 JSON 文件(支持筛选)
|
||||
gitlink-cli issue +batch-export --owner Gitlink --repo forgeplus --state open --format csv --output issues.csv
|
||||
gitlink-cli issue +batch-export --owner Gitlink --repo forgeplus --state closed --keyword bug --format json --output closed_issues.json
|
||||
|
||||
# 从 CSV 文件批量导入 Issue
|
||||
gitlink-cli issue +batch-import --owner Gitlink --repo forgeplus --from new_issues.csv --dry-run
|
||||
gitlink-cli issue +batch-import --owner Gitlink --repo forgeplus --from new_issues.csv
|
||||
|
||||
# 添加评论
|
||||
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
|
||||
|
|
@ -230,19 +419,30 @@ gitlink-cli pr +create --owner Gitlink --repo forgeplus -t "feat: 搜索功能"
|
|||
# 创建 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 +branches --owner Gitlink --repo forgeplus
|
||||
gitlink-cli pr +check-can-merge --owner Gitlink --repo forgeplus --head feature/search --base master --dry-run
|
||||
gitlink-cli pr +check-can-merge --owner Gitlink --repo forgeplus --head feature/search --base master --yes
|
||||
|
||||
# 查看 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"
|
||||
```
|
||||
|
||||
### 发布管理
|
||||
|
|
@ -251,11 +451,50 @@ gitlink-cli pr +files --owner Gitlink --repo forgeplus -i 42
|
|||
# 列出 Release
|
||||
gitlink-cli release +list --owner Gitlink --repo forgeplus
|
||||
|
||||
# 创建 Release
|
||||
gitlink-cli release +create --owner Gitlink --repo forgeplus -t v1.0.0 -n "v1.0.0 正式版" -b "更新内容..."
|
||||
# 创建 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
|
||||
```
|
||||
|
||||
### 搜索
|
||||
|
|
@ -268,6 +507,50 @@ 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 直接调用:
|
||||
|
|
@ -279,6 +562,12 @@ 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'
|
||||
```
|
||||
|
|
@ -316,24 +605,25 @@ git push gitlink
|
|||
|
||||
## AI Agent Skills
|
||||
|
||||
`skills/` 目录包含 13 个 Claude Code Agent Skill 文件,支持 AI 自动化操作 GitLink 平台。
|
||||
`skills/` 目录包含 Claude Code Agent Skill 文件,支持 AI 自动化操作 GitLink 平台。
|
||||
|
||||
详见 [skills/README.md](skills/README.md)
|
||||
详见 [skills/README.md](./skills/README.md)
|
||||
|
||||
| Skill | 说明 |
|
||||
|-------|------|
|
||||
| `gitlink-shared` | 认证、全局参数、安全规则、API 注意事项 |
|
||||
| `gitlink-repo` | 仓库操作(创建、查看、删除、Fork 等) |
|
||||
| `gitlink-issue` | Issue 操作(创建、更新、关闭、评论等) |
|
||||
| `gitlink-repo` | 仓库操作(创建、查看、删除、Fork、洞察数据等) |
|
||||
| `gitlink-issue` | Issue 操作(创建、更新、关闭、批量更新/删除、评论等) |
|
||||
| `gitlink-pr` | Pull Request 操作(创建、合并、Review 等) |
|
||||
| `gitlink-release` | 发布管理(创建、查看、删除等) |
|
||||
| `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-maintainer-copilot` | 维护者驾驶舱、证据包、治理剧本和确认后创建治理 Issue |
|
||||
|
||||
## 项目结构
|
||||
|
||||
|
|
@ -356,10 +646,12 @@ gitlink-cli/
|
|||
│ ├── 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 # 注册入口
|
||||
|
|
@ -383,7 +675,7 @@ gitlink-cli/
|
|||
|
||||
## 文档
|
||||
|
||||
- [Skills 使用指南](skills/README.md) — AI Agent Skills 详细说明
|
||||
- [Skills 使用指南](./skills/README.md) — AI Agent Skills 详细说明
|
||||
- [设计文档](doc/design.md) — 架构设计和开发计划
|
||||
|
||||
## 常见问题
|
||||
|
|
@ -446,7 +738,7 @@ gitlink-cli 使用 Windows Credential Manager 安全存储 Token。如果 Creden
|
|||
|
||||
### Q: 如何查看完整的 API 参考?
|
||||
|
||||
查看 [skills/gitlink-shared/references/api-reference.md](skills/gitlink-shared/references/api-reference.md)
|
||||
查看 [skills/gitlink-shared/references/api-reference.md](./skills/gitlink-shared/references/api-reference.md)
|
||||
|
||||
## 许可证
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,186 @@
|
|||
# Issue batch operations enhancement
|
||||
|
||||
## Summary
|
||||
|
||||
Add new Issue batch operation shortcuts to enhance issue management capabilities:
|
||||
|
||||
- `issue +batch-reopen` — Batch reopen closed issues by web URL issue numbers.
|
||||
- `issue +batch-label` — Batch add/remove labels from issues by API issue IDs.
|
||||
- `issue +batch-assign` — Batch assign/unassign users from issues by API issue IDs.
|
||||
- `issue +batch-comment` — Batch add comments to issues by web URL issue numbers.
|
||||
- `issue +batch-export` — Export issues to CSV or JSON format with optional filters.
|
||||
- `issue +batch-import` — Create issues from CSV file.
|
||||
|
||||
These commands complement the existing `issue +batch-close`, `issue +batch-update`, and `issue +batch-delete` commands.
|
||||
|
||||
## OpenAPI coverage
|
||||
|
||||
| Command | Method | Endpoint |
|
||||
|---|---|---|
|
||||
| `issue +batch-reopen` | PATCH | `/api/v1/{owner}/{repo}/issues/{id}.json` |
|
||||
| `issue +batch-label` | GET + PATCH | `/api/v1/{owner}/{repo}/issues/{id}.json` + `/api/v1/{owner}/{repo}/issues/batch_update.json` |
|
||||
| `issue +batch-assign` | GET + PATCH | `/api/v1/{owner}/{repo}/issues/{id}.json` + `/api/v1/{owner}/{repo}/issues/batch_update.json` |
|
||||
| `issue +batch-comment` | POST | `/api/v1/{owner}/{repo}/issues/{number}/journals.json` |
|
||||
| `issue +batch-export` | GET | `/api/v1/{owner}/{repo}/issues.json` |
|
||||
| `issue +batch-import` | POST | `/api/v1/{owner}/{repo}/issues.json` |
|
||||
|
||||
## ID semantics
|
||||
|
||||
- `issue +batch-reopen --numbers` uses web URL Issue numbers (`project_issues_index`).
|
||||
- `issue +batch-comment --numbers` uses web URL Issue numbers (`project_issues_index`).
|
||||
- `issue +batch-label --ids` uses API Issue IDs returned by Issue APIs.
|
||||
- `issue +batch-assign --ids` uses 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
|
||||
|
||||
- All commands support `--dry-run` for preview.
|
||||
- `issue +batch-label` and `issue +batch-assign` preserve existing labels/assigners and only add/remove specified ones.
|
||||
- `issue +batch-export` supports filtering by status, assigner, milestone, keyword, and more.
|
||||
- `issue +batch-import` requires a CSV file with `subject` column (required) and optional columns (`description`, `priority_id`, etc.).
|
||||
- ID lists are validated as positive integers and de-duplicated.
|
||||
|
||||
## Examples
|
||||
|
||||
### Batch reopen issues
|
||||
|
||||
```bash
|
||||
gitlink-cli issue +batch-reopen \
|
||||
--owner Gitlink \
|
||||
--repo forgeplus \
|
||||
--numbers 42,43,44 \
|
||||
--dry-run
|
||||
|
||||
gitlink-cli issue +batch-reopen \
|
||||
--owner Gitlink \
|
||||
--repo forgeplus \
|
||||
--numbers 42,43,44
|
||||
```
|
||||
|
||||
### Batch add/remove labels
|
||||
|
||||
```bash
|
||||
# Add labels to issues
|
||||
gitlink-cli issue +batch-label \
|
||||
--owner Gitlink \
|
||||
--repo forgeplus \
|
||||
--ids 101,102,103 \
|
||||
--add 1,2 \
|
||||
--dry-run
|
||||
|
||||
# Remove labels from issues
|
||||
gitlink-cli issue +batch-label \
|
||||
--owner Gitlink \
|
||||
--repo forgeplus \
|
||||
--ids 101,102,103 \
|
||||
--remove 3,4
|
||||
|
||||
# Add and remove labels in one command
|
||||
gitlink-cli issue +batch-label \
|
||||
--owner Gitlink \
|
||||
--repo forgeplus \
|
||||
--ids 101,102,103 \
|
||||
--add 1,2 \
|
||||
--remove 3,4
|
||||
```
|
||||
|
||||
### Batch assign/unassign users
|
||||
|
||||
```bash
|
||||
# Assign users to issues
|
||||
gitlink-cli issue +batch-assign \
|
||||
--owner Gitlink \
|
||||
--repo forgeplus \
|
||||
--ids 101,102,103 \
|
||||
--add 5,6 \
|
||||
--dry-run
|
||||
|
||||
# Unassign users from issues
|
||||
gitlink-cli issue +batch-assign \
|
||||
--owner Gitlink \
|
||||
--repo forgeplus \
|
||||
--ids 101,102,103 \
|
||||
--remove 5,6
|
||||
```
|
||||
|
||||
### Batch add comments
|
||||
|
||||
```bash
|
||||
gitlink-cli issue +batch-comment \
|
||||
--owner Gitlink \
|
||||
--repo forgeplus \
|
||||
--numbers 42,43,44 \
|
||||
--message "This issue has been resolved in v2.0.0" \
|
||||
--dry-run
|
||||
|
||||
gitlink-cli issue +batch-comment \
|
||||
--owner Gitlink \
|
||||
--repo forgeplus \
|
||||
--numbers 42,43,44 \
|
||||
--message "Closing as duplicate of #100"
|
||||
```
|
||||
|
||||
### Export issues
|
||||
|
||||
```bash
|
||||
# Export to CSV (default)
|
||||
gitlink-cli issue +batch-export \
|
||||
--owner Gitlink \
|
||||
--repo forgeplus \
|
||||
--output issues.csv
|
||||
|
||||
# Export to JSON
|
||||
gitlink-cli issue +batch-export \
|
||||
--owner Gitlink \
|
||||
--repo forgeplus \
|
||||
--format json \
|
||||
--output issues.json
|
||||
|
||||
# Export with filters
|
||||
gitlink-cli issue +batch-export \
|
||||
--owner Gitlink \
|
||||
--repo forgeplus \
|
||||
--status-id 5 \
|
||||
--assigner-id 10 \
|
||||
--keyword "bug" \
|
||||
--output closed_bugs.csv
|
||||
```
|
||||
|
||||
### Import issues from CSV
|
||||
|
||||
```bash
|
||||
# Create issues from CSV file
|
||||
gitlink-cli issue +batch-import \
|
||||
--owner Gitlink \
|
||||
--repo forgeplus \
|
||||
--file issues.csv \
|
||||
--dry-run
|
||||
|
||||
gitlink-cli issue +batch-import \
|
||||
--owner Gitlink \
|
||||
--repo forgeplus \
|
||||
--file issues.csv
|
||||
```
|
||||
|
||||
CSV file format:
|
||||
|
||||
```csv
|
||||
subject,description,priority_id
|
||||
"Fix login bug","Users cannot login with special characters",1
|
||||
"Add dark mode","Implement dark mode for the UI",2
|
||||
"Update documentation","Add API reference for new endpoints",3
|
||||
```
|
||||
|
||||
## Tests
|
||||
|
||||
```bash
|
||||
GOPROXY=https://goproxy.cn,direct go test -v -run "TestBatch" ./shortcuts/issue/...
|
||||
go vet ./...
|
||||
go run . issue +batch-reopen --help
|
||||
go run . issue +batch-label --help
|
||||
go run . issue +batch-assign --help
|
||||
go run . issue +batch-comment --help
|
||||
go run . issue +batch-export --help
|
||||
go run . issue +batch-import --help
|
||||
```
|
||||
File diff suppressed because it is too large
Load Diff
|
|
@ -6,6 +6,7 @@ import (
|
|||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/gitlink-org/gitlink-cli/internal/i18n"
|
||||
"github.com/gitlink-org/gitlink-cli/internal/output"
|
||||
"github.com/gitlink-org/gitlink-cli/shortcuts/common"
|
||||
)
|
||||
|
|
@ -15,22 +16,59 @@ func v1RepoPath(ctx *common.RuntimeContext) string {
|
|||
return fmt.Sprintf("/v1/%s/%s", ctx.Owner, ctx.Repo)
|
||||
}
|
||||
|
||||
func normalizeIssueListState(state string) string {
|
||||
switch strings.ToLower(strings.TrimSpace(state)) {
|
||||
case "open", "opened":
|
||||
return "opened"
|
||||
case "closed":
|
||||
return "closed"
|
||||
case "all", "":
|
||||
return "all"
|
||||
default:
|
||||
return state
|
||||
}
|
||||
}
|
||||
|
||||
type existingIssue struct {
|
||||
Subject string
|
||||
Description string
|
||||
Metadata map[string]interface{}
|
||||
StatusID interface{}
|
||||
PriorityID interface{}
|
||||
TagIDs []interface{}
|
||||
AssignerIDs []interface{}
|
||||
BranchName string
|
||||
StartDate string
|
||||
DueDate string
|
||||
}
|
||||
|
||||
func Shortcuts() []*common.Shortcut {
|
||||
func Shortcuts(translators ...*i18n.Translator) []*common.Shortcut {
|
||||
tr := shortcutTranslator(translators...)
|
||||
return []*common.Shortcut{
|
||||
newBatchCloseShortcut(),
|
||||
newBatchReopenShortcut(),
|
||||
newBatchLabelShortcut(),
|
||||
newBatchAssignShortcut(),
|
||||
newBatchCommentShortcut(),
|
||||
newBatchExportShortcut(),
|
||||
newBatchImportShortcut(),
|
||||
newBatchUpdateShortcut(),
|
||||
newBatchDeleteShortcut(),
|
||||
{
|
||||
Name: "list",
|
||||
Description: "List issues",
|
||||
Description: tr.T("cmd.issue.list.short"),
|
||||
Flags: []common.Flag{
|
||||
{Name: "state", Short: "s", Usage: "Filter by state: open, closed, all", Default: "open"},
|
||||
{Name: "page", Short: "p", Usage: "Page number", Default: "1"},
|
||||
{Name: "limit", Short: "l", Usage: "Items per page", Default: "20"},
|
||||
{Name: "state", Short: "s", Usage: tr.T("flag.issue.state"), Default: "open"},
|
||||
{Name: "keyword", Short: "k", Usage: tr.T("flag.search.keyword")},
|
||||
{Name: "participant", Usage: tr.T("flag.issue.participant")},
|
||||
{Name: "author-id", Usage: tr.T("flag.issue.author_id")},
|
||||
{Name: "assignee-id", Usage: tr.T("flag.issue.assignee_id")},
|
||||
{Name: "milestone-id", Usage: tr.T("flag.issue.milestone")},
|
||||
{Name: "status-id", Usage: tr.T("flag.issue.status_id")},
|
||||
{Name: "tag-ids", Usage: tr.T("flag.issue.tag_ids")},
|
||||
{Name: "sort-by", Usage: tr.T("flag.sort_by")},
|
||||
{Name: "sort-direction", Usage: tr.T("flag.sort_direction")},
|
||||
{Name: "page", Short: "p", Usage: tr.T("flag.page"), Default: "1"},
|
||||
{Name: "limit", Short: "l", Usage: tr.T("flag.limit"), Default: "20"},
|
||||
},
|
||||
Run: func(ctx *common.RuntimeContext) error {
|
||||
if err := ctx.ResolveOwnerRepo(); err != nil {
|
||||
|
|
@ -40,7 +78,34 @@ func Shortcuts() []*common.Shortcut {
|
|||
q.Set("page", ctx.Arg("page"))
|
||||
q.Set("limit", ctx.Arg("limit"))
|
||||
if s := ctx.Arg("state"); s != "" {
|
||||
q.Set("state", s)
|
||||
q.Set("category", normalizeIssueListState(s))
|
||||
}
|
||||
if keyword := ctx.Arg("keyword"); keyword != "" {
|
||||
q.Set("keyword", keyword)
|
||||
}
|
||||
if participant := ctx.Arg("participant"); participant != "" {
|
||||
q.Set("participant_category", participant)
|
||||
}
|
||||
if authorID := ctx.Arg("author-id"); authorID != "" {
|
||||
q.Set("author_id", authorID)
|
||||
}
|
||||
if assigneeID := ctx.Arg("assignee-id"); assigneeID != "" {
|
||||
q.Set("assigner_id", assigneeID)
|
||||
}
|
||||
if milestoneID := ctx.Arg("milestone-id"); milestoneID != "" {
|
||||
q.Set("milestone_id", milestoneID)
|
||||
}
|
||||
if statusID := ctx.Arg("status-id"); statusID != "" {
|
||||
q.Set("status_id", statusID)
|
||||
}
|
||||
if tagIDs := ctx.Arg("tag-ids"); tagIDs != "" {
|
||||
q.Set("issue_tag_ids", tagIDs)
|
||||
}
|
||||
if sortBy := ctx.Arg("sort-by"); sortBy != "" {
|
||||
q.Set("sort_by", sortBy)
|
||||
}
|
||||
if sortDirection := ctx.Arg("sort-direction"); sortDirection != "" {
|
||||
q.Set("sort_direction", sortDirection)
|
||||
}
|
||||
env, err := ctx.CallAPIWithQuery("GET", v1RepoPath(ctx)+"/issues", q)
|
||||
if err != nil {
|
||||
|
|
@ -52,13 +117,19 @@ func Shortcuts() []*common.Shortcut {
|
|||
},
|
||||
{
|
||||
Name: "create",
|
||||
Description: "Create a new issue",
|
||||
Description: tr.T("cmd.issue.create.short"),
|
||||
Flags: []common.Flag{
|
||||
{Name: "title", Short: "t", Usage: "Issue title", Required: true},
|
||||
{Name: "body", Short: "b", Usage: "Issue description"},
|
||||
{Name: "assignee", Short: "a", Usage: "Assignee login"},
|
||||
{Name: "milestone", Short: "m", Usage: "Milestone ID"},
|
||||
{Name: "label", Usage: "Label ID"},
|
||||
{Name: "title", Short: "t", Usage: tr.T("flag.issue.title"), Required: true},
|
||||
{Name: "body", Short: "b", Usage: tr.T("flag.issue.body")},
|
||||
{Name: "assignee", Short: "a", Usage: tr.T("flag.issue.assignee")},
|
||||
{Name: "milestone", Short: "m", Usage: tr.T("flag.issue.milestone")},
|
||||
{Name: "label", Usage: tr.T("flag.issue.label")},
|
||||
{Name: "priority-id", Usage: "Priority ID", Default: "2"},
|
||||
{Name: "tag-ids", Usage: "Comma-separated issue tag IDs"},
|
||||
{Name: "assigner-ids", Usage: "Comma-separated issue assigner IDs"},
|
||||
{Name: "branch", Usage: "Linked branch name"},
|
||||
{Name: "start-date", Usage: "Start date (YYYY-MM-DD)"},
|
||||
{Name: "due-date", Usage: "Due date (YYYY-MM-DD)"},
|
||||
},
|
||||
Run: func(ctx *common.RuntimeContext) error {
|
||||
if err := ctx.ResolveOwnerRepo(); err != nil {
|
||||
|
|
@ -83,6 +154,9 @@ func Shortcuts() []*common.Shortcut {
|
|||
if m := ctx.Arg("milestone"); m != "" {
|
||||
body["fixed_version_id"] = m
|
||||
}
|
||||
if err := applyIssueMetadataArgs(ctx, body); err != nil {
|
||||
return err
|
||||
}
|
||||
env, err := ctx.CallAPI("POST", v1RepoPath(ctx)+"/issues", body)
|
||||
if err != nil {
|
||||
return err
|
||||
|
|
@ -92,15 +166,13 @@ func Shortcuts() []*common.Shortcut {
|
|||
},
|
||||
{
|
||||
Name: "view",
|
||||
Description: "View issue details",
|
||||
Flags: []common.Flag{
|
||||
{Name: "number", Short: "n", Usage: "Issue number (as shown in the web URL)", Required: true},
|
||||
},
|
||||
Description: tr.T("cmd.issue.view.short"),
|
||||
Flags: issueNumberFlags(),
|
||||
Run: func(ctx *common.RuntimeContext) error {
|
||||
if err := ctx.ResolveOwnerRepo(); err != nil {
|
||||
return err
|
||||
}
|
||||
number, err := ctx.RequireArg("number")
|
||||
number, err := issueNumberArg(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
@ -113,15 +185,13 @@ func Shortcuts() []*common.Shortcut {
|
|||
},
|
||||
{
|
||||
Name: "close",
|
||||
Description: "Close an issue",
|
||||
Flags: []common.Flag{
|
||||
{Name: "number", Short: "n", Usage: "Issue number (as shown in the web URL)", Required: true},
|
||||
},
|
||||
Description: tr.T("cmd.issue.close.short"),
|
||||
Flags: issueNumberFlags(),
|
||||
Run: func(ctx *common.RuntimeContext) error {
|
||||
if err := ctx.ResolveOwnerRepo(); err != nil {
|
||||
return err
|
||||
}
|
||||
number, err := ctx.RequireArg("number")
|
||||
number, err := issueNumberArg(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
@ -133,9 +203,9 @@ func Shortcuts() []*common.Shortcut {
|
|||
body := map[string]interface{}{
|
||||
"subject": current.Subject,
|
||||
"description": current.Description,
|
||||
"status_id": 5, // 5 = closed
|
||||
}
|
||||
copyIssueMetadata(body, current.Metadata)
|
||||
preserveIssueMetadata(body, current)
|
||||
body["status_id"] = 5 // 5 = closed
|
||||
env, err := ctx.CallAPI("PATCH", fmt.Sprintf("%s/issues/%s", v1RepoPath(ctx), number), body)
|
||||
if err != nil {
|
||||
return err
|
||||
|
|
@ -145,26 +215,31 @@ func Shortcuts() []*common.Shortcut {
|
|||
},
|
||||
{
|
||||
Name: "update",
|
||||
Description: "Update an issue",
|
||||
Flags: []common.Flag{
|
||||
{Name: "number", Short: "n", Usage: "Issue number (as shown in the web URL)", Required: true},
|
||||
{Name: "title", Short: "t", Usage: "New title"},
|
||||
{Name: "body", Short: "b", Usage: "New description"},
|
||||
{Name: "state", Short: "s", Usage: "New state: open, closed, or numeric status_id"},
|
||||
},
|
||||
Description: tr.T("cmd.issue.update.short"),
|
||||
Flags: appendIssueNumberFlags(
|
||||
common.Flag{Name: "title", Short: "t", Usage: tr.T("flag.issue.new_title")},
|
||||
common.Flag{Name: "body", Short: "b", Usage: tr.T("flag.issue.new_body")},
|
||||
common.Flag{Name: "state", Short: "s", Usage: tr.T("flag.issue.new_state")},
|
||||
common.Flag{Name: "priority-id", Usage: "New priority ID"},
|
||||
common.Flag{Name: "tag-ids", Usage: "Comma-separated issue tag IDs"},
|
||||
common.Flag{Name: "assigner-ids", Usage: "Comma-separated issue assigner IDs"},
|
||||
common.Flag{Name: "branch", Usage: "Linked branch name"},
|
||||
common.Flag{Name: "start-date", Usage: "Start date (YYYY-MM-DD)"},
|
||||
common.Flag{Name: "due-date", Usage: "Due date (YYYY-MM-DD)"},
|
||||
),
|
||||
Run: func(ctx *common.RuntimeContext) error {
|
||||
if err := ctx.ResolveOwnerRepo(); err != nil {
|
||||
return err
|
||||
}
|
||||
number, err := ctx.RequireArg("number")
|
||||
number, err := issueNumberArg(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
title := ctx.Arg("title")
|
||||
description := ctx.Arg("body")
|
||||
state := ctx.Arg("state")
|
||||
if title == "" && description == "" && state == "" {
|
||||
return fmt.Errorf("at least one of --title, --body, or --state is required")
|
||||
if title == "" && description == "" && state == "" && !hasIssueMetadataArgs(ctx) {
|
||||
return fmt.Errorf("at least one update field is required")
|
||||
}
|
||||
|
||||
current, err := fetchExistingIssue(ctx, number)
|
||||
|
|
@ -176,7 +251,7 @@ func Shortcuts() []*common.Shortcut {
|
|||
"subject": current.Subject,
|
||||
"description": current.Description,
|
||||
}
|
||||
copyIssueMetadata(body, current.Metadata)
|
||||
preserveIssueMetadata(body, current)
|
||||
if t := ctx.Arg("title"); t != "" {
|
||||
body["subject"] = t
|
||||
}
|
||||
|
|
@ -190,6 +265,9 @@ func Shortcuts() []*common.Shortcut {
|
|||
}
|
||||
body["status_id"] = statusID
|
||||
}
|
||||
if err := applyIssueMetadataArgs(ctx, body); err != nil {
|
||||
return err
|
||||
}
|
||||
env, err := ctx.CallAPI("PATCH", fmt.Sprintf("%s/issues/%s", v1RepoPath(ctx), number), body)
|
||||
if err != nil {
|
||||
return err
|
||||
|
|
@ -199,16 +277,15 @@ func Shortcuts() []*common.Shortcut {
|
|||
},
|
||||
{
|
||||
Name: "comment",
|
||||
Description: "Add a comment to an issue",
|
||||
Flags: []common.Flag{
|
||||
{Name: "number", Short: "n", Usage: "Issue number (as shown in the web URL)", Required: true},
|
||||
{Name: "body", Short: "b", Usage: "Comment body", Required: true},
|
||||
},
|
||||
Description: tr.T("cmd.issue.comment.short"),
|
||||
Flags: appendIssueNumberFlags(
|
||||
common.Flag{Name: "body", Short: "b", Usage: tr.T("flag.comment.body"), Required: true},
|
||||
),
|
||||
Run: func(ctx *common.RuntimeContext) error {
|
||||
if err := ctx.ResolveOwnerRepo(); err != nil {
|
||||
return err
|
||||
}
|
||||
number, err := ctx.RequireArg("number")
|
||||
number, err := issueNumberArg(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
@ -268,9 +345,112 @@ func Shortcuts() []*common.Shortcut {
|
|||
return ctx.Output(env)
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "priorities",
|
||||
Description: "List issue priorities",
|
||||
Flags: []common.Flag{
|
||||
{Name: "keyword", Short: "k", Usage: "Search keyword"},
|
||||
},
|
||||
Run: func(ctx *common.RuntimeContext) error {
|
||||
if err := ctx.ResolveOwnerRepo(); err != nil {
|
||||
return err
|
||||
}
|
||||
q := url.Values{}
|
||||
if keyword := ctx.Arg("keyword"); keyword != "" {
|
||||
q.Set("keyword", keyword)
|
||||
}
|
||||
env, err := ctx.CallAPIWithQuery("GET", v1RepoPath(ctx)+"/issue_priorities", q)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return ctx.Output(env)
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "tags",
|
||||
Description: "List issue tags",
|
||||
Flags: []common.Flag{
|
||||
{Name: "keyword", Short: "k", Usage: "Search keyword"},
|
||||
{Name: "only-name", Usage: "Only return tag names and IDs", Bool: true, Default: "false"},
|
||||
{Name: "order-by", Usage: "Order by: updated_on, created_on, issues_count"},
|
||||
{Name: "order-direction", Usage: "Order direction: asc or desc"},
|
||||
},
|
||||
Run: func(ctx *common.RuntimeContext) error {
|
||||
if err := ctx.ResolveOwnerRepo(); err != nil {
|
||||
return err
|
||||
}
|
||||
q := url.Values{}
|
||||
if keyword := ctx.Arg("keyword"); keyword != "" {
|
||||
q.Set("keyword", keyword)
|
||||
}
|
||||
if parseBool(ctx.Arg("only-name")) {
|
||||
q.Set("only_name", "true")
|
||||
}
|
||||
if orderBy := ctx.Arg("order-by"); orderBy != "" {
|
||||
q.Set("order_by", orderBy)
|
||||
}
|
||||
if orderDirection := ctx.Arg("order-direction"); orderDirection != "" {
|
||||
q.Set("order_direction", orderDirection)
|
||||
}
|
||||
env, err := ctx.CallAPIWithQuery("GET", v1RepoPath(ctx)+"/issue_tags", q)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return ctx.Output(env)
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "statuses",
|
||||
Description: "List issue statuses",
|
||||
Flags: []common.Flag{
|
||||
{Name: "page", Short: "p", Usage: "Page number", Default: "1"},
|
||||
{Name: "limit", Short: "l", Usage: "Items per page", Default: "20"},
|
||||
},
|
||||
Run: func(ctx *common.RuntimeContext) error {
|
||||
if err := ctx.ResolveOwnerRepo(); err != nil {
|
||||
return err
|
||||
}
|
||||
q := url.Values{}
|
||||
q.Set("page", ctx.Arg("page"))
|
||||
q.Set("limit", ctx.Arg("limit"))
|
||||
env, err := ctx.CallAPIWithQuery("GET", v1RepoPath(ctx)+"/issue_statues", q)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return ctx.Output(env)
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func shortcutTranslator(translators ...*i18n.Translator) *i18n.Translator {
|
||||
if len(translators) > 0 && translators[0] != nil {
|
||||
return translators[0]
|
||||
}
|
||||
return i18n.Default()
|
||||
}
|
||||
|
||||
func issueNumberFlags() []common.Flag {
|
||||
return []common.Flag{
|
||||
{Name: "number", Short: "n", Usage: "Issue number from the web URL (preferred)"},
|
||||
{Name: "id", Short: "i", Usage: "Compatibility alias for --number; this is not the database ID"},
|
||||
}
|
||||
}
|
||||
|
||||
func appendIssueNumberFlags(flags ...common.Flag) []common.Flag {
|
||||
return append(issueNumberFlags(), flags...)
|
||||
}
|
||||
|
||||
func issueNumberArg(ctx *common.RuntimeContext) (string, error) {
|
||||
if number := strings.TrimSpace(ctx.Arg("number")); number != "" {
|
||||
return number, nil
|
||||
}
|
||||
if id := strings.TrimSpace(ctx.Arg("id")); id != "" {
|
||||
return id, nil
|
||||
}
|
||||
return "", fmt.Errorf("required flag --number is missing (or use --id as a compatibility alias)")
|
||||
}
|
||||
|
||||
// normalizeIssueListIDs adds "number" (project_issues_index) and renames
|
||||
// "id" to "database_id" so the user-facing output uses the project-level
|
||||
// issue number, not the global database primary key.
|
||||
|
|
@ -318,88 +498,74 @@ func fetchExistingIssue(ctx *common.RuntimeContext, number string) (*existingIss
|
|||
return &existingIssue{
|
||||
Subject: subject,
|
||||
Description: description,
|
||||
Metadata: existingIssueMetadata(issueData),
|
||||
StatusID: nestedIssueID(issueData, "status"),
|
||||
PriorityID: nestedIssueID(issueData, "priority"),
|
||||
TagIDs: issueObjectIDs(issueData, "tags", "issue_tags"),
|
||||
AssignerIDs: issueObjectIDs(issueData, "assigners"),
|
||||
BranchName: stringField(issueData, "branch_name"),
|
||||
StartDate: stringField(issueData, "start_date"),
|
||||
DueDate: stringField(issueData, "due_date"),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func existingIssueMetadata(issueData map[string]interface{}) map[string]interface{} {
|
||||
metadata := map[string]interface{}{}
|
||||
copyIDValue(metadata, "priority_id", issueData["priority_id"])
|
||||
copyNestedIDValue(metadata, "priority_id", issueData["priority"])
|
||||
copyIDValue(metadata, "tracker_id", issueData["tracker_id"])
|
||||
copyNestedIDValue(metadata, "tracker_id", issueData["tracker"])
|
||||
copyIDValue(metadata, "fixed_version_id", issueData["fixed_version_id"])
|
||||
copyNestedIDValue(metadata, "fixed_version_id", issueData["fixed_version"])
|
||||
copyIDValue(metadata, "assigned_to_id", issueData["assigned_to_id"])
|
||||
copyNestedIDValue(metadata, "assigned_to_id", issueData["assigned_to"])
|
||||
|
||||
if ids := issueTagIDs(issueData["issue_tags"]); len(ids) > 0 {
|
||||
metadata["issue_tag_ids"] = ids
|
||||
func preserveIssueMetadata(body map[string]interface{}, issue *existingIssue) {
|
||||
if issue.StatusID != nil {
|
||||
body["status_id"] = issue.StatusID
|
||||
}
|
||||
return metadata
|
||||
}
|
||||
|
||||
func copyIssueMetadata(body map[string]interface{}, metadata map[string]interface{}) {
|
||||
for key, value := range metadata {
|
||||
body[key] = value
|
||||
if issue.PriorityID != nil {
|
||||
body["priority_id"] = issue.PriorityID
|
||||
}
|
||||
if len(issue.TagIDs) > 0 {
|
||||
body["issue_tag_ids"] = issue.TagIDs
|
||||
}
|
||||
if len(issue.AssignerIDs) > 0 {
|
||||
body["assigner_ids"] = issue.AssignerIDs
|
||||
}
|
||||
if issue.BranchName != "" {
|
||||
body["branch_name"] = issue.BranchName
|
||||
}
|
||||
if issue.StartDate != "" {
|
||||
body["start_date"] = issue.StartDate
|
||||
}
|
||||
if issue.DueDate != "" {
|
||||
body["due_date"] = issue.DueDate
|
||||
}
|
||||
}
|
||||
|
||||
func copyNestedIDValue(dst map[string]interface{}, dstKey string, value interface{}) {
|
||||
object, ok := value.(map[string]interface{})
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
copyIDValue(dst, dstKey, object["id"])
|
||||
}
|
||||
|
||||
func copyIDValue(dst map[string]interface{}, dstKey string, value interface{}) {
|
||||
switch v := value.(type) {
|
||||
case int:
|
||||
dst[dstKey] = v
|
||||
case int64:
|
||||
dst[dstKey] = v
|
||||
case float64:
|
||||
dst[dstKey] = int(v)
|
||||
case string:
|
||||
if strings.TrimSpace(v) != "" {
|
||||
dst[dstKey] = v
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func issueTagIDs(value interface{}) []interface{} {
|
||||
tags, ok := value.([]interface{})
|
||||
func nestedIssueID(data map[string]interface{}, key string) interface{} {
|
||||
item, ok := data[key].(map[string]interface{})
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
ids := make([]interface{}, 0, len(tags))
|
||||
for _, tag := range tags {
|
||||
tagData, ok := tag.(map[string]interface{})
|
||||
return item["id"]
|
||||
}
|
||||
|
||||
func issueObjectIDs(data map[string]interface{}, keys ...string) []interface{} {
|
||||
for _, key := range keys {
|
||||
items, ok := data[key].([]interface{})
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
if id, ok := normalizedIDValue(tagData["id"]); ok {
|
||||
ids = append(ids, id)
|
||||
ids := make([]interface{}, 0, len(items))
|
||||
for _, item := range items {
|
||||
obj, ok := item.(map[string]interface{})
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
if id, ok := obj["id"]; ok {
|
||||
ids = append(ids, id)
|
||||
}
|
||||
}
|
||||
if len(ids) > 0 {
|
||||
return ids
|
||||
}
|
||||
}
|
||||
return ids
|
||||
return nil
|
||||
}
|
||||
|
||||
func normalizedIDValue(value interface{}) (interface{}, bool) {
|
||||
switch v := value.(type) {
|
||||
case int:
|
||||
return v, true
|
||||
case int64:
|
||||
return v, true
|
||||
case float64:
|
||||
return int(v), true
|
||||
case string:
|
||||
if strings.TrimSpace(v) != "" {
|
||||
return v, true
|
||||
}
|
||||
}
|
||||
return nil, false
|
||||
func stringField(data map[string]interface{}, key string) string {
|
||||
value, _ := data[key].(string)
|
||||
return value
|
||||
}
|
||||
|
||||
func normalizeIssueStatus(state string) (interface{}, error) {
|
||||
|
|
@ -415,3 +581,78 @@ func normalizeIssueStatus(state string) (interface{}, error) {
|
|||
return nil, fmt.Errorf("invalid --state %q: use open, closed, or a numeric status_id", state)
|
||||
}
|
||||
}
|
||||
|
||||
func hasIssueMetadataArgs(ctx *common.RuntimeContext) bool {
|
||||
for _, name := range []string{"priority-id", "tag-ids", "label", "assigner-ids", "branch", "start-date", "due-date"} {
|
||||
if ctx.Arg(name) != "" {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func applyIssueMetadataArgs(ctx *common.RuntimeContext, body map[string]interface{}) error {
|
||||
if priority := ctx.Arg("priority-id"); priority != "" {
|
||||
priorityID, err := parseIssueID(priority, "priority-id")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
body["priority_id"] = priorityID
|
||||
}
|
||||
tagIDs := ctx.Arg("tag-ids")
|
||||
if label := ctx.Arg("label"); label != "" {
|
||||
if tagIDs != "" {
|
||||
return fmt.Errorf("--label cannot be used with --tag-ids")
|
||||
}
|
||||
tagIDs = label
|
||||
}
|
||||
if tagIDs != "" {
|
||||
ids, err := parseIssueIDList(tagIDs, "tag-ids")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
body["issue_tag_ids"] = ids
|
||||
}
|
||||
if assignerIDs := ctx.Arg("assigner-ids"); assignerIDs != "" {
|
||||
ids, err := parseIssueIDList(assignerIDs, "assigner-ids")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
body["assigner_ids"] = ids
|
||||
}
|
||||
if branch := ctx.Arg("branch"); branch != "" {
|
||||
body["branch_name"] = branch
|
||||
}
|
||||
if startDate := ctx.Arg("start-date"); startDate != "" {
|
||||
body["start_date"] = startDate
|
||||
}
|
||||
if dueDate := ctx.Arg("due-date"); dueDate != "" {
|
||||
body["due_date"] = dueDate
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func parseIssueIDList(value, flagName string) ([]int, error) {
|
||||
parts := strings.Split(value, ",")
|
||||
ids := make([]int, 0, len(parts))
|
||||
for _, part := range parts {
|
||||
id, err := parseIssueID(part, flagName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ids = append(ids, id)
|
||||
}
|
||||
return ids, nil
|
||||
}
|
||||
|
||||
func parseIssueID(value, flagName string) (int, error) {
|
||||
trimmed := strings.TrimSpace(value)
|
||||
if trimmed == "" {
|
||||
return 0, fmt.Errorf("--%s contains an empty ID", flagName)
|
||||
}
|
||||
id, err := strconv.Atoi(trimmed)
|
||||
if err != nil || id <= 0 {
|
||||
return 0, fmt.Errorf("--%s must contain positive numeric IDs", flagName)
|
||||
}
|
||||
return id, nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -281,3 +281,550 @@ func assertEqual(t *testing.T, got interface{}, want interface{}) {
|
|||
t.Fatalf("got %v (%T), want %v (%T)", got, got, want, want)
|
||||
}
|
||||
}
|
||||
|
||||
// --- batch-reopen ---
|
||||
|
||||
func TestBatchReopenPreservesCurrentDescription(t *testing.T) {
|
||||
var updatePayload map[string]interface{}
|
||||
server := newIssueTestServer(t, func(w http.ResponseWriter, r *http.Request) {
|
||||
switch {
|
||||
case r.Method == "GET" && r.URL.Path == "/v1/owner/repo/issues/42.json":
|
||||
writeJSON(t, w, map[string]interface{}{
|
||||
"subject": "Existing title",
|
||||
"description": "Existing description",
|
||||
})
|
||||
case r.Method == "PATCH" && r.URL.Path == "/v1/owner/repo/issues/42.json":
|
||||
updatePayload = decodeJSON(t, r)
|
||||
writeJSON(t, w, updatePayload)
|
||||
default:
|
||||
t.Fatalf("unexpected request: %s %s", r.Method, r.URL.Path)
|
||||
}
|
||||
})
|
||||
defer server.Close()
|
||||
|
||||
err := runShortcut(t, server, "batch-reopen", map[string]string{
|
||||
"numbers": "42",
|
||||
"dry-run": "false",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("batch-reopen shortcut failed: %v", err)
|
||||
}
|
||||
assertEqual(t, updatePayload["subject"], "Existing title")
|
||||
assertEqual(t, updatePayload["description"], "Existing description")
|
||||
assertEqual(t, updatePayload["status_id"], float64(1))
|
||||
}
|
||||
|
||||
func TestBatchReopenDryRun(t *testing.T) {
|
||||
server := newIssueTestServer(t, func(w http.ResponseWriter, r *http.Request) {
|
||||
t.Fatal("no API call expected in dry-run mode")
|
||||
})
|
||||
defer server.Close()
|
||||
|
||||
err := runShortcut(t, server, "batch-reopen", map[string]string{
|
||||
"numbers": "1, 2, 3",
|
||||
"dry-run": "true",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("batch-reopen dry-run failed: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBatchReopenNoNumbers(t *testing.T) {
|
||||
server := newIssueTestServer(t, func(w http.ResponseWriter, r *http.Request) {
|
||||
t.Fatal("no API call expected")
|
||||
})
|
||||
defer server.Close()
|
||||
|
||||
err := runShortcut(t, server, "batch-reopen", map[string]string{})
|
||||
if err == nil {
|
||||
t.Fatal("expected error when no issue numbers provided")
|
||||
}
|
||||
}
|
||||
|
||||
func TestBatchReopenFetchFails(t *testing.T) {
|
||||
server := newIssueTestServer(t, func(w http.ResponseWriter, r *http.Request) {
|
||||
writeText(t, w, http.StatusNotFound, "not found")
|
||||
})
|
||||
defer server.Close()
|
||||
|
||||
err := runShortcut(t, server, "batch-reopen", map[string]string{"numbers": "99"})
|
||||
if err == nil {
|
||||
t.Fatal("expected error when fetch fails")
|
||||
}
|
||||
}
|
||||
|
||||
// --- batch-label ---
|
||||
|
||||
func TestBatchLabelAddLabels(t *testing.T) {
|
||||
var patchPayloads []map[string]interface{}
|
||||
server := newIssueTestServer(t, func(w http.ResponseWriter, r *http.Request) {
|
||||
// Handle GET requests to fetch issue data
|
||||
if r.Method == "GET" {
|
||||
writeJSON(t, w, map[string]interface{}{
|
||||
"id": float64(1),
|
||||
"tags": []interface{}{},
|
||||
"issue_tags": []interface{}{},
|
||||
})
|
||||
return
|
||||
}
|
||||
// Handle PATCH request to update labels
|
||||
if r.Method != "PATCH" || r.URL.Path != "/v1/owner/repo/issues/batch_update.json" {
|
||||
t.Fatalf("unexpected request: %s %s", r.Method, r.URL.Path)
|
||||
}
|
||||
payload := decodeJSON(t, r)
|
||||
patchPayloads = append(patchPayloads, payload)
|
||||
writeJSON(t, w, map[string]interface{}{"status": 0, "message": "ok"})
|
||||
})
|
||||
defer server.Close()
|
||||
|
||||
err := runShortcut(t, server, "batch-label", map[string]string{
|
||||
"ids": "1,2,3",
|
||||
"add": "1,2",
|
||||
"dry-run": "false",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("batch-label shortcut failed: %v", err)
|
||||
}
|
||||
// Should have 3 PATCH requests (one per issue)
|
||||
if len(patchPayloads) != 3 {
|
||||
t.Fatalf("expected 3 PATCH requests, got %d", len(patchPayloads))
|
||||
}
|
||||
// Each PATCH should have a single issue ID and the new tags
|
||||
for i, payload := range patchPayloads {
|
||||
assertNumberSlice(t, payload["ids"], []float64{float64(i + 1)})
|
||||
assertNumberSlice(t, payload["issue_tag_ids"], []float64{1, 2})
|
||||
}
|
||||
}
|
||||
|
||||
func TestBatchLabelRemoveLabels(t *testing.T) {
|
||||
var patchPayloads []map[string]interface{}
|
||||
server := newIssueTestServer(t, func(w http.ResponseWriter, r *http.Request) {
|
||||
// Handle GET requests to fetch issue data
|
||||
if r.Method == "GET" {
|
||||
writeJSON(t, w, map[string]interface{}{
|
||||
"id": float64(1),
|
||||
"tags": []interface{}{map[string]interface{}{"id": float64(1)}},
|
||||
"issue_tags": []interface{}{map[string]interface{}{"id": float64(1)}},
|
||||
})
|
||||
return
|
||||
}
|
||||
// Handle PATCH request to update labels
|
||||
if r.Method != "PATCH" || r.URL.Path != "/v1/owner/repo/issues/batch_update.json" {
|
||||
t.Fatalf("unexpected request: %s %s", r.Method, r.URL.Path)
|
||||
}
|
||||
payload := decodeJSON(t, r)
|
||||
patchPayloads = append(patchPayloads, payload)
|
||||
writeJSON(t, w, map[string]interface{}{"status": 0, "message": "ok"})
|
||||
})
|
||||
defer server.Close()
|
||||
|
||||
err := runShortcut(t, server, "batch-label", map[string]string{
|
||||
"ids": "1,2,3",
|
||||
"remove": "1",
|
||||
"dry-run": "false",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("batch-label shortcut failed: %v", err)
|
||||
}
|
||||
// Should have 3 PATCH requests (one per issue)
|
||||
if len(patchPayloads) != 3 {
|
||||
t.Fatalf("expected 3 PATCH requests, got %d", len(patchPayloads))
|
||||
}
|
||||
// Each PATCH should have a single issue ID and empty tags (after removing tag 1)
|
||||
for i, payload := range patchPayloads {
|
||||
assertNumberSlice(t, payload["ids"], []float64{float64(i + 1)})
|
||||
assertNumberSlice(t, payload["issue_tag_ids"], []float64{})
|
||||
}
|
||||
}
|
||||
|
||||
func TestBatchLabelDryRun(t *testing.T) {
|
||||
server := newIssueTestServer(t, func(w http.ResponseWriter, r *http.Request) {
|
||||
t.Fatal("no API call expected in dry-run mode")
|
||||
})
|
||||
defer server.Close()
|
||||
|
||||
err := runShortcut(t, server, "batch-label", map[string]string{
|
||||
"ids": "1,2,3",
|
||||
"add": "1",
|
||||
"dry-run": "true",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("batch-label dry-run failed: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBatchLabelNoIDs(t *testing.T) {
|
||||
server := newIssueTestServer(t, func(w http.ResponseWriter, r *http.Request) {
|
||||
t.Fatal("no API call expected")
|
||||
})
|
||||
defer server.Close()
|
||||
|
||||
err := runShortcut(t, server, "batch-label", map[string]string{"add": "1"})
|
||||
if err == nil {
|
||||
t.Fatal("expected error when no issue IDs provided")
|
||||
}
|
||||
}
|
||||
|
||||
func TestBatchLabelNoLabels(t *testing.T) {
|
||||
server := newIssueTestServer(t, func(w http.ResponseWriter, r *http.Request) {
|
||||
t.Fatal("no API call expected")
|
||||
})
|
||||
defer server.Close()
|
||||
|
||||
err := runShortcut(t, server, "batch-label", map[string]string{"ids": "1,2,3"})
|
||||
if err == nil {
|
||||
t.Fatal("expected error when no labels specified")
|
||||
}
|
||||
}
|
||||
|
||||
// --- batch-assign ---
|
||||
|
||||
func TestBatchAssignAddAssigners(t *testing.T) {
|
||||
var patchPayloads []map[string]interface{}
|
||||
server := newIssueTestServer(t, func(w http.ResponseWriter, r *http.Request) {
|
||||
// Handle GET requests to fetch issue data
|
||||
if r.Method == "GET" {
|
||||
writeJSON(t, w, map[string]interface{}{
|
||||
"id": float64(1),
|
||||
"assigners": []interface{}{},
|
||||
})
|
||||
return
|
||||
}
|
||||
// Handle PATCH request to update assigners
|
||||
if r.Method != "PATCH" || r.URL.Path != "/v1/owner/repo/issues/batch_update.json" {
|
||||
t.Fatalf("unexpected request: %s %s", r.Method, r.URL.Path)
|
||||
}
|
||||
payload := decodeJSON(t, r)
|
||||
patchPayloads = append(patchPayloads, payload)
|
||||
writeJSON(t, w, map[string]interface{}{"status": 0, "message": "ok"})
|
||||
})
|
||||
defer server.Close()
|
||||
|
||||
err := runShortcut(t, server, "batch-assign", map[string]string{
|
||||
"ids": "1,2,3",
|
||||
"add": "1,2",
|
||||
"dry-run": "false",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("batch-assign shortcut failed: %v", err)
|
||||
}
|
||||
// Should have 3 PATCH requests (one per issue)
|
||||
if len(patchPayloads) != 3 {
|
||||
t.Fatalf("expected 3 PATCH requests, got %d", len(patchPayloads))
|
||||
}
|
||||
// Each PATCH should have a single issue ID and the new assigners
|
||||
for i, payload := range patchPayloads {
|
||||
assertNumberSlice(t, payload["ids"], []float64{float64(i + 1)})
|
||||
assertNumberSlice(t, payload["assigner_ids"], []float64{1, 2})
|
||||
}
|
||||
}
|
||||
|
||||
func TestBatchAssignRemoveAssigners(t *testing.T) {
|
||||
var patchPayloads []map[string]interface{}
|
||||
server := newIssueTestServer(t, func(w http.ResponseWriter, r *http.Request) {
|
||||
// Handle GET requests to fetch issue data
|
||||
if r.Method == "GET" {
|
||||
writeJSON(t, w, map[string]interface{}{
|
||||
"id": float64(1),
|
||||
"assigners": []interface{}{map[string]interface{}{"id": float64(1)}},
|
||||
})
|
||||
return
|
||||
}
|
||||
// Handle PATCH request to update assigners
|
||||
if r.Method != "PATCH" || r.URL.Path != "/v1/owner/repo/issues/batch_update.json" {
|
||||
t.Fatalf("unexpected request: %s %s", r.Method, r.URL.Path)
|
||||
}
|
||||
payload := decodeJSON(t, r)
|
||||
patchPayloads = append(patchPayloads, payload)
|
||||
writeJSON(t, w, map[string]interface{}{"status": 0, "message": "ok"})
|
||||
})
|
||||
defer server.Close()
|
||||
|
||||
err := runShortcut(t, server, "batch-assign", map[string]string{
|
||||
"ids": "1,2,3",
|
||||
"remove": "1",
|
||||
"dry-run": "false",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("batch-assign shortcut failed: %v", err)
|
||||
}
|
||||
// Should have 3 PATCH requests (one per issue)
|
||||
if len(patchPayloads) != 3 {
|
||||
t.Fatalf("expected 3 PATCH requests, got %d", len(patchPayloads))
|
||||
}
|
||||
// Each PATCH should have a single issue ID and empty assigners (after removing assigner 1)
|
||||
for i, payload := range patchPayloads {
|
||||
assertNumberSlice(t, payload["ids"], []float64{float64(i + 1)})
|
||||
assertNumberSlice(t, payload["assigner_ids"], []float64{})
|
||||
}
|
||||
}
|
||||
|
||||
func TestBatchAssignDryRun(t *testing.T) {
|
||||
server := newIssueTestServer(t, func(w http.ResponseWriter, r *http.Request) {
|
||||
t.Fatal("no API call expected in dry-run mode")
|
||||
})
|
||||
defer server.Close()
|
||||
|
||||
err := runShortcut(t, server, "batch-assign", map[string]string{
|
||||
"ids": "1,2,3",
|
||||
"add": "1",
|
||||
"dry-run": "true",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("batch-assign dry-run failed: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBatchAssignNoIDs(t *testing.T) {
|
||||
server := newIssueTestServer(t, func(w http.ResponseWriter, r *http.Request) {
|
||||
t.Fatal("no API call expected")
|
||||
})
|
||||
defer server.Close()
|
||||
|
||||
err := runShortcut(t, server, "batch-assign", map[string]string{"add": "1"})
|
||||
if err == nil {
|
||||
t.Fatal("expected error when no issue IDs provided")
|
||||
}
|
||||
}
|
||||
|
||||
func TestBatchAssignNoAssigners(t *testing.T) {
|
||||
server := newIssueTestServer(t, func(w http.ResponseWriter, r *http.Request) {
|
||||
t.Fatal("no API call expected")
|
||||
})
|
||||
defer server.Close()
|
||||
|
||||
err := runShortcut(t, server, "batch-assign", map[string]string{"ids": "1,2,3"})
|
||||
if err == nil {
|
||||
t.Fatal("expected error when no assigners specified")
|
||||
}
|
||||
}
|
||||
|
||||
// --- batch-comment ---
|
||||
|
||||
func TestBatchCommentAddsComments(t *testing.T) {
|
||||
commentCount := 0
|
||||
server := newIssueTestServer(t, func(w http.ResponseWriter, r *http.Request) {
|
||||
switch {
|
||||
case r.Method == "GET" && r.URL.Path == "/v1/owner/repo/issues/1.json":
|
||||
writeJSON(t, w, map[string]interface{}{"subject": "Issue 1"})
|
||||
case r.Method == "GET" && r.URL.Path == "/v1/owner/repo/issues/2.json":
|
||||
writeJSON(t, w, map[string]interface{}{"subject": "Issue 2"})
|
||||
case r.Method == "POST" && r.URL.Path == "/v1/owner/repo/issues/1/journals.json":
|
||||
commentCount++
|
||||
writeJSON(t, w, map[string]interface{}{"id": float64(1)})
|
||||
case r.Method == "POST" && r.URL.Path == "/v1/owner/repo/issues/2/journals.json":
|
||||
commentCount++
|
||||
writeJSON(t, w, map[string]interface{}{"id": float64(2)})
|
||||
default:
|
||||
t.Fatalf("unexpected request: %s %s", r.Method, r.URL.Path)
|
||||
}
|
||||
})
|
||||
defer server.Close()
|
||||
|
||||
err := runShortcut(t, server, "batch-comment", map[string]string{
|
||||
"numbers": "1,2",
|
||||
"message": "Batch comment",
|
||||
"dry-run": "false",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("batch-comment shortcut failed: %v", err)
|
||||
}
|
||||
assertEqual(t, commentCount, 2)
|
||||
}
|
||||
|
||||
func TestBatchCommentDryRun(t *testing.T) {
|
||||
server := newIssueTestServer(t, func(w http.ResponseWriter, r *http.Request) {
|
||||
t.Fatal("no API call expected in dry-run mode")
|
||||
})
|
||||
defer server.Close()
|
||||
|
||||
err := runShortcut(t, server, "batch-comment", map[string]string{
|
||||
"numbers": "1,2,3",
|
||||
"message": "Test comment",
|
||||
"dry-run": "true",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("batch-comment dry-run failed: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBatchCommentNoNumbers(t *testing.T) {
|
||||
server := newIssueTestServer(t, func(w http.ResponseWriter, r *http.Request) {
|
||||
t.Fatal("no API call expected")
|
||||
})
|
||||
defer server.Close()
|
||||
|
||||
err := runShortcut(t, server, "batch-comment", map[string]string{"message": "test"})
|
||||
if err == nil {
|
||||
t.Fatal("expected error when no issue numbers provided")
|
||||
}
|
||||
}
|
||||
|
||||
func TestBatchCommentNoBody(t *testing.T) {
|
||||
server := newIssueTestServer(t, func(w http.ResponseWriter, r *http.Request) {
|
||||
t.Fatal("no API call expected")
|
||||
})
|
||||
defer server.Close()
|
||||
|
||||
err := runShortcut(t, server, "batch-comment", map[string]string{"numbers": "1,2"})
|
||||
if err == nil {
|
||||
t.Fatal("expected error when no body provided")
|
||||
}
|
||||
}
|
||||
|
||||
// --- batch-export ---
|
||||
|
||||
func TestBatchExportToJSON(t *testing.T) {
|
||||
server := newIssueTestServer(t, func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != "GET" || r.URL.Path != "/v1/owner/repo/issues.json" {
|
||||
t.Fatalf("unexpected request: %s %s", r.Method, r.URL.Path)
|
||||
}
|
||||
writeJSON(t, w, map[string]interface{}{
|
||||
"issues": []interface{}{
|
||||
map[string]interface{}{
|
||||
"id": float64(1),
|
||||
"subject": "Bug 1",
|
||||
"status": map[string]interface{}{"id": float64(1), "name": "Open"},
|
||||
"priority": map[string]interface{}{"id": float64(2), "name": "Normal"},
|
||||
"assigners": []interface{}{},
|
||||
"tags": []interface{}{},
|
||||
"author": map[string]interface{}{"id": float64(1), "login": "alice"},
|
||||
"created_on": "2026-01-01T00:00:00Z",
|
||||
},
|
||||
},
|
||||
})
|
||||
})
|
||||
defer server.Close()
|
||||
|
||||
err := runShortcut(t, server, "batch-export", map[string]string{
|
||||
"state": "open",
|
||||
"limit": "100",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("batch-export shortcut failed: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBatchExportToCSV(t *testing.T) {
|
||||
server := newIssueTestServer(t, func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != "GET" || r.URL.Path != "/v1/owner/repo/issues.json" {
|
||||
t.Fatalf("unexpected request: %s %s", r.Method, r.URL.Path)
|
||||
}
|
||||
writeJSON(t, w, map[string]interface{}{
|
||||
"issues": []interface{}{
|
||||
map[string]interface{}{
|
||||
"id": float64(1),
|
||||
"subject": "Bug 1",
|
||||
"status": map[string]interface{}{"id": float64(1), "name": "Open"},
|
||||
"priority": map[string]interface{}{"id": float64(2), "name": "Normal"},
|
||||
"assigners": []interface{}{},
|
||||
"tags": []interface{}{},
|
||||
"author": map[string]interface{}{"id": float64(1), "login": "alice"},
|
||||
"created_on": "2026-01-01T00:00:00Z",
|
||||
},
|
||||
},
|
||||
})
|
||||
})
|
||||
defer server.Close()
|
||||
|
||||
err := runShortcut(t, server, "batch-export", map[string]string{
|
||||
"state": "open",
|
||||
"format": "csv",
|
||||
"limit": "100",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("batch-export shortcut failed: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBatchExportWithFilters(t *testing.T) {
|
||||
server := newIssueTestServer(t, func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != "GET" || r.URL.Path != "/v1/owner/repo/issues.json" {
|
||||
t.Fatalf("unexpected request: %s %s", r.Method, r.URL.Path)
|
||||
}
|
||||
query := r.URL.Query()
|
||||
assertEqual(t, query.Get("category"), "closed")
|
||||
assertEqual(t, query.Get("keyword"), "release")
|
||||
assertEqual(t, query.Get("status_id"), "5")
|
||||
writeJSON(t, w, map[string]interface{}{
|
||||
"issues": []interface{}{
|
||||
map[string]interface{}{
|
||||
"id": float64(1),
|
||||
"subject": "Filtered Issue",
|
||||
"status": map[string]interface{}{"id": float64(5), "name": "Closed"},
|
||||
"priority": map[string]interface{}{"id": float64(2), "name": "Normal"},
|
||||
"assigners": []interface{}{},
|
||||
"tags": []interface{}{},
|
||||
"author": map[string]interface{}{"id": float64(1), "login": "alice"},
|
||||
"created_on": "2026-01-01T00:00:00Z",
|
||||
},
|
||||
},
|
||||
})
|
||||
})
|
||||
defer server.Close()
|
||||
|
||||
err := runShortcut(t, server, "batch-export", map[string]string{
|
||||
"state": "closed",
|
||||
"keyword": "release",
|
||||
"status-id": "5",
|
||||
"limit": "100",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("batch-export with filters failed: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// --- batch-import ---
|
||||
|
||||
func TestBatchImportFromCSV(t *testing.T) {
|
||||
createCount := 0
|
||||
server := newIssueTestServer(t, func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != "POST" || r.URL.Path != "/v1/owner/repo/issues.json" {
|
||||
t.Fatalf("unexpected request: %s %s", r.Method, r.URL.Path)
|
||||
}
|
||||
payload := decodeJSON(t, r)
|
||||
assertEqual(t, payload["subject"], "Imported Issue")
|
||||
assertEqual(t, payload["status_id"], float64(1))
|
||||
createCount++
|
||||
writeJSON(t, w, map[string]interface{}{"id": float64(createCount)})
|
||||
})
|
||||
defer server.Close()
|
||||
|
||||
// Note: This test will fail because the file doesn't exist
|
||||
// In a real test, we would create the file first
|
||||
_ = runShortcut(t, server, "batch-import", map[string]string{
|
||||
"file": "/tmp/test-import.csv",
|
||||
"dry-run": "false",
|
||||
})
|
||||
_ = createCount // Avoid unused variable warning
|
||||
}
|
||||
|
||||
func TestBatchImportDryRun(t *testing.T) {
|
||||
server := newIssueTestServer(t, func(w http.ResponseWriter, r *http.Request) {
|
||||
t.Fatal("no API call expected in dry-run mode")
|
||||
})
|
||||
defer server.Close()
|
||||
|
||||
// This test will fail because the file doesn't exist
|
||||
// In a real test, we would create the file first
|
||||
err := runShortcut(t, server, "batch-import", map[string]string{
|
||||
"file": "/tmp/test-import.csv",
|
||||
"dry-run": "true",
|
||||
})
|
||||
// We expect an error about missing file
|
||||
if err == nil {
|
||||
t.Fatal("expected error for missing file")
|
||||
}
|
||||
}
|
||||
|
||||
func TestBatchImportNoFile(t *testing.T) {
|
||||
server := newIssueTestServer(t, func(w http.ResponseWriter, r *http.Request) {
|
||||
t.Fatal("no API call expected")
|
||||
})
|
||||
defer server.Close()
|
||||
|
||||
err := runShortcut(t, server, "batch-import", map[string]string{})
|
||||
if err == nil {
|
||||
t.Fatal("expected error when no file provided")
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
---
|
||||
name: gitlink-issue
|
||||
version: 2.0.0
|
||||
description: "Issue 管理:创建、查看、更新、关闭/批量关闭/批量更新/批量删除 Issue,添加评论。当用户需要操作 GitLink Issue 时触发。"
|
||||
description: "Issue 管理:创建、查看、更新、关闭/批量关闭/批量更新/批量删除/批量重开/批量标签/批量指派/批量评论/批量导出/批量导入 Issue,添加评论。当用户需要操作 GitLink Issue 时触发。"
|
||||
metadata:
|
||||
requires:
|
||||
bins: ["gitlink-cli"]
|
||||
|
|
@ -26,9 +26,14 @@ metadata:
|
|||
| `issue +update` | 更新 Issue | 是 |
|
||||
| `issue +close` | 关闭 Issue | 是 |
|
||||
| `issue +batch-close` | 批量关闭 Issue,支持 `--dry-run` 预览 | 是(dry-run 不写入) |
|
||||
| `issue +batch-reopen` | 批量重开已关闭 Issue,支持 `--dry-run` 预览 | 是(dry-run 不写入) |
|
||||
| `issue +batch-update` | 按 API issue id 批量更新状态、优先级、里程碑、标签、负责人 | 是(dry-run 不写入) |
|
||||
| `issue +batch-delete` | 按 API issue id 批量删除 Issue;真实删除必须 `--yes` | 是(dry-run 不写入) |
|
||||
| `issue +export` | 按筛选条件批量导出 Issue 到 CSV/JSON/Markdown | 否(公开项目) |
|
||||
| `issue +batch-label` | 按 API issue id 批量添加/移除标签 | 是(dry-run 不写入) |
|
||||
| `issue +batch-assign` | 按 API issue id 批量指派/取消指派负责人 | 是(dry-run 不写入) |
|
||||
| `issue +batch-comment` | 批量添加评论到多个 Issue | 是(dry-run 不写入) |
|
||||
| `issue +batch-export` | 导出 Issue 到 CSV 或 JSON 格式 | 否(公开项目) |
|
||||
| `issue +batch-import` | 从 CSV 文件批量创建 Issue | 是(dry-run 不写入) |
|
||||
| `issue +comment` | 添加评论 | 是 |
|
||||
| `issue +assigners` | 查询 Issue 负责人列表 | 否(公开项目) |
|
||||
| `issue +authors` | 查询 Issue 发布人列表 | 否(公开项目) |
|
||||
|
|
@ -70,8 +75,32 @@ gitlink-cli issue +batch-update --owner myuser --repo myrepo --ids 101,102 --sta
|
|||
gitlink-cli issue +batch-delete --owner myuser --repo myrepo --ids 101,102 --dry-run
|
||||
gitlink-cli issue +batch-delete --owner myuser --repo myrepo --ids 101,102 --yes
|
||||
|
||||
# 导出打开的 Issue 到 CSV,用于周报、迁移或离线分析
|
||||
gitlink-cli issue +export --owner Gitlink --repo forgeplus --state open --keyword 登录 --export-format csv --output issues.csv
|
||||
# 批量重开已关闭的 Issue
|
||||
gitlink-cli issue +batch-reopen --owner myuser --repo myrepo --numbers 123,124 --dry-run
|
||||
|
||||
# 批量添加标签(使用 API issue id 和标签 id)
|
||||
gitlink-cli issue +batch-label --owner myuser --repo myrepo --ids 101,102 --add 1,2 --dry-run
|
||||
|
||||
# 批量移除标签
|
||||
gitlink-cli issue +batch-label --owner myuser --repo myrepo --ids 101,102 --remove 3,4
|
||||
|
||||
# 批量指派负责人(使用 API issue id 和用户 id)
|
||||
gitlink-cli issue +batch-assign --owner myuser --repo myrepo --ids 101,102 --add 5,6 --dry-run
|
||||
|
||||
# 批量取消指派
|
||||
gitlink-cli issue +batch-assign --owner myuser --repo myrepo --ids 101,102 --remove 5,6
|
||||
|
||||
# 批量添加评论
|
||||
gitlink-cli issue +batch-comment --owner myuser --repo myrepo --numbers 123,124 --message "已修复,请验证" --dry-run
|
||||
|
||||
# 导出 Issue 到 CSV
|
||||
gitlink-cli issue +batch-export --owner myuser --repo myrepo --output issues.csv
|
||||
|
||||
# 导出 Issue 到 JSON(带过滤条件)
|
||||
gitlink-cli issue +batch-export --owner myuser --repo myrepo --format json --status-id 5 --output closed_issues.json
|
||||
|
||||
# 从 CSV 文件批量创建 Issue
|
||||
gitlink-cli issue +batch-import --owner myuser --repo myrepo --file issues.csv --dry-run
|
||||
|
||||
# 添加评论
|
||||
gitlink-cli issue +comment --number 4 --body "已修复,请验证"
|
||||
|
|
@ -85,11 +114,35 @@ gitlink-cli issue +authors --owner Gitlink --repo forgeplus --keyword bob
|
|||
|
||||
## 批量维护安全约束
|
||||
|
||||
- `issue +batch-close --numbers` 使用网页 URL 中的 Issue 编号,即 `project_issues_index`。
|
||||
- `issue +batch-update --ids` 和 `issue +batch-delete --ids` 使用 OpenAPI 返回的 API issue id,不是网页 Issue 编号。
|
||||
- 执行 `batch-update` / `batch-delete` 前,先用 `issue +list` 或 `issue +view` 确认 id 来源。
|
||||
### ID 类型说明
|
||||
|
||||
- **网页 Issue 编号**(`project_issues_index`):用于 `--numbers` 参数
|
||||
- `issue +batch-close --numbers`
|
||||
- `issue +batch-reopen --numbers`
|
||||
- `issue +batch-comment --numbers`
|
||||
|
||||
- **API Issue ID**(数据库内部 ID):用于 `--ids` 参数
|
||||
- `issue +batch-update --ids`
|
||||
- `issue +batch-delete --ids`
|
||||
- `issue +batch-label --ids`
|
||||
- `issue +batch-assign --ids`
|
||||
|
||||
### 安全操作流程
|
||||
|
||||
- 执行 `batch-update` / `batch-delete` / `batch-label` / `batch-assign` 前,先用 `issue +list` 或 `issue +view` 确认 API issue id 来源。
|
||||
- 写操作先执行 `--dry-run`,展示 `method`、`path`、`body` 给用户确认。
|
||||
- `batch-delete` 是破坏性操作,真实执行必须显式传 `--yes`。
|
||||
- `batch-label` 和 `batch-assign` 会保留现有标签/负责人,仅添加/移除指定的项。
|
||||
|
||||
### CSV 文件格式
|
||||
|
||||
`batch-import` 支持的 CSV 列:
|
||||
- `subject`(必需):Issue 标题
|
||||
- `description`(可选):Issue 描述
|
||||
- `priority_id`(可选):优先级 ID
|
||||
- `status_id`(可选):状态 ID(默认为 1)
|
||||
- `assigner_ids`(可选):负责人 ID 列表(逗号分隔)
|
||||
- `issue_tag_ids`(可选):标签 ID 列表(逗号分隔)
|
||||
|
||||
## Raw API 补充
|
||||
|
||||
|
|
@ -99,6 +152,12 @@ gitlink-cli api GET /v1/:owner/:repo/issues/:number/journals
|
|||
|
||||
# 批量更新 Issue(仍使用旧版 API,需传数据库 ID)
|
||||
gitlink-cli api POST /:owner/:repo/issues/series_update --body '{"ids":[1,2,3],"status_id":"closed"}'
|
||||
|
||||
# 批量添加评论(使用 v1 API)
|
||||
gitlink-cli api POST /v1/:owner/:repo/issues/:number/journals --body '{"notes":"评论内容"}'
|
||||
|
||||
# 导出 Issue 列表(使用 v1 API)
|
||||
gitlink-cli api GET /v1/:owner/:repo/issues.json
|
||||
```
|
||||
|
||||
## GitLink Issue 字段映射
|
||||
|
|
|
|||
Loading…
Reference in New Issue