forked from Gitlink/gitlink-cli
289 lines
8.9 KiB
Go
289 lines
8.9 KiB
Go
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"
|
||
}
|
||
}
|