Merge PR #78: feat(branch): complete OpenAPI shortcuts
# Conflicts: # README.md # shortcuts/branch/branch.go # shortcuts/branch/branch_test.go # skills/gitlink-branch/SKILL.md
This commit is contained in:
commit
011a914f2b
134
README.md
134
README.md
|
|
@ -5,7 +5,7 @@
|
|||
[](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, webhooks, member collaboration, CI/CD, and AI-powered workflows, with 40+ commands and 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)**
|
||||
|
||||
|
|
@ -78,11 +78,19 @@ The official [GitLink](https://www.gitlink.org.cn) CLI tool — built for humans
|
|||
<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** — Structured [Skills](./skills/) out of the box, compatible with Claude Code, OpenClaw, and other AI platforms — Agents can operate GitLink with zero extra setup
|
||||
- **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
|
||||
|
|
@ -100,19 +108,19 @@ The official [GitLink](https://www.gitlink.org.cn) CLI tool — built for humans
|
|||
| 🔖 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, edit, update, view, delete releases, and manage release assets |
|
||||
| 🌿 Branch | List, create, delete, restore, set default, protect, unprotect branches |
|
||||
| 🏷️ 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 |
|
||||
|
||||
The `message` shortcut group adds inbox automation for listing messages, checking unread counters, marking messages as read, and deleting older notifications.
|
||||
|
||||
## Installation & Quick Start
|
||||
|
||||
### Requirements
|
||||
|
|
@ -287,22 +295,6 @@ gitlink-cli member +role --owner Gitlink --repo forgeplus --user-id 101 --role D
|
|||
gitlink-cli member +invite-link --owner Gitlink --repo forgeplus --role developer --apply true
|
||||
```
|
||||
|
||||
### Message Center Management
|
||||
|
||||
```bash
|
||||
# List the latest notifications with unread filtering
|
||||
gitlink-cli message +list --login Mengz --type notification --status unread --limit 10
|
||||
|
||||
# Show unread counters for notifications and @mentions
|
||||
gitlink-cli message +stats --login Mengz
|
||||
|
||||
# Preview marking all notifications as read
|
||||
gitlink-cli message +read --login Mengz --type notification --all --dry-run
|
||||
|
||||
# Delete selected @mention messages
|
||||
gitlink-cli message +delete --login Mengz --type atme --ids 101,102
|
||||
```
|
||||
|
||||
### Issue Management
|
||||
|
||||
```bash
|
||||
|
|
@ -414,14 +406,24 @@ gitlink-cli pr +review --owner Gitlink --repo forgeplus -i 42 --status approved
|
|||
### Branch Management
|
||||
|
||||
```bash
|
||||
# List branches
|
||||
gitlink-cli branch +list --owner Gitlink --repo forgeplus
|
||||
# List branches, including deleted branches when needed
|
||||
gitlink-cli branch +list --owner Gitlink --repo forgeplus --keyword feature
|
||||
gitlink-cli branch +list --owner Gitlink --repo forgeplus --state deleted
|
||||
|
||||
# Create a branch
|
||||
gitlink-cli branch +create --name feature/new-feature
|
||||
# List all branches without pagination
|
||||
gitlink-cli branch +all --owner Gitlink --repo forgeplus
|
||||
|
||||
# Delete a branch
|
||||
gitlink-cli branch +delete --name feature/old-feature
|
||||
# Create a branch, with dry-run preview
|
||||
gitlink-cli branch +create --owner Gitlink --repo forgeplus --name feature/new-feature --from master --dry-run
|
||||
|
||||
# Delete a branch, with dry-run preview
|
||||
gitlink-cli branch +delete --owner Gitlink --repo forgeplus --name feature/old-feature --dry-run
|
||||
|
||||
# Set default branch
|
||||
gitlink-cli branch +set-default --owner Gitlink --repo forgeplus --name develop --dry-run
|
||||
|
||||
# Restore a deleted branch
|
||||
gitlink-cli branch +restore --owner Gitlink --repo forgeplus --branch-id 7 --name feature/old-feature --dry-run
|
||||
|
||||
# Protect a branch
|
||||
gitlink-cli branch +protect --name main
|
||||
|
|
@ -442,16 +444,6 @@ gitlink-cli release +create --owner Gitlink --repo forgeplus -t v1.0.0 -n "v1.0.
|
|||
# View a release
|
||||
gitlink-cli release +view --owner Gitlink --repo forgeplus -i <version_id>
|
||||
|
||||
# List assets attached to a release
|
||||
gitlink-cli release +assets --owner Gitlink --repo forgeplus -i <version_id>
|
||||
|
||||
# Attach or detach existing asset IDs while preserving release metadata
|
||||
gitlink-cli release +attach --owner Gitlink --repo forgeplus -i <version_id> --attachment-ids 12,34 --dry-run
|
||||
gitlink-cli release +detach --owner Gitlink --repo forgeplus -i <version_id> --attachment-ids 34 --dry-run
|
||||
|
||||
# Upload a local file and attach it to the release in one step
|
||||
gitlink-cli release +upload --owner Gitlink --repo forgeplus -i <version_id> --file dist/gitlink-cli_linux_amd64.tar.gz --asset-name gitlink-cli-linux-amd64.tar.gz --description "Linux binary" --dry-run
|
||||
|
||||
# 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
|
||||
|
|
@ -495,6 +487,16 @@ gitlink-cli pipeline +disable --owner Gitlink --repo forgeplus --id 7 --workflow
|
|||
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
|
||||
|
|
@ -505,17 +507,27 @@ gitlink-cli search +repos -k "machine learning"
|
|||
gitlink-cli search +users -k "zhangsan"
|
||||
```
|
||||
|
||||
### Feedback
|
||||
### 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
|
||||
# Preview a feedback submission without calling the API
|
||||
gitlink-cli feedback +create --content "The CLI install guide needs a Windows note." --category docs --dry-run
|
||||
# Development ability scores + language breakdown
|
||||
gitlink-cli profile +ability --user zhangsan
|
||||
|
||||
# Submit longer feedback from a file and attach contact/repository context
|
||||
gitlink-cli feedback +create --from feedback.md --category cli --contact mengz@example.com --repo-ref Gitlink/gitlink-cli
|
||||
# Role positioning / major (discipline) categories
|
||||
gitlink-cli profile +role --user zhangsan
|
||||
gitlink-cli profile +major --user zhangsan
|
||||
|
||||
# Pipe feedback from another command
|
||||
Get-Content feedback.md | gitlink-cli feedback +create --stdin --category feature
|
||||
# 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
|
||||
|
|
@ -617,6 +629,32 @@ Safety:
|
|||
- `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:
|
||||
|
|
@ -673,7 +711,7 @@ git push gitlink
|
|||
|
||||
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 |
|
||||
|-------|-------------|
|
||||
|
|
@ -743,7 +781,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
|
||||
|
|
@ -808,7 +846,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/REFERENCE.md](skills/gitlink-shared/REFERENCE.md).
|
||||
See [skills/gitlink-shared/references/api-reference.md](./skills/gitlink-shared/references/api-reference.md).
|
||||
|
||||
## License
|
||||
|
||||
|
|
|
|||
|
|
@ -100,7 +100,7 @@
|
|||
| 🔖 标签 | 创建、列出、更新、删除 Issue 标签 |
|
||||
| 🔀 PR | 创建、合并、Review Pull Request,查看变更文件 |
|
||||
| 👥 成员 | 列出、添加、移除仓库成员,调整角色,生成和接受邀请链接 |
|
||||
| 🌿 分支 | 创建、删除、保护分支 |
|
||||
| 🌿 分支 | 列出、创建、删除、恢复、设置默认分支、保护分支 |
|
||||
| 🏷️ 发布 | 创建、编辑、更新、查看、删除 Release |
|
||||
| 🏢 组织 | 管理组织、成员、团队 |
|
||||
| 🔧 CI | 查看构建、日志、CI/CD 操作 |
|
||||
|
|
@ -403,6 +403,34 @@ gitlink-cli pr +review --owner Gitlink --repo forgeplus -i 42 --status approved
|
|||
gitlink-cli pr +review --owner Gitlink --repo forgeplus -i 42 --status approved -c "LGTM"
|
||||
```
|
||||
|
||||
|
||||
### 分支管理
|
||||
|
||||
```bash
|
||||
# 列出分支,支持关键字和已删除分支过滤
|
||||
gitlink-cli branch +list --owner Gitlink --repo forgeplus --keyword feature
|
||||
gitlink-cli branch +list --owner Gitlink --repo forgeplus --state deleted
|
||||
|
||||
# 列出全部分支(无分页)
|
||||
gitlink-cli branch +all --owner Gitlink --repo forgeplus
|
||||
|
||||
# 创建分支,先 dry-run 预览
|
||||
gitlink-cli branch +create --owner Gitlink --repo forgeplus --name feature/new-feature --from master --dry-run
|
||||
|
||||
# 删除分支,先 dry-run 预览
|
||||
gitlink-cli branch +delete --owner Gitlink --repo forgeplus --name feature/old-feature --dry-run
|
||||
|
||||
# 设置默认分支
|
||||
gitlink-cli branch +set-default --owner Gitlink --repo forgeplus --name develop --dry-run
|
||||
|
||||
# 恢复已删除分支
|
||||
gitlink-cli branch +restore --owner Gitlink --repo forgeplus --branch-id 7 --name feature/old-feature --dry-run
|
||||
|
||||
# 保护/取消保护分支
|
||||
gitlink-cli branch +protect --owner Gitlink --repo forgeplus --name main
|
||||
gitlink-cli branch +unprotect --owner Gitlink --repo forgeplus --name main
|
||||
```
|
||||
|
||||
### 发布管理
|
||||
|
||||
```bash
|
||||
|
|
|
|||
|
|
@ -0,0 +1,40 @@
|
|||
# Branch OpenAPI Shortcuts
|
||||
|
||||
补齐 GitLink 分支 OpenAPI 的生命周期操作,并增强现有 branch shortcut 的安全性和参数能力。
|
||||
|
||||
## 新增 / 增强命令
|
||||
|
||||
- `branch +list`:新增 `--keyword` 和 `--state all|deleted`,对齐 OpenAPI 查询参数。
|
||||
- `branch +all`:调用无分页分支列表接口。
|
||||
- `branch +create`:新增 `--dry-run`,预览创建分支请求。
|
||||
- `branch +delete`:切换到 OpenAPI 文档中的 `DELETE /api/v1/{owner}/{repo}/branches/{branch}.json`,并新增 `--dry-run`。
|
||||
- `branch +set-default`:设置仓库默认分支,支持 `--dry-run`。
|
||||
- `branch +restore`:恢复已删除分支,支持 `--dry-run`。
|
||||
|
||||
## OpenAPI 对齐
|
||||
|
||||
- `GET /api/v1/{owner}/{repo}/branches.json`
|
||||
- `POST /api/v1/{owner}/{repo}/branches.json`
|
||||
- `GET /api/v1/{owner}/{repo}/branches/all.json`
|
||||
- `DELETE /api/v1/{owner}/{repo}/branches/{branch}.json`
|
||||
- `PATCH /api/v1/{owner}/{repo}/branches/update_default_branch.json`
|
||||
- `POST /api/v1/{owner}/{repo}/branches/restore.json`
|
||||
|
||||
## 安全设计
|
||||
|
||||
- `branch +create`、`branch +delete`、`branch +set-default`、`branch +restore` 都支持 `--dry-run`。
|
||||
- `branch +delete` 会对包含 `/` 的分支名进行路径转义,避免把 `feature/foo` 误解析为多级路径。
|
||||
- `branch +restore` 校验 `--branch-id` 必须为正整数。
|
||||
- `branch +list --state` 仅允许 `all` 或 `deleted`,避免无效状态参数。
|
||||
|
||||
## 测试
|
||||
|
||||
新增单元测试覆盖:
|
||||
|
||||
- list 查询参数。
|
||||
- all endpoint。
|
||||
- create payload 与 dry-run。
|
||||
- delete v1 endpoint 与路径转义。
|
||||
- set-default query 参数。
|
||||
- restore payload。
|
||||
- 无效 state / branch-id 不触发 API。
|
||||
|
|
@ -1,12 +1,14 @@
|
|||
package branch
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"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"
|
||||
)
|
||||
|
||||
|
|
@ -17,28 +19,29 @@ func Shortcuts(translators ...*i18n.Translator) []*common.Shortcut {
|
|||
Name: "list",
|
||||
Description: tr.T("cmd.branch.list.short"),
|
||||
Flags: []common.Flag{
|
||||
{Name: "keyword", Short: "k", Usage: "Search keyword"},
|
||||
{Name: "state", Short: "s", Usage: "Branch state: all or deleted"},
|
||||
{Name: "page", Short: "p", Usage: tr.T("flag.page"), Default: "1"},
|
||||
{Name: "limit", Short: "l", Usage: tr.T("flag.limit"), Default: "20"},
|
||||
{Name: "keyword", Short: "k", Usage: "Filter branches by keyword"},
|
||||
{Name: "state", Short: "s", Usage: "Branch state: all, deleted, or empty for active branches"},
|
||||
},
|
||||
Run: func(ctx *common.RuntimeContext) error {
|
||||
if err := ctx.ResolveOwnerRepo(); err != nil {
|
||||
return err
|
||||
}
|
||||
state, err := normalizeBranchState(ctx.Arg("state"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
q := url.Values{}
|
||||
q.Set("page", ctx.Arg("page"))
|
||||
q.Set("limit", ctx.Arg("limit"))
|
||||
if keyword := strings.TrimSpace(ctx.Arg("keyword")); keyword != "" {
|
||||
q.Set("page", defaultBranchValue(ctx.Arg("page"), "1"))
|
||||
q.Set("limit", defaultBranchValue(ctx.Arg("limit"), "20"))
|
||||
if keyword := ctx.Arg("keyword"); keyword != "" {
|
||||
q.Set("keyword", keyword)
|
||||
}
|
||||
if state := strings.TrimSpace(ctx.Arg("state")); state != "" {
|
||||
if err := validateBranchState(state); err != nil {
|
||||
return err
|
||||
}
|
||||
if state != "" {
|
||||
q.Set("state", state)
|
||||
}
|
||||
env, err := ctx.CallAPIWithQuery("GET", "/v1"+ctx.RepoPath()+"/branches", q)
|
||||
env, err := ctx.CallAPIWithQuery("GET", branchPath(ctx), q)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
@ -52,11 +55,11 @@ func Shortcuts(translators ...*i18n.Translator) []*common.Shortcut {
|
|||
if err := ctx.ResolveOwnerRepo(); err != nil {
|
||||
return err
|
||||
}
|
||||
env, err := ctx.CallAPI("GET", "/v1"+ctx.RepoPath()+"/branches/all", nil)
|
||||
env, err := ctx.CallAPI("GET", branchPath(ctx)+"/all", nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return ctx.Output(env)
|
||||
return outputBranchEnvelope(ctx, env)
|
||||
},
|
||||
},
|
||||
{
|
||||
|
|
@ -65,21 +68,25 @@ func Shortcuts(translators ...*i18n.Translator) []*common.Shortcut {
|
|||
Flags: []common.Flag{
|
||||
{Name: "name", Short: "n", Usage: tr.T("flag.branch.name"), Required: true},
|
||||
{Name: "from", Short: "f", Usage: tr.T("flag.branch.from"), Default: "master"},
|
||||
{Name: "dry-run", Usage: "Preview the request body without creating the branch", Bool: true, Default: "false"},
|
||||
},
|
||||
Run: func(ctx *common.RuntimeContext) error {
|
||||
if err := ctx.ResolveOwnerRepo(); err != nil {
|
||||
return err
|
||||
}
|
||||
name, _ := ctx.RequireArg("name")
|
||||
from := ctx.Arg("from")
|
||||
if from == "" {
|
||||
from = "master"
|
||||
name, err := ctx.RequireArg("name")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
from := defaultBranchValue(ctx.Arg("from"), "master")
|
||||
payload := map[string]interface{}{
|
||||
"new_branch_name": name,
|
||||
"old_branch_name": from,
|
||||
}
|
||||
env, err := ctx.CallAPI("POST", "/v1"+ctx.RepoPath()+"/branches", payload)
|
||||
if parseBranchBool(ctx.Arg("dry-run")) {
|
||||
return ctx.OutputData(branchDryRun("create_branch", "POST", branchPath(ctx), payload, nil))
|
||||
}
|
||||
env, err := ctx.CallAPI("POST", branchPath(ctx), payload)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
@ -91,16 +98,88 @@ func Shortcuts(translators ...*i18n.Translator) []*common.Shortcut {
|
|||
Description: tr.T("cmd.branch.delete.short"),
|
||||
Flags: []common.Flag{
|
||||
{Name: "name", Short: "n", Usage: tr.T("flag.branch.name"), Required: true},
|
||||
{Name: "dry-run", Usage: "Preview the delete request without deleting the branch", Bool: true, Default: "false"},
|
||||
},
|
||||
Run: func(ctx *common.RuntimeContext) error {
|
||||
if err := ctx.ResolveOwnerRepo(); err != nil {
|
||||
return err
|
||||
}
|
||||
name, _ := ctx.RequireArg("name")
|
||||
name, err := ctx.RequireArg("name")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
path := fmt.Sprintf("%s/%s", branchPath(ctx), url.PathEscape(name))
|
||||
if parseBranchBool(ctx.Arg("dry-run")) {
|
||||
return ctx.OutputData(branchDryRun("delete_branch", "DELETE", path, nil, nil))
|
||||
}
|
||||
env, err := ctx.CallAPI("DELETE", path, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return ctx.Output(env)
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "set-default",
|
||||
Description: "Set the repository default branch",
|
||||
Flags: []common.Flag{
|
||||
{Name: "name", Short: "n", Usage: "Branch name to set as default", Required: true},
|
||||
{Name: "dry-run", Usage: "Preview the request without changing the default branch", Bool: true, Default: "false"},
|
||||
},
|
||||
Run: func(ctx *common.RuntimeContext) error {
|
||||
if err := ctx.ResolveOwnerRepo(); err != nil {
|
||||
return err
|
||||
}
|
||||
name, err := ctx.RequireArg("name")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
path := branchPath(ctx) + "/update_default_branch"
|
||||
q := url.Values{}
|
||||
q.Set("name", name)
|
||||
if parseBranchBool(ctx.Arg("dry-run")) {
|
||||
return ctx.OutputData(branchDryRun("set_default_branch", "PATCH", path, nil, q))
|
||||
}
|
||||
env, err := ctx.CallAPIWithQuery("PATCH", path, q)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return ctx.Output(env)
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "restore",
|
||||
Description: "Restore a deleted branch",
|
||||
Flags: []common.Flag{
|
||||
{Name: "branch-id", Short: "i", Usage: "Deleted branch ID", Required: true},
|
||||
{Name: "name", Short: "n", Usage: "Deleted branch name", Required: true},
|
||||
{Name: "dry-run", Usage: "Preview the request body without restoring the branch", Bool: true, Default: "false"},
|
||||
},
|
||||
Run: func(ctx *common.RuntimeContext) error {
|
||||
if err := ctx.ResolveOwnerRepo(); err != nil {
|
||||
return err
|
||||
}
|
||||
branchIDValue, err := ctx.RequireArg("branch-id")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
branchID, err := parsePositiveBranchInt(branchIDValue, "branch-id")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
name, err := ctx.RequireArg("name")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
payload := map[string]interface{}{
|
||||
"branch_id": branchID,
|
||||
"branch_name": name,
|
||||
}
|
||||
env, err := ctx.CallAPI("POST", "/v1"+ctx.RepoPath()+"/branches/delete", payload)
|
||||
path := branchPath(ctx) + "/restore"
|
||||
if parseBranchBool(ctx.Arg("dry-run")) {
|
||||
return ctx.OutputData(branchDryRun("restore_branch", "POST", path, payload, nil))
|
||||
}
|
||||
env, err := ctx.CallAPI("POST", path, payload)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
@ -146,25 +225,6 @@ func Shortcuts(translators ...*i18n.Translator) []*common.Shortcut {
|
|||
return ctx.Output(env)
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "set-default",
|
||||
Description: "Set repository default branch",
|
||||
Flags: []common.Flag{
|
||||
{Name: "name", Short: "n", Usage: tr.T("flag.branch.name"), Required: true},
|
||||
{Name: "dry-run", Usage: "Preview the request without changing the default branch", Bool: true, Default: "false"},
|
||||
},
|
||||
Run: runSetDefault,
|
||||
},
|
||||
{
|
||||
Name: "restore",
|
||||
Description: "Restore a deleted branch",
|
||||
Flags: []common.Flag{
|
||||
{Name: "id", Short: "i", Usage: "Deleted branch ID", Required: true},
|
||||
{Name: "name", Short: "n", Usage: tr.T("flag.branch.name"), Required: true},
|
||||
{Name: "dry-run", Usage: "Preview the request without restoring the branch", Bool: true, Default: "false"},
|
||||
},
|
||||
Run: runRestore,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -175,80 +235,65 @@ func shortcutTranslator(translators ...*i18n.Translator) *i18n.Translator {
|
|||
return i18n.Default()
|
||||
}
|
||||
|
||||
func runSetDefault(ctx *common.RuntimeContext) error {
|
||||
if err := ctx.ResolveOwnerRepo(); err != nil {
|
||||
return err
|
||||
}
|
||||
name, err := ctx.RequireArg("name")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
path := "/v1" + ctx.RepoPath() + "/branches/update_default_branch"
|
||||
query := url.Values{}
|
||||
query.Set("name", name)
|
||||
if ctx.Arg("dry-run") == "true" {
|
||||
return ctx.OutputData(map[string]interface{}{
|
||||
"dry_run": true,
|
||||
"action": "set_default_branch",
|
||||
"method": "PATCH",
|
||||
"path": path,
|
||||
"query": query.Encode(),
|
||||
})
|
||||
}
|
||||
env, err := ctx.CallAPIWithQuery("PATCH", path, query)
|
||||
if err != nil {
|
||||
return err
|
||||
func branchPath(ctx *common.RuntimeContext) string {
|
||||
return "/v1" + ctx.RepoPath() + "/branches"
|
||||
}
|
||||
|
||||
func outputBranchEnvelope(ctx *common.RuntimeContext, env *output.Envelope) error {
|
||||
if raw, ok := env.Data.(string); ok {
|
||||
var parsed interface{}
|
||||
if err := json.Unmarshal([]byte(raw), &parsed); err == nil {
|
||||
return ctx.OutputData(parsed)
|
||||
}
|
||||
}
|
||||
return ctx.Output(env)
|
||||
}
|
||||
|
||||
func runRestore(ctx *common.RuntimeContext) error {
|
||||
if err := ctx.ResolveOwnerRepo(); err != nil {
|
||||
return err
|
||||
func normalizeBranchState(value string) (string, error) {
|
||||
state := strings.ToLower(strings.TrimSpace(value))
|
||||
if state == "" {
|
||||
return "", nil
|
||||
}
|
||||
id, err := parsePositiveInt(ctx.Arg("id"), "id")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
name, err := ctx.RequireArg("name")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
path := "/v1" + ctx.RepoPath() + "/branches/restore"
|
||||
body := map[string]interface{}{
|
||||
"branch_id": id,
|
||||
"branch_name": name,
|
||||
}
|
||||
if ctx.Arg("dry-run") == "true" {
|
||||
return ctx.OutputData(map[string]interface{}{
|
||||
"dry_run": true,
|
||||
"action": "restore_branch",
|
||||
"method": "POST",
|
||||
"path": path,
|
||||
"body": body,
|
||||
})
|
||||
}
|
||||
env, err := ctx.CallAPI("POST", path, body)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return ctx.Output(env)
|
||||
}
|
||||
|
||||
func validateBranchState(state string) error {
|
||||
switch state {
|
||||
case "all", "deleted":
|
||||
return nil
|
||||
return state, nil
|
||||
default:
|
||||
return fmt.Errorf("invalid --state %q: use all or deleted", state)
|
||||
return "", fmt.Errorf("invalid --state %q: use all or deleted", value)
|
||||
}
|
||||
}
|
||||
|
||||
func parsePositiveInt(value, name string) (int64, error) {
|
||||
value = strings.TrimSpace(value)
|
||||
parsed, err := strconv.ParseInt(value, 10, 64)
|
||||
if err != nil || parsed <= 0 {
|
||||
return 0, fmt.Errorf("invalid --%s %q: use a positive integer", name, value)
|
||||
func parsePositiveBranchInt(value, flagName string) (int, error) {
|
||||
id, err := strconv.Atoi(strings.TrimSpace(value))
|
||||
if err != nil || id <= 0 {
|
||||
return 0, fmt.Errorf("invalid --%s %q: use a positive integer", flagName, value)
|
||||
}
|
||||
return parsed, nil
|
||||
return id, nil
|
||||
}
|
||||
|
||||
func defaultBranchValue(value, fallback string) string {
|
||||
if strings.TrimSpace(value) == "" {
|
||||
return fallback
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
func parseBranchBool(value string) bool {
|
||||
parsed, err := strconv.ParseBool(strings.TrimSpace(value))
|
||||
return err == nil && parsed
|
||||
}
|
||||
|
||||
func branchDryRun(action, method, path string, body map[string]interface{}, query url.Values) map[string]interface{} {
|
||||
data := map[string]interface{}{
|
||||
"dry_run": true,
|
||||
"action": action,
|
||||
"method": method,
|
||||
"path": path,
|
||||
}
|
||||
if body != nil {
|
||||
data["body"] = body
|
||||
}
|
||||
if len(query) > 0 {
|
||||
data["query"] = query.Encode()
|
||||
}
|
||||
return data
|
||||
}
|
||||
|
|
|
|||
|
|
@ -20,13 +20,15 @@ func runShortcut(t *testing.T, server *httptest.Server, name string, args map[st
|
|||
Format: "json",
|
||||
Args: args,
|
||||
}
|
||||
if ctx.Args == nil {
|
||||
ctx.Args = map[string]string{}
|
||||
}
|
||||
return shortcut.Run(ctx)
|
||||
}
|
||||
|
||||
func findShortcut(t *testing.T, name string) *common.Shortcut {
|
||||
t.Helper()
|
||||
shortcuts := Shortcuts()
|
||||
for _, s := range shortcuts {
|
||||
for _, s := range Shortcuts() {
|
||||
if s.Name == name {
|
||||
return s
|
||||
}
|
||||
|
|
@ -35,154 +37,205 @@ func findShortcut(t *testing.T, name string) *common.Shortcut {
|
|||
return nil
|
||||
}
|
||||
|
||||
func writeJSON(w http.ResponseWriter, v interface{}) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(v)
|
||||
func newBranchTestServer(t *testing.T, handler http.HandlerFunc) *httptest.Server {
|
||||
t.Helper()
|
||||
return httptest.NewServer(handler)
|
||||
}
|
||||
|
||||
// --- list ---
|
||||
|
||||
func TestBranchList(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.Path != "/v1/owner/repo/branches.json" {
|
||||
t.Fatalf("unexpected path: %s", r.URL.Path)
|
||||
}
|
||||
writeJSON(w, []interface{}{
|
||||
map[string]interface{}{"name": "master"},
|
||||
map[string]interface{}{"name": "develop"},
|
||||
})
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
err := runShortcut(t, server, "list", map[string]string{"page": "1", "limit": "20"})
|
||||
if err != nil {
|
||||
t.Fatalf("list failed: %v", err)
|
||||
func assertBranchRequest(t *testing.T, r *http.Request, method, path string) {
|
||||
t.Helper()
|
||||
if r.Method != method || r.URL.Path != path {
|
||||
t.Fatalf("got request %s %s, want %s %s", r.Method, r.URL.Path, method, path)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBranchListFilters(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.Path != "/v1/owner/repo/branches.json" {
|
||||
t.Fatalf("unexpected path: %s", r.URL.Path)
|
||||
}
|
||||
if got := r.URL.Query().Get("keyword"); got != "feature" {
|
||||
t.Fatalf("keyword = %q, want feature", got)
|
||||
}
|
||||
if got := r.URL.Query().Get("state"); got != "deleted" {
|
||||
t.Fatalf("state = %q, want deleted", got)
|
||||
}
|
||||
writeJSON(w, map[string]interface{}{"total_count": 0, "branches": []interface{}{}})
|
||||
}))
|
||||
func decodeBranchJSON(t *testing.T, r *http.Request) map[string]interface{} {
|
||||
t.Helper()
|
||||
var payload map[string]interface{}
|
||||
if err := json.NewDecoder(r.Body).Decode(&payload); err != nil {
|
||||
t.Fatalf("failed to decode request body: %v", err)
|
||||
}
|
||||
return payload
|
||||
}
|
||||
|
||||
func writeBranchJSON(t *testing.T, w http.ResponseWriter, payload interface{}) {
|
||||
t.Helper()
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
if err := json.NewEncoder(w).Encode(payload); err != nil {
|
||||
t.Fatalf("failed to write response: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func writeBranchText(t *testing.T, w http.ResponseWriter, code int, text string) {
|
||||
t.Helper()
|
||||
w.WriteHeader(code)
|
||||
if _, err := w.Write([]byte(text)); err != nil {
|
||||
t.Fatalf("failed to write response: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func assertBranchEqual(t *testing.T, got interface{}, want interface{}) {
|
||||
t.Helper()
|
||||
if got != want {
|
||||
t.Fatalf("got %v (%T), want %v (%T)", got, got, want, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBranchListBuildsQuery(t *testing.T) {
|
||||
server := newBranchTestServer(t, func(w http.ResponseWriter, r *http.Request) {
|
||||
assertBranchRequest(t, r, "GET", "/v1/owner/repo/branches.json")
|
||||
query := r.URL.Query()
|
||||
assertBranchEqual(t, query.Get("keyword"), "feature")
|
||||
assertBranchEqual(t, query.Get("state"), "deleted")
|
||||
assertBranchEqual(t, query.Get("page"), "2")
|
||||
assertBranchEqual(t, query.Get("limit"), "5")
|
||||
writeBranchJSON(t, w, map[string]interface{}{"total_count": 0, "branches": []interface{}{}})
|
||||
})
|
||||
defer server.Close()
|
||||
|
||||
err := runShortcut(t, server, "list", map[string]string{
|
||||
"page": "1", "limit": "20", "keyword": "feature", "state": "deleted",
|
||||
"keyword": "feature",
|
||||
"state": "deleted",
|
||||
"page": "2",
|
||||
"limit": "5",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("list with filters failed: %v", err)
|
||||
t.Fatalf("list shortcut failed: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBranchListRejectsInvalidState(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
t.Fatalf("invalid state should not call API, got %s %s", r.Method, r.URL.Path)
|
||||
}))
|
||||
func TestBranchAllUsesAllEndpoint(t *testing.T) {
|
||||
server := newBranchTestServer(t, func(w http.ResponseWriter, r *http.Request) {
|
||||
assertBranchRequest(t, r, "GET", "/v1/owner/repo/branches/all.json")
|
||||
writeBranchJSON(t, w, []map[string]interface{}{{"name": "master"}})
|
||||
})
|
||||
defer server.Close()
|
||||
|
||||
err := runShortcut(t, server, "list", map[string]string{"page": "1", "limit": "20", "state": "open"})
|
||||
if err == nil {
|
||||
t.Fatal("expected validation error")
|
||||
if err := runShortcut(t, server, "all", nil); err != nil {
|
||||
t.Fatalf("all shortcut failed: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBranchAll(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != "GET" || r.URL.Path != "/v1/owner/repo/branches/all.json" {
|
||||
t.Fatalf("unexpected request: %s %s", r.Method, r.URL.Path)
|
||||
}
|
||||
writeJSON(w, []interface{}{map[string]interface{}{"name": "master"}})
|
||||
}))
|
||||
func TestBranchCreatePayload(t *testing.T) {
|
||||
var payload map[string]interface{}
|
||||
server := newBranchTestServer(t, func(w http.ResponseWriter, r *http.Request) {
|
||||
assertBranchRequest(t, r, "POST", "/v1/owner/repo/branches.json")
|
||||
payload = decodeBranchJSON(t, r)
|
||||
writeBranchJSON(t, w, map[string]interface{}{"name": "feature/a"})
|
||||
})
|
||||
defer server.Close()
|
||||
|
||||
err := runShortcut(t, server, "all", nil)
|
||||
err := runShortcut(t, server, "create", map[string]string{
|
||||
"name": "feature/a",
|
||||
"from": "master",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("all failed: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// --- create ---
|
||||
|
||||
func TestBranchCreate(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != "POST" {
|
||||
t.Fatalf("expected POST, got %s", r.Method)
|
||||
}
|
||||
if r.URL.Path != "/v1/owner/repo/branches.json" {
|
||||
t.Fatalf("unexpected path: %s", r.URL.Path)
|
||||
}
|
||||
writeJSON(w, map[string]interface{}{"name": "feature-x"})
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
err := runShortcut(t, server, "create", map[string]string{"name": "feature-x", "from": "master"})
|
||||
if err != nil {
|
||||
t.Fatalf("create failed: %v", err)
|
||||
t.Fatalf("create shortcut failed: %v", err)
|
||||
}
|
||||
assertBranchEqual(t, payload["new_branch_name"], "feature/a")
|
||||
assertBranchEqual(t, payload["old_branch_name"], "master")
|
||||
}
|
||||
|
||||
func TestBranchCreateDefaultFrom(t *testing.T) {
|
||||
// When 'from' is not set, it falls back to the repository default branch.
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
switch r.URL.Path {
|
||||
case "/owner/repo.json":
|
||||
writeJSON(w, map[string]interface{}{"default_branch": "main"})
|
||||
case "/v1/owner/repo/branches.json":
|
||||
var payload map[string]interface{}
|
||||
if err := json.NewDecoder(r.Body).Decode(&payload); err != nil {
|
||||
t.Fatalf("decode payload: %v", err)
|
||||
}
|
||||
if payload["old_branch_name"] != "main" {
|
||||
t.Fatalf("expected old_branch_name to be default branch main, got %v", payload["old_branch_name"])
|
||||
}
|
||||
writeJSON(w, map[string]interface{}{"name": "feature-y"})
|
||||
default:
|
||||
t.Fatalf("unexpected path: %s", r.URL.Path)
|
||||
}
|
||||
}))
|
||||
var payload map[string]interface{}
|
||||
server := newBranchTestServer(t, func(w http.ResponseWriter, r *http.Request) {
|
||||
assertBranchRequest(t, r, "POST", "/v1/owner/repo/branches.json")
|
||||
payload = decodeBranchJSON(t, r)
|
||||
writeBranchJSON(t, w, map[string]interface{}{"name": "feature-y"})
|
||||
})
|
||||
defer server.Close()
|
||||
|
||||
err := runShortcut(t, server, "create", map[string]string{"name": "feature-y"})
|
||||
if err != nil {
|
||||
t.Fatalf("create failed: %v", err)
|
||||
}
|
||||
assertBranchEqual(t, payload["old_branch_name"], "master")
|
||||
}
|
||||
|
||||
// --- delete ---
|
||||
|
||||
func TestBranchDelete(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.Path != "/v1/owner/repo/branches/delete.json" {
|
||||
t.Fatalf("unexpected path: %s", r.URL.Path)
|
||||
}
|
||||
writeJSON(w, map[string]interface{}{"message": "deleted"})
|
||||
}))
|
||||
func TestBranchCreateDryRunDoesNotCallAPI(t *testing.T) {
|
||||
server := newBranchTestServer(t, func(w http.ResponseWriter, r *http.Request) {
|
||||
t.Fatalf("dry-run should not call API, got: %s %s", r.Method, r.URL.Path)
|
||||
})
|
||||
defer server.Close()
|
||||
|
||||
err := runShortcut(t, server, "delete", map[string]string{"name": "old-branch"})
|
||||
err := runShortcut(t, server, "create", map[string]string{
|
||||
"name": "feature/a",
|
||||
"from": "master",
|
||||
"dry-run": "true",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("delete failed: %v", err)
|
||||
t.Fatalf("create dry-run failed: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// --- protect ---
|
||||
func TestBranchDeleteUsesV1EndpointAndEscapesName(t *testing.T) {
|
||||
server := newBranchTestServer(t, func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != "DELETE" || r.URL.EscapedPath() != "/v1/owner/repo/branches/feature%2Fold.json" {
|
||||
t.Fatalf("got request %s %s, want DELETE /v1/owner/repo/branches/feature%%2Fold.json", r.Method, r.URL.EscapedPath())
|
||||
}
|
||||
writeBranchJSON(t, w, map[string]interface{}{"status": 0, "message": "success"})
|
||||
})
|
||||
defer server.Close()
|
||||
|
||||
err := runShortcut(t, server, "delete", map[string]string{"name": "feature/old"})
|
||||
if err != nil {
|
||||
t.Fatalf("delete shortcut failed: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBranchSetDefaultUsesQueryName(t *testing.T) {
|
||||
server := newBranchTestServer(t, func(w http.ResponseWriter, r *http.Request) {
|
||||
assertBranchRequest(t, r, "PATCH", "/v1/owner/repo/branches/update_default_branch.json")
|
||||
assertBranchEqual(t, r.URL.Query().Get("name"), "develop")
|
||||
writeBranchJSON(t, w, map[string]interface{}{"status": 0, "message": "success"})
|
||||
})
|
||||
defer server.Close()
|
||||
|
||||
err := runShortcut(t, server, "set-default", map[string]string{"name": "develop"})
|
||||
if err != nil {
|
||||
t.Fatalf("set-default shortcut failed: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBranchRestorePayload(t *testing.T) {
|
||||
var payload map[string]interface{}
|
||||
server := newBranchTestServer(t, func(w http.ResponseWriter, r *http.Request) {
|
||||
assertBranchRequest(t, r, "POST", "/v1/owner/repo/branches/restore.json")
|
||||
payload = decodeBranchJSON(t, r)
|
||||
writeBranchJSON(t, w, map[string]interface{}{"status": 0, "message": "success"})
|
||||
})
|
||||
defer server.Close()
|
||||
|
||||
err := runShortcut(t, server, "restore", map[string]string{
|
||||
"branch-id": "7",
|
||||
"name": "feature/deleted",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("restore shortcut failed: %v", err)
|
||||
}
|
||||
assertBranchEqual(t, payload["branch_id"], float64(7))
|
||||
assertBranchEqual(t, payload["branch_name"], "feature/deleted")
|
||||
}
|
||||
|
||||
func TestBranchRejectsInvalidInputs(t *testing.T) {
|
||||
server := newBranchTestServer(t, func(w http.ResponseWriter, r *http.Request) {
|
||||
t.Fatalf("server should not be called for invalid input: %s %s", r.Method, r.URL.Path)
|
||||
})
|
||||
defer server.Close()
|
||||
|
||||
if err := runShortcut(t, server, "list", map[string]string{"state": "open"}); err == nil {
|
||||
t.Fatal("expected invalid state to fail")
|
||||
}
|
||||
if err := runShortcut(t, server, "restore", map[string]string{"branch-id": "abc", "name": "deleted"}); err == nil {
|
||||
t.Fatal("expected invalid branch-id to fail")
|
||||
}
|
||||
}
|
||||
|
||||
func TestBranchProtect(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.Path != "/owner/repo/protected_branches.json" {
|
||||
t.Fatalf("unexpected path: %s", r.URL.Path)
|
||||
}
|
||||
writeJSON(w, map[string]interface{}{"message": "protected"})
|
||||
}))
|
||||
server := newBranchTestServer(t, func(w http.ResponseWriter, r *http.Request) {
|
||||
assertBranchRequest(t, r, "POST", "/owner/repo/protected_branches.json")
|
||||
writeBranchJSON(t, w, map[string]interface{}{"message": "protected"})
|
||||
})
|
||||
defer server.Close()
|
||||
|
||||
err := runShortcut(t, server, "protect", map[string]string{"name": "master"})
|
||||
|
|
@ -191,18 +244,11 @@ func TestBranchProtect(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
// --- unprotect ---
|
||||
|
||||
func TestBranchUnprotect(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != "DELETE" {
|
||||
t.Fatalf("expected DELETE, got %s", r.Method)
|
||||
}
|
||||
if r.URL.Path != "/owner/repo/protected_branches/master.json" {
|
||||
t.Fatalf("unexpected path: %s", r.URL.Path)
|
||||
}
|
||||
writeJSON(w, map[string]interface{}{"message": "unprotected"})
|
||||
}))
|
||||
server := newBranchTestServer(t, func(w http.ResponseWriter, r *http.Request) {
|
||||
assertBranchRequest(t, r, "DELETE", "/owner/repo/protected_branches/master.json")
|
||||
writeBranchJSON(t, w, map[string]interface{}{"message": "unprotected"})
|
||||
})
|
||||
defer server.Close()
|
||||
|
||||
err := runShortcut(t, server, "unprotect", map[string]string{"name": "master"})
|
||||
|
|
@ -211,76 +257,10 @@ func TestBranchUnprotect(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestBranchSetDefault(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != "PATCH" || r.URL.Path != "/v1/owner/repo/branches/update_default_branch.json" {
|
||||
t.Fatalf("unexpected request: %s %s", r.Method, r.URL.Path)
|
||||
}
|
||||
if got := r.URL.Query().Get("name"); got != "main" {
|
||||
t.Fatalf("name query = %q, want main", got)
|
||||
}
|
||||
writeJSON(w, map[string]interface{}{"status": 0, "message": "success"})
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
if err := runShortcut(t, server, "set-default", map[string]string{"name": "main"}); err != nil {
|
||||
t.Fatalf("set-default failed: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBranchSetDefaultDryRunDoesNotCallAPI(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
t.Fatalf("dry-run should not call API, got %s %s", r.Method, r.URL.Path)
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
if err := runShortcut(t, server, "set-default", map[string]string{"name": "main", "dry-run": "true"}); err != nil {
|
||||
t.Fatalf("set-default dry-run failed: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBranchRestore(t *testing.T) {
|
||||
var payload map[string]interface{}
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != "POST" || r.URL.Path != "/v1/owner/repo/branches/restore.json" {
|
||||
t.Fatalf("unexpected request: %s %s", r.Method, r.URL.Path)
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&payload); err != nil {
|
||||
t.Fatalf("decode body: %v", err)
|
||||
}
|
||||
writeJSON(w, map[string]interface{}{"status": 0, "message": "success"})
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
if err := runShortcut(t, server, "restore", map[string]string{"id": "7", "name": "feature/deleted"}); err != nil {
|
||||
t.Fatalf("restore failed: %v", err)
|
||||
}
|
||||
if payload["branch_id"] != float64(7) {
|
||||
t.Fatalf("branch_id = %v, want 7", payload["branch_id"])
|
||||
}
|
||||
if payload["branch_name"] != "feature/deleted" {
|
||||
t.Fatalf("branch_name = %v, want feature/deleted", payload["branch_name"])
|
||||
}
|
||||
}
|
||||
|
||||
func TestBranchRestoreValidation(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
t.Fatalf("invalid restore should not call API, got %s %s", r.Method, r.URL.Path)
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
if err := runShortcut(t, server, "restore", map[string]string{"id": "0", "name": "feature/deleted"}); err == nil {
|
||||
t.Fatal("expected invalid id error")
|
||||
}
|
||||
}
|
||||
|
||||
// --- HTTP error paths ---
|
||||
|
||||
func TestBranchListHTTPError(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
w.Write([]byte("server error"))
|
||||
}))
|
||||
server := newBranchTestServer(t, func(w http.ResponseWriter, r *http.Request) {
|
||||
writeBranchText(t, w, http.StatusInternalServerError, "server error")
|
||||
})
|
||||
defer server.Close()
|
||||
|
||||
err := runShortcut(t, server, "list", map[string]string{"page": "1", "limit": "20"})
|
||||
|
|
@ -290,10 +270,9 @@ func TestBranchListHTTPError(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestBranchCreateHTTPError(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
w.Write([]byte("server error"))
|
||||
}))
|
||||
server := newBranchTestServer(t, func(w http.ResponseWriter, r *http.Request) {
|
||||
writeBranchText(t, w, http.StatusInternalServerError, "server error")
|
||||
})
|
||||
defer server.Close()
|
||||
|
||||
err := runShortcut(t, server, "create", map[string]string{"name": "feature-x"})
|
||||
|
|
@ -303,10 +282,9 @@ func TestBranchCreateHTTPError(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestBranchDeleteHTTPError(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
w.Write([]byte("server error"))
|
||||
}))
|
||||
server := newBranchTestServer(t, func(w http.ResponseWriter, r *http.Request) {
|
||||
writeBranchText(t, w, http.StatusInternalServerError, "server error")
|
||||
})
|
||||
defer server.Close()
|
||||
|
||||
err := runShortcut(t, server, "delete", map[string]string{"name": "old-branch"})
|
||||
|
|
@ -316,10 +294,9 @@ func TestBranchDeleteHTTPError(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestBranchProtectHTTPError(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
w.Write([]byte("server error"))
|
||||
}))
|
||||
server := newBranchTestServer(t, func(w http.ResponseWriter, r *http.Request) {
|
||||
writeBranchText(t, w, http.StatusInternalServerError, "server error")
|
||||
})
|
||||
defer server.Close()
|
||||
|
||||
err := runShortcut(t, server, "protect", map[string]string{"name": "master"})
|
||||
|
|
@ -329,10 +306,9 @@ func TestBranchProtectHTTPError(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestBranchUnprotectHTTPError(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
w.Write([]byte("server error"))
|
||||
}))
|
||||
server := newBranchTestServer(t, func(w http.ResponseWriter, r *http.Request) {
|
||||
writeBranchText(t, w, http.StatusInternalServerError, "server error")
|
||||
})
|
||||
defer server.Close()
|
||||
|
||||
err := runShortcut(t, server, "unprotect", map[string]string{"name": "master"})
|
||||
|
|
@ -340,38 +316,3 @@ func TestBranchUnprotectHTTPError(t *testing.T) {
|
|||
t.Fatal("expected error for HTTP 500")
|
||||
}
|
||||
}
|
||||
|
||||
func TestBranchAllUsesAllEndpoint(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != "GET" || r.URL.Path != "/v1/owner/repo/branches/all.json" {
|
||||
t.Fatalf("unexpected request: %s %s", r.Method, r.URL.Path)
|
||||
}
|
||||
writeJSON(w, []interface{}{map[string]interface{}{"name": "master"}})
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
if err := runShortcut(t, server, "all", nil); err != nil {
|
||||
t.Fatalf("all failed: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBranchSetDefaultPatchesName(t *testing.T) {
|
||||
var payload map[string]interface{}
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != "PATCH" || r.URL.Path != "/v1/owner/repo/branches/update_default_branch.json" {
|
||||
t.Fatalf("unexpected request: %s %s", r.Method, r.URL.Path)
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&payload); err != nil {
|
||||
t.Fatalf("decode payload: %v", err)
|
||||
}
|
||||
writeJSON(w, map[string]interface{}{"status": float64(0), "message": "success"})
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
if err := runShortcut(t, server, "set-default", map[string]string{"name": "develop"}); err != nil {
|
||||
t.Fatalf("set-default failed: %v", err)
|
||||
}
|
||||
if payload["name"] != "develop" {
|
||||
t.Fatalf("expected name=develop, got %v", payload["name"])
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -131,7 +131,7 @@ skills/
|
|||
| **gitlink-issue** | Issue 管理 | `issue +create`, `issue +list`, `issue +view`, `issue +close`, `issue +batch-close`, `issue +batch-update`, `issue +batch-delete` |
|
||||
| **gitlink-pr** | Pull Request | `pr +list`, `pr +create`, `pr +view`, `pr +merge`, `pr +versions`, `pr +version-diff`, `pr +reviews`, `pr +review` |
|
||||
| **gitlink-member** | 仓库成员管理 | `member +list`, `member +add`, `member +batch-add`, `member +role`, `member +invite-link` |
|
||||
| **gitlink-branch** | 分支管理 | `branch +list`, `branch +create`, `branch +delete`, `branch +protect` |
|
||||
| **gitlink-branch** | 分支管理 | `branch +list`, `branch +all`, `branch +create`, `branch +delete`, `branch +set-default`, `branch +restore` |
|
||||
| **gitlink-release** | 版本发布 | `release +list`, `release +create`, `release +edit`, `release +update`, `release +view` |
|
||||
|
||||
### 辅助 Skills
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
---
|
||||
name: gitlink-branch
|
||||
version: 1.1.0
|
||||
description: "分支管理:创建、查看、过滤、删除、保护、设置默认分支、恢复已删除分支。当用户需要操作 GitLink 分支时触发。"
|
||||
version: 1.0.0
|
||||
description: "分支管理:列出、创建、删除、恢复、设置默认分支、保护分支。当用户需要操作 GitLink 分支时触发。"
|
||||
metadata:
|
||||
requires:
|
||||
bins: ["gitlink-cli"]
|
||||
|
|
@ -20,14 +20,14 @@ metadata:
|
|||
|
||||
| Shortcut | 说明 | 操作类型 |
|
||||
|----------|------|----------|
|
||||
| `branch +list` | 列出仓库的所有分支 | Read |
|
||||
| `branch +all` | 无分页列出仓库所有分支 | Read |
|
||||
| `branch +create` | 创建新分支 | ⚠️ Write Operation |
|
||||
| `branch +delete` | 删除分支 | 🔴 Destructive Operation |
|
||||
| `branch +list` | 分页列出分支,支持 keyword/state 过滤 | Read |
|
||||
| `branch +all` | 列出全部分支(无分页) | Read |
|
||||
| `branch +create` | 创建新分支,支持 dry-run | ⚠️ Write Operation |
|
||||
| `branch +delete` | 删除分支,支持 dry-run | 🔴 Destructive Operation |
|
||||
| `branch +set-default` | 设置默认分支,支持 dry-run | ⚠️ Write Operation |
|
||||
| `branch +restore` | 恢复已删除分支,支持 dry-run | ⚠️ Write Operation |
|
||||
| `branch +protect` | 设置分支保护规则 | ⚠️ Write Operation |
|
||||
| `branch +unprotect` | 移除分支保护规则 | ⚠️ Write Operation |
|
||||
| `branch +set-default` | 设置默认分支 | ⚠️ Write Operation |
|
||||
| `branch +restore` | 恢复已删除分支 | ⚠️ Write Operation |
|
||||
|
||||
## 参数参考
|
||||
|
||||
|
|
@ -37,19 +37,10 @@ metadata:
|
|||
|------|------|------|
|
||||
| `--owner` | 是* | 仓库所有者(可从 git remote 自动推断) |
|
||||
| `--repo` | 是* | 仓库名称(可从 git remote 自动推断) |
|
||||
| `--keyword, -k` | 否 | 搜索关键词 |
|
||||
| `--state, -s` | 否 | 分支状态:`all` 或 `deleted` |
|
||||
| `--page, -p` | 否 | 页码(默认 `1`) |
|
||||
| `--limit, -l` | 否 | 每页条数(默认 `20`) |
|
||||
| `--keyword, -k` | 否 | 分支关键词过滤 |
|
||||
| `--state, -s` | 否 | 分支状态:`all` 或 `deleted` |
|
||||
| `--format` | 否 | 输出格式:`json`/`table`/`yaml` |
|
||||
| `--debug` | 否 | 启用调试输出 |
|
||||
|
||||
### branch +all
|
||||
|
||||
| 参数 | 必填 | 说明 |
|
||||
|------|------|------|
|
||||
| `--owner` | 是* | 仓库所有者(可从 git remote 自动推断) |
|
||||
| `--repo` | 是* | 仓库名称(可从 git remote 自动推断) |
|
||||
| `--format` | 否 | 输出格式:`json`/`table`/`yaml` |
|
||||
| `--debug` | 否 | 启用调试输出 |
|
||||
|
||||
|
|
@ -59,6 +50,7 @@ metadata:
|
|||
|------|------|------|
|
||||
| `--name, -n` | 是 | 新分支名称 |
|
||||
| `--from, -f` | 否 | 源分支或 commit(默认 `master`) |
|
||||
| `--dry-run` | 否 | 只预览请求,不创建分支 |
|
||||
| `--owner` | 是* | 仓库所有者(可从 git remote 自动推断) |
|
||||
| `--repo` | 是* | 仓库名称(可从 git remote 自动推断) |
|
||||
| `--format` | 否 | 输出格式:`json`/`table`/`yaml` |
|
||||
|
|
@ -69,11 +61,32 @@ metadata:
|
|||
| 参数 | 必填 | 说明 |
|
||||
|------|------|------|
|
||||
| `--name, -n` | 是 | 要删除的分支名称 |
|
||||
| `--dry-run` | 否 | 只预览请求,不删除分支 |
|
||||
| `--owner` | 是* | 仓库所有者(可从 git remote 自动推断) |
|
||||
| `--repo` | 是* | 仓库名称(可从 git remote 自动推断) |
|
||||
| `--format` | 否 | 输出格式:`json`/`table`/`yaml` |
|
||||
| `--debug` | 否 | 启用调试输出 |
|
||||
|
||||
|
||||
### branch +set-default
|
||||
|
||||
| 参数 | 必填 | 说明 |
|
||||
|------|------|------|
|
||||
| `--name, -n` | 是 | 要设置为默认分支的名称 |
|
||||
| `--dry-run` | 否 | 只预览请求,不修改默认分支 |
|
||||
| `--owner` | 是* | 仓库所有者(可从 git remote 自动推断) |
|
||||
| `--repo` | 是* | 仓库名称(可从 git remote 自动推断) |
|
||||
|
||||
### branch +restore
|
||||
|
||||
| 参数 | 必填 | 说明 |
|
||||
|------|------|------|
|
||||
| `--branch-id, -i` | 是 | 已删除分支的 branch_id,可从 `branch +list --state deleted` 获取 |
|
||||
| `--name, -n` | 是 | 已删除分支名称 |
|
||||
| `--dry-run` | 否 | 只预览请求,不恢复分支 |
|
||||
| `--owner` | 是* | 仓库所有者(可从 git remote 自动推断) |
|
||||
| `--repo` | 是* | 仓库名称(可从 git remote 自动推断) |
|
||||
|
||||
### branch +protect
|
||||
|
||||
| 参数 | 必填 | 说明 |
|
||||
|
|
@ -94,29 +107,6 @@ metadata:
|
|||
| `--format` | 否 | 输出格式:`json`/`table`/`yaml` |
|
||||
| `--debug` | 否 | 启用调试输出 |
|
||||
|
||||
### branch +set-default
|
||||
|
||||
| 参数 | 必填 | 说明 |
|
||||
|------|------|------|
|
||||
| `--name, -n` | 是 | 要设为默认分支的名称 |
|
||||
| `--dry-run` | 否 | 预览请求,不修改默认分支 |
|
||||
| `--owner` | 是* | 仓库所有者(可从 git remote 自动推断) |
|
||||
| `--repo` | 是* | 仓库名称(可从 git remote 自动推断) |
|
||||
| `--format` | 否 | 输出格式:`json`/`table`/`yaml` |
|
||||
| `--debug` | 否 | 启用调试输出 |
|
||||
|
||||
### branch +restore
|
||||
|
||||
| 参数 | 必填 | 说明 |
|
||||
|------|------|------|
|
||||
| `--id, -i` | 是 | 已删除分支的 branch_id |
|
||||
| `--name, -n` | 是 | 要恢复的分支名称 |
|
||||
| `--dry-run` | 否 | 预览请求,不恢复分支 |
|
||||
| `--owner` | 是* | 仓库所有者(可从 git remote 自动推断) |
|
||||
| `--repo` | 是* | 仓库名称(可从 git remote 自动推断) |
|
||||
| `--format` | 否 | 输出格式:`json`/`table`/`yaml` |
|
||||
| `--debug` | 否 | 启用调试输出 |
|
||||
|
||||
> *如果在 GitLink 仓库目录下执行,`--owner` 和 `--repo` 可自动推断。
|
||||
|
||||
## 使用示例
|
||||
|
|
@ -125,20 +115,18 @@ metadata:
|
|||
# 列出当前仓库的分支
|
||||
gitlink-cli branch +list
|
||||
|
||||
# 指定仓库并分页
|
||||
# 指定仓库并分页 / 过滤
|
||||
gitlink-cli branch +list --owner Gitlink --repo forgeplus --page 1 --limit 10
|
||||
gitlink-cli branch +list --owner Gitlink --repo forgeplus --keyword feature --state all
|
||||
|
||||
# 搜索分支或查看已删除分支
|
||||
gitlink-cli branch +list --owner Gitlink --repo forgeplus --keyword fix --state deleted
|
||||
|
||||
# 无分页列出所有分支
|
||||
# 列出全部分支(无分页)
|
||||
gitlink-cli branch +all --owner Gitlink --repo forgeplus
|
||||
|
||||
# 输出为 JSON
|
||||
gitlink-cli branch +list --format json
|
||||
|
||||
# 从 master 创建分支
|
||||
gitlink-cli branch +create --name feature/new-feature
|
||||
# 从 master 创建分支(先 dry-run)
|
||||
gitlink-cli branch +create --name feature/new-feature --dry-run
|
||||
|
||||
# 从指定分支创建
|
||||
gitlink-cli branch +create --name hotfix/bug-123 --from develop
|
||||
|
|
@ -146,12 +134,19 @@ gitlink-cli branch +create --name hotfix/bug-123 --from develop
|
|||
# 指定仓库创建分支
|
||||
gitlink-cli branch +create --name feature/x --owner someone --repo myrepo
|
||||
|
||||
# 删除分支
|
||||
gitlink-cli branch +delete --name feature/old-feature
|
||||
# 删除分支(先 dry-run)
|
||||
gitlink-cli branch +delete --name feature/old-feature --dry-run
|
||||
|
||||
# 指定仓库删除分支
|
||||
gitlink-cli branch +delete --name feature/old-feature --owner someone --repo myrepo
|
||||
|
||||
# 设置默认分支(先 dry-run)
|
||||
gitlink-cli branch +set-default --name develop --dry-run
|
||||
|
||||
# 查看已删除分支并恢复(先 dry-run)
|
||||
gitlink-cli branch +list --state deleted
|
||||
gitlink-cli branch +restore --branch-id 7 --name feature/old-feature --dry-run
|
||||
|
||||
# 保护分支
|
||||
gitlink-cli branch +protect --name main
|
||||
|
||||
|
|
@ -160,14 +155,6 @@ gitlink-cli branch +protect --name main --owner someone --repo myrepo
|
|||
|
||||
# 移除分支保护(仅简单分支名,含 / 的路径需通过 Web 操作)
|
||||
gitlink-cli branch +unprotect --name main
|
||||
|
||||
# 设置默认分支(先 dry-run 预览)
|
||||
gitlink-cli branch +set-default --name main --dry-run
|
||||
gitlink-cli branch +set-default --name main
|
||||
|
||||
# 恢复已删除分支(先 dry-run 预览)
|
||||
gitlink-cli branch +restore --id 7 --name feature/old --dry-run
|
||||
gitlink-cli branch +restore --id 7 --name feature/old
|
||||
```
|
||||
|
||||
## Workflow 注意事项
|
||||
|
|
@ -178,8 +165,9 @@ gitlink-cli branch +restore --id 7 --name feature/old
|
|||
> This is a **Write Operation** — confirm user intent.
|
||||
|
||||
1. 确认用户希望创建的分支名称和源分支。
|
||||
2. 执行 `branch +create --name <name> --from <source>`。
|
||||
3. 输出创建结果。
|
||||
2. 先执行 `branch +create --name <name> --from <source> --dry-run`。
|
||||
3. 用户确认后去掉 `--dry-run` 执行。
|
||||
4. 输出创建结果。
|
||||
|
||||
### branch +delete(Destructive Operation)
|
||||
|
||||
|
|
@ -187,8 +175,30 @@ gitlink-cli branch +restore --id 7 --name feature/old
|
|||
> This is a **Destructive Operation** — confirm user intent.
|
||||
|
||||
1. 确认用户确实希望删除该分支(此操作不可逆)。
|
||||
2. 执行 `branch +delete --name <name>`。
|
||||
3. 输出删除结果。
|
||||
2. 先执行 `branch +delete --name <name> --dry-run`。
|
||||
3. 用户确认后去掉 `--dry-run` 执行。
|
||||
4. 输出删除结果。
|
||||
|
||||
|
||||
### branch +set-default(Write Operation)
|
||||
|
||||
> [!CAUTION]
|
||||
> This is a **Write Operation** — confirm user intent.
|
||||
|
||||
1. 确认用户希望设置的默认分支。
|
||||
2. 先执行 `branch +set-default --name <name> --dry-run`。
|
||||
3. 用户确认后去掉 `--dry-run` 执行。
|
||||
4. 输出设置结果。
|
||||
|
||||
### branch +restore(Write Operation)
|
||||
|
||||
> [!CAUTION]
|
||||
> This is a **Write Operation** — confirm user intent.
|
||||
|
||||
1. 先用 `branch +list --state deleted` 找到 `branch_id` 和分支名。
|
||||
2. 执行 `branch +restore --branch-id <id> --name <name> --dry-run`。
|
||||
3. 用户确认后去掉 `--dry-run` 执行。
|
||||
4. 输出恢复结果。
|
||||
|
||||
### branch +protect(Write Operation)
|
||||
|
||||
|
|
@ -208,25 +218,7 @@ gitlink-cli branch +restore --id 7 --name feature/old
|
|||
2. 执行 `branch +unprotect --name <name>`。
|
||||
3. 输出结果。
|
||||
|
||||
### branch +set-default(Write Operation)
|
||||
|
||||
> [!CAUTION]
|
||||
> This is a **Write Operation** — confirm user intent.
|
||||
|
||||
1. 确认用户希望切换默认分支。
|
||||
2. 先执行 `branch +set-default --name <name> --dry-run` 预览。
|
||||
3. 用户确认后执行不带 `--dry-run` 的命令。
|
||||
|
||||
### branch +restore(Write Operation)
|
||||
|
||||
> [!CAUTION]
|
||||
> This is a **Write Operation** — confirm user intent.
|
||||
|
||||
1. 通过 `branch +list --state deleted` 确认 `branch_id` 和分支名。
|
||||
2. 先执行 `branch +restore --id <branch_id> --name <name> --dry-run` 预览。
|
||||
3. 用户确认后执行不带 `--dry-run` 的命令。
|
||||
|
||||
> **注意:** 含 `/` 的分支名(如 `feature/my-branch`)可能无法通过 CLI 解除保护(受限于 API 路由),需通过 Web 页面操作。
|
||||
> **注意:** `branch +delete` 使用 v1 OpenAPI 路径并会对含 `/` 的分支名做路径转义;`branch +unprotect` 仍使用保护分支接口。
|
||||
|
||||
## References
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue