mirror of
https://github.com/github/gh-stack.git
synced 2026-09-14 20:26:28 +08:00
416 lines
10 KiB
Go
416 lines
10 KiB
Go
package cmd
|
|
|
|
import (
|
|
"encoding/json"
|
|
"io"
|
|
"os"
|
|
"path/filepath"
|
|
"testing"
|
|
|
|
"github.com/github/gh-stack/internal/config"
|
|
"github.com/github/gh-stack/internal/git"
|
|
"github.com/github/gh-stack/internal/stack"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
// readCfgOutput closes cfg writers and reads all captured output.
|
|
func readCfgOutput(cfg *config.Config, outR, errR *os.File) string {
|
|
cfg.Out.Close()
|
|
cfg.Err.Close()
|
|
out, _ := io.ReadAll(outR)
|
|
errOut, _ := io.ReadAll(errR)
|
|
return string(out) + string(errOut)
|
|
}
|
|
|
|
func TestNavigate_UpOne(t *testing.T) {
|
|
s := stack.Stack{
|
|
Trunk: stack.BranchRef{Branch: "main"},
|
|
Branches: []stack.BranchRef{{Branch: "b1"}, {Branch: "b2"}, {Branch: "b3"}},
|
|
}
|
|
|
|
var checkedOut []string
|
|
tmpDir := t.TempDir()
|
|
writeStackFile(t, tmpDir, s)
|
|
|
|
mock := &git.MockOps{
|
|
GitDirFn: func() (string, error) { return tmpDir, nil },
|
|
CurrentBranchFn: func() (string, error) { return "b1", nil },
|
|
CheckoutBranchFn: func(name string) error {
|
|
checkedOut = append(checkedOut, name)
|
|
return nil
|
|
},
|
|
}
|
|
restore := git.SetOps(mock)
|
|
defer restore()
|
|
|
|
cfg, _, _ := config.NewTestConfig()
|
|
cmd := UpCmd(cfg)
|
|
cmd.SetOut(io.Discard)
|
|
cmd.SetErr(io.Discard)
|
|
err := cmd.Execute()
|
|
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, []string{"b2"}, checkedOut)
|
|
}
|
|
|
|
func TestNavigate_UpN(t *testing.T) {
|
|
s := stack.Stack{
|
|
Trunk: stack.BranchRef{Branch: "main"},
|
|
Branches: []stack.BranchRef{{Branch: "b1"}, {Branch: "b2"}, {Branch: "b3"}},
|
|
}
|
|
|
|
var checkedOut []string
|
|
tmpDir := t.TempDir()
|
|
writeStackFile(t, tmpDir, s)
|
|
|
|
mock := &git.MockOps{
|
|
GitDirFn: func() (string, error) { return tmpDir, nil },
|
|
CurrentBranchFn: func() (string, error) { return "b1", nil },
|
|
CheckoutBranchFn: func(name string) error {
|
|
checkedOut = append(checkedOut, name)
|
|
return nil
|
|
},
|
|
}
|
|
restore := git.SetOps(mock)
|
|
defer restore()
|
|
|
|
cfg, _, _ := config.NewTestConfig()
|
|
cmd := UpCmd(cfg)
|
|
cmd.SetArgs([]string{"2"})
|
|
cmd.SetOut(io.Discard)
|
|
cmd.SetErr(io.Discard)
|
|
err := cmd.Execute()
|
|
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, []string{"b3"}, checkedOut)
|
|
}
|
|
|
|
func TestNavigate_DownOne(t *testing.T) {
|
|
s := stack.Stack{
|
|
Trunk: stack.BranchRef{Branch: "main"},
|
|
Branches: []stack.BranchRef{{Branch: "b1"}, {Branch: "b2"}, {Branch: "b3"}},
|
|
}
|
|
|
|
var checkedOut []string
|
|
tmpDir := t.TempDir()
|
|
writeStackFile(t, tmpDir, s)
|
|
|
|
mock := &git.MockOps{
|
|
GitDirFn: func() (string, error) { return tmpDir, nil },
|
|
CurrentBranchFn: func() (string, error) { return "b3", nil },
|
|
CheckoutBranchFn: func(name string) error {
|
|
checkedOut = append(checkedOut, name)
|
|
return nil
|
|
},
|
|
}
|
|
restore := git.SetOps(mock)
|
|
defer restore()
|
|
|
|
cfg, _, _ := config.NewTestConfig()
|
|
cmd := DownCmd(cfg)
|
|
cmd.SetOut(io.Discard)
|
|
cmd.SetErr(io.Discard)
|
|
err := cmd.Execute()
|
|
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, []string{"b2"}, checkedOut)
|
|
}
|
|
|
|
func TestNavigate_AtTopClamps(t *testing.T) {
|
|
s := stack.Stack{
|
|
Trunk: stack.BranchRef{Branch: "main"},
|
|
Branches: []stack.BranchRef{{Branch: "b1"}, {Branch: "b2"}},
|
|
}
|
|
|
|
var checkedOut []string
|
|
tmpDir := t.TempDir()
|
|
writeStackFile(t, tmpDir, s)
|
|
|
|
mock := &git.MockOps{
|
|
GitDirFn: func() (string, error) { return tmpDir, nil },
|
|
CurrentBranchFn: func() (string, error) { return "b2", nil },
|
|
CheckoutBranchFn: func(name string) error {
|
|
checkedOut = append(checkedOut, name)
|
|
return nil
|
|
},
|
|
}
|
|
restore := git.SetOps(mock)
|
|
defer restore()
|
|
|
|
cfg, outR, errR := config.NewTestConfig()
|
|
cmd := UpCmd(cfg)
|
|
cmd.SetOut(io.Discard)
|
|
cmd.SetErr(io.Discard)
|
|
err := cmd.Execute()
|
|
|
|
cfg.Out.Close()
|
|
cfg.Err.Close()
|
|
out, _ := io.ReadAll(outR)
|
|
errOut, _ := io.ReadAll(errR)
|
|
output := string(out) + string(errOut)
|
|
|
|
assert.NoError(t, err)
|
|
assert.Empty(t, checkedOut, "should not checkout any branch")
|
|
assert.Contains(t, output, "Already at the top")
|
|
}
|
|
|
|
func TestNavigate_AtBottomClamps(t *testing.T) {
|
|
s := stack.Stack{
|
|
Trunk: stack.BranchRef{Branch: "main"},
|
|
Branches: []stack.BranchRef{{Branch: "b1"}, {Branch: "b2"}},
|
|
}
|
|
|
|
var checkedOut []string
|
|
tmpDir := t.TempDir()
|
|
writeStackFile(t, tmpDir, s)
|
|
|
|
mock := &git.MockOps{
|
|
GitDirFn: func() (string, error) { return tmpDir, nil },
|
|
CurrentBranchFn: func() (string, error) { return "b1", nil },
|
|
CheckoutBranchFn: func(name string) error {
|
|
checkedOut = append(checkedOut, name)
|
|
return nil
|
|
},
|
|
}
|
|
restore := git.SetOps(mock)
|
|
defer restore()
|
|
|
|
cfg, outR, errR := config.NewTestConfig()
|
|
cmd := DownCmd(cfg)
|
|
cmd.SetOut(io.Discard)
|
|
cmd.SetErr(io.Discard)
|
|
err := cmd.Execute()
|
|
|
|
cfg.Out.Close()
|
|
cfg.Err.Close()
|
|
out, _ := io.ReadAll(outR)
|
|
errOut, _ := io.ReadAll(errR)
|
|
output := string(out) + string(errOut)
|
|
|
|
assert.NoError(t, err)
|
|
assert.Empty(t, checkedOut, "should not checkout any branch")
|
|
assert.Contains(t, output, "Already at the bottom")
|
|
}
|
|
|
|
func TestNavigate_FromTrunkGoesUp(t *testing.T) {
|
|
s := stack.Stack{
|
|
Trunk: stack.BranchRef{Branch: "main"},
|
|
Branches: []stack.BranchRef{{Branch: "b1"}, {Branch: "b2"}, {Branch: "b3"}},
|
|
}
|
|
|
|
var checkedOut []string
|
|
tmpDir := t.TempDir()
|
|
writeStackFile(t, tmpDir, s)
|
|
|
|
mock := &git.MockOps{
|
|
GitDirFn: func() (string, error) { return tmpDir, nil },
|
|
CurrentBranchFn: func() (string, error) { return "main", nil },
|
|
CheckoutBranchFn: func(name string) error {
|
|
checkedOut = append(checkedOut, name)
|
|
return nil
|
|
},
|
|
}
|
|
restore := git.SetOps(mock)
|
|
defer restore()
|
|
|
|
cfg, _, _ := config.NewTestConfig()
|
|
cmd := UpCmd(cfg)
|
|
cmd.SetOut(io.Discard)
|
|
cmd.SetErr(io.Discard)
|
|
err := cmd.Execute()
|
|
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, []string{"b1"}, checkedOut)
|
|
}
|
|
|
|
func TestNavigate_SkipsMergedBranches(t *testing.T) {
|
|
s := stack.Stack{
|
|
Trunk: stack.BranchRef{Branch: "main"},
|
|
Branches: []stack.BranchRef{
|
|
{Branch: "b1"},
|
|
{Branch: "b2", PullRequest: &stack.PullRequestRef{Number: 2, Merged: true}},
|
|
{Branch: "b3"},
|
|
},
|
|
}
|
|
|
|
var checkedOut []string
|
|
tmpDir := t.TempDir()
|
|
writeStackFile(t, tmpDir, s)
|
|
|
|
mock := &git.MockOps{
|
|
GitDirFn: func() (string, error) { return tmpDir, nil },
|
|
CurrentBranchFn: func() (string, error) { return "b1", nil },
|
|
CheckoutBranchFn: func(name string) error {
|
|
checkedOut = append(checkedOut, name)
|
|
return nil
|
|
},
|
|
}
|
|
restore := git.SetOps(mock)
|
|
defer restore()
|
|
|
|
cfg, outR, errR := config.NewTestConfig()
|
|
cmd := UpCmd(cfg)
|
|
cmd.SetOut(io.Discard)
|
|
cmd.SetErr(io.Discard)
|
|
err := cmd.Execute()
|
|
|
|
cfg.Out.Close()
|
|
cfg.Err.Close()
|
|
out, _ := io.ReadAll(outR)
|
|
errOut, _ := io.ReadAll(errR)
|
|
output := string(out) + string(errOut)
|
|
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, []string{"b3"}, checkedOut, "should skip merged b2")
|
|
assert.Contains(t, output, "Skipped")
|
|
}
|
|
|
|
func TestNavigate_Top(t *testing.T) {
|
|
s := stack.Stack{
|
|
Trunk: stack.BranchRef{Branch: "main"},
|
|
Branches: []stack.BranchRef{{Branch: "b1"}, {Branch: "b2"}, {Branch: "b3"}},
|
|
}
|
|
|
|
var checkedOut []string
|
|
tmpDir := t.TempDir()
|
|
writeStackFile(t, tmpDir, s)
|
|
|
|
mock := &git.MockOps{
|
|
GitDirFn: func() (string, error) { return tmpDir, nil },
|
|
CurrentBranchFn: func() (string, error) { return "b1", nil },
|
|
CheckoutBranchFn: func(name string) error {
|
|
checkedOut = append(checkedOut, name)
|
|
return nil
|
|
},
|
|
}
|
|
restore := git.SetOps(mock)
|
|
defer restore()
|
|
|
|
cfg, _, _ := config.NewTestConfig()
|
|
cmd := TopCmd(cfg)
|
|
cmd.SetOut(io.Discard)
|
|
cmd.SetErr(io.Discard)
|
|
err := cmd.Execute()
|
|
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, []string{"b3"}, checkedOut)
|
|
}
|
|
|
|
func TestNavigate_Bottom(t *testing.T) {
|
|
s := stack.Stack{
|
|
Trunk: stack.BranchRef{Branch: "main"},
|
|
Branches: []stack.BranchRef{{Branch: "b1"}, {Branch: "b2"}, {Branch: "b3"}},
|
|
}
|
|
|
|
var checkedOut []string
|
|
tmpDir := t.TempDir()
|
|
writeStackFile(t, tmpDir, s)
|
|
|
|
mock := &git.MockOps{
|
|
GitDirFn: func() (string, error) { return tmpDir, nil },
|
|
CurrentBranchFn: func() (string, error) { return "b3", nil },
|
|
CheckoutBranchFn: func(name string) error {
|
|
checkedOut = append(checkedOut, name)
|
|
return nil
|
|
},
|
|
}
|
|
restore := git.SetOps(mock)
|
|
defer restore()
|
|
|
|
cfg, _, _ := config.NewTestConfig()
|
|
cmd := BottomCmd(cfg)
|
|
cmd.SetOut(io.Discard)
|
|
cmd.SetErr(io.Discard)
|
|
err := cmd.Execute()
|
|
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, []string{"b1"}, checkedOut)
|
|
}
|
|
|
|
func TestNavigate_BottomWithMergedFirst(t *testing.T) {
|
|
s := stack.Stack{
|
|
Trunk: stack.BranchRef{Branch: "main"},
|
|
Branches: []stack.BranchRef{
|
|
{Branch: "b1", PullRequest: &stack.PullRequestRef{Number: 1, Merged: true}},
|
|
{Branch: "b2"},
|
|
{Branch: "b3"},
|
|
},
|
|
}
|
|
|
|
var checkedOut []string
|
|
tmpDir := t.TempDir()
|
|
writeStackFile(t, tmpDir, s)
|
|
|
|
mock := &git.MockOps{
|
|
GitDirFn: func() (string, error) { return tmpDir, nil },
|
|
CurrentBranchFn: func() (string, error) { return "b3", nil },
|
|
CheckoutBranchFn: func(name string) error {
|
|
checkedOut = append(checkedOut, name)
|
|
return nil
|
|
},
|
|
}
|
|
restore := git.SetOps(mock)
|
|
defer restore()
|
|
|
|
cfg, _, _ := config.NewTestConfig()
|
|
cmd := BottomCmd(cfg)
|
|
cmd.SetOut(io.Discard)
|
|
cmd.SetErr(io.Discard)
|
|
err := cmd.Execute()
|
|
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, []string{"b2"}, checkedOut, "should skip merged b1")
|
|
}
|
|
|
|
func TestNavigate_AllMerged_Up(t *testing.T) {
|
|
s := stack.Stack{
|
|
Trunk: stack.BranchRef{Branch: "main"},
|
|
Branches: []stack.BranchRef{
|
|
{Branch: "b1", PullRequest: &stack.PullRequestRef{Number: 1, Merged: true}},
|
|
{Branch: "b2", PullRequest: &stack.PullRequestRef{Number: 2, Merged: true}},
|
|
},
|
|
}
|
|
|
|
var checkedOut []string
|
|
tmpDir := t.TempDir()
|
|
writeStackFile(t, tmpDir, s)
|
|
|
|
mock := &git.MockOps{
|
|
GitDirFn: func() (string, error) { return tmpDir, nil },
|
|
CurrentBranchFn: func() (string, error) { return "b2", nil },
|
|
CheckoutBranchFn: func(name string) error {
|
|
checkedOut = append(checkedOut, name)
|
|
return nil
|
|
},
|
|
}
|
|
restore := git.SetOps(mock)
|
|
defer restore()
|
|
|
|
cfg, outR, errR := config.NewTestConfig()
|
|
cmd := UpCmd(cfg)
|
|
cmd.SetOut(io.Discard)
|
|
cmd.SetErr(io.Discard)
|
|
err := cmd.Execute()
|
|
|
|
output := readCfgOutput(cfg, outR, errR)
|
|
|
|
assert.NoError(t, err)
|
|
assert.Empty(t, checkedOut, "should not checkout when already at top of all-merged stack")
|
|
assert.Contains(t, output, "Already at the top")
|
|
// On a merged branch, navigate prints a warning before the at-top message
|
|
assert.Contains(t, output, "you are on merged branch")
|
|
}
|
|
|
|
// writeStackFile is a helper to write a stack file to a temp dir.
|
|
func writeStackFile(t *testing.T, dir string, s stack.Stack) {
|
|
t.Helper()
|
|
sf := &stack.StackFile{
|
|
SchemaVersion: 1,
|
|
Stacks: []stack.Stack{s},
|
|
}
|
|
data, err := json.MarshalIndent(sf, "", " ")
|
|
require.NoError(t, err)
|
|
require.NoError(t, os.WriteFile(filepath.Join(dir, "gh-stack"), data, 0644))
|
|
}
|