feat(pulls): support branch-validated stack reordering

Assisted-by: Codet:GPT-5-Codex
This commit is contained in:
Lunny Xiao
2026-09-24 23:31:32 -07:00
parent f28a5e696a
commit b06d902687
5 changed files with 103 additions and 1 deletions
+2 -1
View File
@@ -15,13 +15,14 @@ import (
var CmdPullsStack = cli.Command{ var CmdPullsStack = cli.Command{
Name: "stack", Name: "stack",
Usage: "Manage pull request stacks", Usage: "Manage pull request stacks",
Description: "List, show, push, restack, and merge pull request stacks", Description: "List, show, push, reorder, restack, and merge pull request stacks",
Action: runPullsStackDefault, Action: runPullsStackDefault,
Commands: []*cli.Command{ Commands: []*cli.Command{
&stack.CmdStackList, &stack.CmdStackList,
&stack.CmdStackShow, &stack.CmdStackShow,
&stack.CmdStackPush, &stack.CmdStackPush,
&stack.CmdStackRestack, &stack.CmdStackRestack,
&stack.CmdStackReorder,
&stack.CmdStackMerge, &stack.CmdStackMerge,
}, },
} }
+53
View File
@@ -0,0 +1,53 @@
// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package stack
import (
stdctx "context"
"fmt"
"gitea.dev/tea/cmd/flags"
"gitea.dev/tea/modules/context"
"gitea.dev/tea/modules/task"
"gitea.dev/tea/modules/utils"
"github.com/urfave/cli/v3"
)
// CmdStackReorder stores a validated, already-linked PR branch order.
var CmdStackReorder = cli.Command{
Name: "reorder", Usage: "Reorder PRs after their branches have been rebased",
ArgsUsage: "<stack id> <pull index> <pull index> [more pull indices]",
Flags: flags.AllDefaultFlags, Action: RunStackReorder,
}
func RunStackReorder(requestCtx stdctx.Context, cmd *cli.Command) error {
if cmd.Args().Len() < 3 {
return fmt.Errorf("stack id and at least two pull numbers are required")
}
stackID, err := utils.ArgToIndex(cmd.Args().Get(0))
if err != nil {
return err
}
indices := make([]int64, 0, cmd.Args().Len()-1)
for i := 1; i < cmd.Args().Len(); i++ {
index, err := utils.ArgToIndex(cmd.Args().Get(i))
if err != nil {
return err
}
indices = append(indices, index)
}
ctx, err := context.InitCommand(cmd)
if err != nil {
return err
}
if err := ctx.Ensure(context.CtxRequirement{RemoteRepo: true}); err != nil {
return err
}
if _, err := task.ReorderPullStack(requestCtx, ctx.Login, ctx.Owner, ctx.Repo, stackID, indices); err != nil {
return err
}
fmt.Fprintf(ctx.Writer, "reordered stack #%d\n", stackID)
return nil
}
+17
View File
@@ -0,0 +1,17 @@
// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package stack
import (
"context"
"testing"
"github.com/stretchr/testify/assert"
)
func TestStackReorderRequiresCompleteOrder(t *testing.T) {
cmd := CmdStackReorder
err := cmd.Run(context.Background(), []string{"reorder", "5", "2"})
assert.ErrorContains(t, err, "at least two pull numbers")
}
+13
View File
@@ -120,6 +120,19 @@ func AddPullStackEntry(ctx stdctx.Context, login *config.Login, owner, repo stri
return &s, nil return &s, nil
} }
// ReorderPullStack records the order of PR branches that are already linked.
func ReorderPullStack(ctx stdctx.Context, login *config.Login, owner, repo string, id int64, pullIndices []int64) (*stack.Stack, error) {
var s stack.Stack
err := postStackJSON(ctx, login, fmt.Sprintf("/repos/%s/%s/stacks/%d/reorder", owner, repo, id),
struct {
PullIndices []int64 `json:"pull_indices"`
}{PullIndices: pullIndices}, http.StatusOK, &s)
if err != nil {
return nil, err
}
return &s, nil
}
// RestackPullStack queues a restack starting at the stack's base branch. // RestackPullStack queues a restack starting at the stack's base branch.
func RestackPullStack(ctx stdctx.Context, login *config.Login, owner, repo string, id int64) error { func RestackPullStack(ctx stdctx.Context, login *config.Login, owner, repo string, id int64) error {
client := api.NewClient(login) client := api.NewClient(login)
+18
View File
@@ -134,6 +134,24 @@ func TestAddPullStackEntry(t *testing.T) {
assert.Equal(t, int64(5), stack.ID) assert.Equal(t, int64(5), stack.ID)
} }
func TestReorderPullStack(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
assert.Equal(t, http.MethodPost, r.Method)
assert.Equal(t, "/api/v1/repos/owner/repo/stacks/5/reorder", r.URL.Path)
body, err := io.ReadAll(r.Body)
require.NoError(t, err)
assert.JSONEq(t, `{"pull_indices":[2,3]}`, string(body))
w.Header().Set("Content-Type", "application/json")
_, _ = w.Write([]byte(`{"id":5,"repo_id":2,"owner_id":3,"title":"feat: base","base_branch":"main","state":"open","auto_restack":true,"entries":[]}`))
}))
defer server.Close()
result, err := ReorderPullStack(t.Context(), &config.Login{
Name: "test", URL: server.URL, Token: "secret-token",
}, "owner", "repo", 5, []int64{2, 3})
require.NoError(t, err)
assert.Equal(t, int64(5), result.ID)
}
func TestRestackPullStack(t *testing.T) { func TestRestackPullStack(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
assert.Equal(t, http.MethodPost, r.Method) assert.Equal(t, http.MethodPost, r.Method)