gitlink-cli/shortcuts/release/release.go

289 lines
8.9 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package release
import (
"fmt"
"net/url"
"github.com/gitlink-org/gitlink-cli/internal/output"
"github.com/gitlink-org/gitlink-cli/shortcuts/common"
)
func Shortcuts() []*common.Shortcut {
return []*common.Shortcut{
// +list列出仓库的所有发行版
{
Name: "list",
Description: "列出仓库的所有发行版",
Long: `List all releases in the current repository.
Shows release tag, name, prerelease status, and creation date.
Use --page and --limit for pagination.
Use --prerelease-only to show only pre-release versions.
Use --latest to show only the most recent release.`,
Example: ` # List releases
gitlink release +list
# Show 50 releases per page
gitlink release +list --limit 50
# Show only pre-release versions
gitlink release +list --prerelease-only
# Show only the latest release
gitlink release +list --latest`,
Flags: []common.Flag{
{Name: "page", Short: "p", Usage: "Page number", Default: "1"},
{Name: "limit", Short: "l", Usage: "Items per page", Default: "20"},
{Name: "prerelease-only", Usage: "Show only pre-release versions", Bool: true, Default: "false"},
{Name: "latest", Usage: "Show only the latest release", Bool: true, Default: "false"},
},
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", ctx.RepoPath()+"/releases", q)
if err != nil {
return err
}
// [P2 参数补全] API 不支持"只看预发布版本"的过滤参数,改为客户端过滤
// 拿到全量数据后遍历,只保留 prerelease=true 的条目
if ctx.Arg("prerelease-only") == "true" {
filterReleasesByPrerelease(env)
}
// [P2 参数补全] --latest: 拿到列表后只保留第一条(最新)
if ctx.Arg("latest") == "true" {
keepOnlyLatestRelease(env)
}
return ctx.Output(env)
},
},
// +create创建发行版
{
Name: "create",
Description: "创建发行版",
Long: `Create a new release for the current repository.
A release is associated with a Git tag. If the tag does not exist,
it will be created on the target branch. Use --body for release notes
and --prerelease to mark it as a pre-release version.`,
Example: ` # Create a release
gitlink release +create --tag v1.0.0 --name "Version 1.0.0"
# Create a prerelease with notes targeting a specific branch
gitlink release +create --tag v2.0.0-beta --name "v2.0 Beta" \
--body "Beta release for testing" --target develop --prerelease true`,
Flags: []common.Flag{
{Name: "tag", Short: "t", Usage: "Tag name", Required: true},
{Name: "name", Short: "n", Usage: "Release name", Required: true},
{Name: "body", Short: "b", Usage: "Release notes"},
{Name: "target", Usage: "Target branch (default: repository default branch)"},
{Name: "prerelease", Usage: "Mark as prerelease (true/false)", Default: "false"},
},
Run: func(ctx *common.RuntimeContext) error {
if err := ctx.ResolveOwnerRepo(); err != nil {
return err
}
tag, _ := ctx.RequireArg("tag")
name, _ := ctx.RequireArg("name")
payload := map[string]interface{}{
"tag_name": tag,
"name": name,
}
if b := ctx.Arg("body"); b != "" {
payload["body"] = b
}
// [P1 Bug#2] 原代码: payload["target_commitish"] = "master"(硬编码)
// 修复: 用户未指定 --target 时,使用 GetDefaultBranch() 动态查询
if t := ctx.Arg("target"); t != "" {
payload["target_commitish"] = t // 用户显式指定了目标分支
} else {
payload["target_commitish"] = common.GetDefaultBranch(ctx) // 动态查询默认分支
}
if ctx.Arg("prerelease") == "true" {
payload["prerelease"] = true
}
env, err := ctx.CallAPI("POST", ctx.RepoPath()+"/releases", payload)
if err != nil {
return err
}
return ctx.Output(env)
},
},
// +view查看发行版的详细信息
{
Name: "view",
Description: "查看发行版的详细信息",
Long: `Show detailed information about a specific release.
Provide the release ID or tag name to view its details.`,
Example: ` # View a release by ID
gitlink release +view --id 42
# View a release by tag
gitlink release +view --id v1.0.0`,
Flags: []common.Flag{
{Name: "id", Short: "i", Usage: "Release ID or tag", Required: true},
},
Run: func(ctx *common.RuntimeContext) error {
if err := ctx.ResolveOwnerRepo(); err != nil {
return err
}
id, _ := ctx.RequireArg("id")
env, err := ctx.CallAPI("GET", fmt.Sprintf("%s/releases/%s", ctx.RepoPath(), id), nil)
if err != nil {
return err
}
return ctx.Output(env)
},
},
// +delete删除发行版
{
Name: "delete",
Description: "删除发行版",
Long: `Delete a release from the current repository.
This action is irreversible. A confirmation prompt is shown before deletion.`,
Example: ` # Delete a release
gitlink release +delete --id 42`,
Flags: []common.Flag{
{Name: "id", Short: "i", Usage: "Release ID", Required: true},
},
Run: func(ctx *common.RuntimeContext) error {
if err := ctx.ResolveOwnerRepo(); err != nil {
return err
}
id, _ := ctx.RequireArg("id")
if err := common.ConfirmAction(
fmt.Sprintf("delete release #%s", id),
); err != nil {
return err
}
_, delErr := ctx.CallAPI("DELETE", fmt.Sprintf("%s/releases/%s", ctx.RepoPath(), id), nil)
if delErr != nil {
// GitLink API bug: delete succeeds but returns error status.
// Verify by checking if the release still exists.
_, viewErr := ctx.CallAPI("GET", fmt.Sprintf("%s/releases/%s", ctx.RepoPath(), id), nil)
if viewErr != nil {
// Release no longer exists — delete actually succeeded
return ctx.Output(output.SuccessEnvelope(map[string]interface{}{
"message": "Release deleted successfully",
}, nil))
}
// Release still exists — delete truly failed
return delErr
}
return ctx.Output(output.SuccessEnvelope(map[string]interface{}{
"message": "Release deleted successfully",
}, nil))
},
},
// +edit获取发行版编辑页详情
{
Name: "edit",
Description: "获取发行版编辑页详情",
Flags: []common.Flag{
{Name: "id", Short: "i", Usage: "Release ID", Required: true},
},
Run: func(ctx *common.RuntimeContext) error {
if err := ctx.ResolveOwnerRepo(); err != nil {
return err
}
id, _ := ctx.RequireArg("id")
env, err := ctx.CallAPI("GET", fmt.Sprintf("/%s/%s/releases/%s/edit", ctx.Owner, ctx.Repo, id), nil)
if err != nil {
return err
}
return ctx.Output(env)
},
},
// +update更新发行版
{
Name: "update",
Description: "更新发行版",
Flags: []common.Flag{
{Name: "id", Short: "i", Usage: "Release ID", Required: true},
{Name: "name", Short: "n", Usage: "New release name"},
{Name: "body", Short: "b", Usage: "New release body"},
{Name: "prerelease", Usage: "Mark as prerelease (true/false)"},
},
Run: func(ctx *common.RuntimeContext) error {
if err := ctx.ResolveOwnerRepo(); err != nil {
return err
}
id, _ := ctx.RequireArg("id")
payload := map[string]interface{}{}
if n := ctx.Arg("name"); n != "" {
payload["name"] = n
}
if b := ctx.Arg("body"); b != "" {
payload["body"] = b
}
if p := ctx.Arg("prerelease"); p != "" {
payload["prerelease"] = p == "true"
}
if len(payload) == 0 {
return fmt.Errorf("at least one of --name, --body, or --prerelease is required")
}
env, err := ctx.CallAPI("PUT", fmt.Sprintf("/%s/%s/releases/%s", ctx.Owner, ctx.Repo, id), payload)
if err != nil {
return err
}
return ctx.Output(env)
},
},
}
}
// filterReleasesByPrerelease removes non-prerelease entries from the envelope data.
func filterReleasesByPrerelease(env *output.Envelope) {
data, ok := env.Data.(map[string]interface{})
if !ok {
return
}
releases, ok := data["releases"].([]interface{})
if !ok {
return
}
filtered := make([]interface{}, 0, len(releases))
for _, r := range releases {
m, ok := r.(map[string]interface{})
if !ok {
continue
}
if isPrerelease(m) {
filtered = append(filtered, m)
}
}
data["releases"] = filtered
}
// keepOnlyLatestRelease keeps only the first (most recent) release.
func keepOnlyLatestRelease(env *output.Envelope) {
data, ok := env.Data.(map[string]interface{})
if !ok {
return
}
releases, ok := data["releases"].([]interface{})
if !ok || len(releases) == 0 {
return
}
data["releases"] = []interface{}{releases[0]}
}
// isPrerelease checks whether a release map has prerelease=true.
func isPrerelease(m map[string]interface{}) bool {
v, ok := m["prerelease"]
if !ok {
return false
}
switch val := v.(type) {
case bool:
return val
default:
return fmt.Sprint(val) == "true"
}
}