fix: branch/pr/release default-branch awareness (drop hardcoded master)

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
farmyobutu5233 2026-07-05 12:35:16 +00:00
parent fcde26823b
commit 2dd580c208
7 changed files with 53 additions and 8 deletions

View File

@ -16,3 +16,10 @@
- `go test ./shortcuts/repo/`(默认 ref 断言改为「不携带 ref 参数」)
- 生产 gitlink.org.cn 实测:默认分支为 `main``master` 的仓库均正常列出根目录。
## 追加branch/pr/release 同类问题一并修复
- 新增 `RuntimeContext.DefaultBranch()`:读取仓库详情的 `default_branch`(缺失时回退 master
- `branch +create --from` / `pr +create --base` 未指定时回退到仓库默认分支(不再硬编码 master
- `release +create --target` 未指定时省略 `target_commitish`(平台自动落默认分支)
- 生产实测(默认分支为 main 的仓库):`repo +tree` / `branch +create` / `release +create` 均成功

View File

@ -37,7 +37,7 @@ func Shortcuts(translators ...*i18n.Translator) []*common.Shortcut {
Description: tr.T("cmd.branch.create.short"),
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: "from", Short: "f", Usage: tr.T("flag.branch.from")},
},
Run: func(ctx *common.RuntimeContext) error {
if err := ctx.ResolveOwnerRepo(); err != nil {
@ -46,7 +46,10 @@ func Shortcuts(translators ...*i18n.Translator) []*common.Shortcut {
name, _ := ctx.RequireArg("name")
from := ctx.Arg("from")
if from == "" {
from = "master"
var err error
if from, err = ctx.DefaultBranch(); err != nil {
return err
}
}
payload := map[string]interface{}{
"new_branch_name": name,

View File

@ -81,12 +81,23 @@ func TestBranchCreate(t *testing.T) {
}
func TestBranchCreateDefaultFrom(t *testing.T) {
// When 'from' is not set, it defaults to "master"
// 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) {
if r.URL.Path != "/v1/owner/repo/branches.json" {
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)
}
writeJSON(w, map[string]interface{}{"name": "feature-y"})
}))
defer server.Close()

View File

@ -105,6 +105,20 @@ func (ctx *RuntimeContext) OutputData(data interface{}) error {
return output.Print(output.SuccessEnvelope(data, nil), ctx.Format)
}
// DefaultBranch fetches the repository's default branch, falling back to "master".
func (ctx *RuntimeContext) DefaultBranch() (string, error) {
env, err := ctx.CallAPI("GET", ctx.RepoPath(), nil)
if err != nil {
return "", err
}
if data, ok := env.Data.(map[string]interface{}); ok {
if branch, ok := data["default_branch"].(string); ok && branch != "" {
return branch, nil
}
}
return "master", nil
}
// RepoPath returns the API path prefix for the current owner/repo.
func (ctx *RuntimeContext) RepoPath() string {
return fmt.Sprintf("/%s/%s", ctx.Owner, ctx.Repo)

View File

@ -96,7 +96,7 @@ func Shortcuts(translators ...*i18n.Translator) []*common.Shortcut {
{Name: "title", Short: "t", Usage: tr.T("flag.pr.title"), Required: true},
{Name: "body", Short: "b", Usage: tr.T("flag.pr.body")},
{Name: "head", Usage: tr.T("flag.pr.head"), Required: true},
{Name: "base", Usage: tr.T("flag.pr.base"), Default: "master"},
{Name: "base", Usage: tr.T("flag.pr.base")},
},
Run: func(ctx *common.RuntimeContext) error {
if err := ctx.ResolveOwnerRepo(); err != nil {
@ -106,7 +106,10 @@ func Shortcuts(translators ...*i18n.Translator) []*common.Shortcut {
head, _ := ctx.RequireArg("head")
base := ctx.Arg("base")
if base == "" {
base = "master"
var err error
if base, err = ctx.DefaultBranch(); err != nil {
return err
}
}
payload := map[string]interface{}{
"title": title,

View File

@ -209,6 +209,10 @@ func TestPRCreate(t *testing.T) {
func TestPRCreateNoBody(t *testing.T) {
var payload map[string]interface{}
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path == "/owner/repo.json" {
writeJSON(t, w, map[string]interface{}{"default_branch": "main"})
return
}
payload = decodeJSON(t, r)
writeJSON(t, w, map[string]interface{}{"id": float64(43), "title": "feat: nob"})
}))
@ -224,6 +228,9 @@ func TestPRCreateNoBody(t *testing.T) {
if _, ok := payload["body"]; ok {
t.Fatal("body should not be in payload when not provided")
}
if payload["base"] != "main" {
t.Fatalf("expected base to fall back to default branch main, got %v", payload["base"])
}
}
// --- view ---

View File

@ -42,7 +42,7 @@ func Shortcuts(translators ...*i18n.Translator) []*common.Shortcut {
{Name: "tag", Short: "t", Usage: tr.T("flag.release.tag"), Required: true},
{Name: "name", Short: "n", Usage: tr.T("flag.release.name"), Required: true},
{Name: "body", Short: "b", Usage: tr.T("flag.release.body")},
{Name: "target", Usage: tr.T("flag.release.target"), Default: "master"},
{Name: "target", Usage: tr.T("flag.release.target")},
{Name: "prerelease", Usage: tr.T("flag.release.prerelease"), Default: "false"},
{Name: "draft", Usage: "Mark as draft (true/false)", Default: "false"},
{Name: "attachment-ids", Usage: "Comma-separated attachment IDs"},