Merge pull request 'fix(repo): +tree 未指定 --ref 时遵循仓库默认分支(修复 main 默认分支仓库 404)' (#337) from Taoyouce/gitlink-cli:fix/repo-tree-default-branch into master

This commit is contained in:
wbtiger 2026-07-14 22:24:10 +08:00
commit dd3783d998
9 changed files with 79 additions and 16 deletions

View File

@ -0,0 +1,25 @@
# repo +tree 遵循仓库默认分支
## 背景
`repo +tree` 未指定 `--ref` 时把 ref 硬编码为 `master`,对默认分支是
`main`(或其他名称)的仓库直接返回 `[-2] 你访问的文件不存在`
(生产实测 `datawhalechina/paper-chart-tutorial`,默认分支 `main`)。
平台 `sub_entries` API 在不带 `ref` 参数时会自动使用仓库默认分支。
## 变更
- `--ref` 未指定时不再发送 `ref` 参数交由平台落到默认分支flag 也不再
声明 `master` 默认值。
## 验证
- `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

@ -56,7 +56,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 {
@ -65,7 +65,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

@ -98,7 +98,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 {
@ -108,7 +108,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

@ -253,6 +253,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"})
}))
@ -268,6 +272,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"},

View File

@ -135,21 +135,19 @@ func Shortcuts(translators ...*i18n.Translator) []*common.Shortcut {
Description: tr.T("cmd.repo.tree.short"),
Flags: []common.Flag{
{Name: "path", Short: "p", Usage: tr.T("flag.repo.tree.path")},
{Name: "ref", Short: "r", Usage: tr.T("flag.repo.tree.ref"), Default: "master"},
{Name: "ref", Short: "r", Usage: tr.T("flag.repo.tree.ref")},
},
Run: func(ctx *common.RuntimeContext) error {
if err := ctx.ResolveOwnerRepo(); err != nil {
return err
}
q := url.Values{}
ref := ctx.Arg("ref")
if ref == "" {
ref = "master"
}
if path := ctx.Arg("path"); path != "" {
q.Set("filepath", path)
}
q.Set("ref", ref)
if ref := ctx.Arg("ref"); ref != "" {
q.Set("ref", ref)
}
env, err := ctx.CallAPIWithQuery("GET", ctx.RepoPath()+"/sub_entries", q)
if err != nil {
return err

View File

@ -161,7 +161,9 @@ func TestRepoTreeListsRootOnDefaultRef(t *testing.T) {
if _, ok := r.URL.Query()["filepath"]; ok {
t.Fatalf("did not expect filepath query for repository root, got %q", r.URL.Query().Get("filepath"))
}
assertEqual(t, r.URL.Query().Get("ref"), "master")
if _, ok := r.URL.Query()["ref"]; ok {
t.Fatalf("did not expect ref query for default ref, got %q", r.URL.Query().Get("ref"))
}
writeJSON(t, w, map[string]interface{}{
"entries": []map[string]interface{}{
{"name": "README.md", "type": "file"},
@ -216,7 +218,7 @@ func TestRepoTreeShortcutRegistersHelpFlags(t *testing.T) {
if !ok {
t.Fatal("tree shortcut missing ref flag")
}
if refFlag.Short != "r" || refFlag.Default != "master" || refFlag.Usage == "" {
if refFlag.Short != "r" || refFlag.Default != "" || refFlag.Usage == "" {
t.Fatalf("unexpected ref flag: %+v", refFlag)
}
}