Archived
Add switch branch (b): stage the tip and restore to it
b opens a centered popup with a search line and the local and remote branches of the selection. Enter stages the branch tip as the pin of a submodule, restores it (and everything below) to it, and puts HEAD on the branch; a remote branch gets a local tracking branch unless one exists elsewhere. Works on the superproject too. Losses are asked like restore. The backdrop behind the popup is dimmed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,182 @@
|
||||
package tui
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
|
||||
"submarine/internal/submodule"
|
||||
"submarine/internal/submodule/mock"
|
||||
)
|
||||
|
||||
// fakeSwitcher is the mock data with a branch switch that records its calls.
|
||||
type fakeSwitcher struct {
|
||||
submodule.Provider
|
||||
loss submodule.Loss
|
||||
branches []submodule.Branch
|
||||
switched []string // path + " " + ref
|
||||
ends string // the branch Switch reports
|
||||
}
|
||||
|
||||
func (f *fakeSwitcher) Branches(context.Context, string) ([]submodule.Branch, error) {
|
||||
return f.branches, nil
|
||||
}
|
||||
|
||||
func (f *fakeSwitcher) SwitchLoss(context.Context, string, submodule.Branch) (submodule.Loss, error) {
|
||||
return f.loss, nil
|
||||
}
|
||||
|
||||
func (f *fakeSwitcher) Switch(_ context.Context, path string, b submodule.Branch) (string, error) {
|
||||
f.switched = append(f.switched, path+" "+b.Ref)
|
||||
return f.ends, nil
|
||||
}
|
||||
|
||||
func newSwitchModel(t *testing.T, loss submodule.Loss) (tea.Model, *fakeSwitcher) {
|
||||
t.Helper()
|
||||
f := &fakeSwitcher{
|
||||
Provider: mock.New(),
|
||||
loss: loss,
|
||||
ends: "feat",
|
||||
branches: []submodule.Branch{
|
||||
{Ref: "refs/heads/main", Name: "main", Local: "main", Current: true, SHA: "aaaaaaa"},
|
||||
{Ref: "refs/remotes/origin/feat", Name: "origin/feat", Local: "feat", Remote: true, SHA: "bbbbbbb"},
|
||||
},
|
||||
}
|
||||
var m tea.Model = New(context.Background(), f)
|
||||
m, _ = m.Update(tea.WindowSizeMsg{Width: 120, Height: 30})
|
||||
repo, err := f.Tree(context.Background())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
m, _ = m.Update(treeLoadedMsg{repo: repo})
|
||||
return m, f
|
||||
}
|
||||
|
||||
// openPicker presses b and delivers the branch list.
|
||||
func openPicker(t *testing.T, m tea.Model) tea.Model {
|
||||
t.Helper()
|
||||
m, cmd := press(m, "b")
|
||||
m = feed(m, cmd)
|
||||
if m.(Model).picker == nil {
|
||||
t.Fatal("the picker did not open")
|
||||
}
|
||||
return m
|
||||
}
|
||||
|
||||
func pressKey(m tea.Model, t tea.KeyType) (tea.Model, tea.Cmd) {
|
||||
return m.Update(tea.KeyMsg{Type: t})
|
||||
}
|
||||
|
||||
// settle delivers the loss check and, if it ran at once, the switch.
|
||||
func settle(m tea.Model, cmd tea.Cmd) tea.Model {
|
||||
var next tea.Cmd
|
||||
for _, msg := range run(cmd) {
|
||||
if l, ok := msg.(lossMsg); ok {
|
||||
m, next = m.Update(l)
|
||||
}
|
||||
}
|
||||
return feed(m, next)
|
||||
}
|
||||
|
||||
func TestSwitchOpensThePickerWithTheBranches(t *testing.T) {
|
||||
m, _ := newSwitchModel(t, submodule.Loss{})
|
||||
m = openPicker(t, m)
|
||||
v := m.View()
|
||||
for _, want := range []string{"Switch the superproject", "main", "origin/", "feat"} {
|
||||
if !strings.Contains(v, want) {
|
||||
t.Errorf("view lacks %q", want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestSwitchTypingFiltersAndEnterSwitchesWithoutLoss(t *testing.T) {
|
||||
m, f := newSwitchModel(t, submodule.Loss{})
|
||||
m = openPicker(t, m)
|
||||
m, _ = press(m, "fe") // j and k would be text, not navigation
|
||||
m, cmd := pressKey(m, tea.KeyEnter)
|
||||
if m.(Model).picker != nil {
|
||||
t.Error("enter must close the picker")
|
||||
}
|
||||
m = settle(m, cmd)
|
||||
if len(f.switched) != 1 || f.switched[0] != " refs/remotes/origin/feat" {
|
||||
t.Fatalf("switched %q", f.switched)
|
||||
}
|
||||
if m.(Model).restore != nil {
|
||||
t.Error("switch still in progress")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSwitchArrowsSelect(t *testing.T) {
|
||||
m, f := newSwitchModel(t, submodule.Loss{})
|
||||
m = openPicker(t, m)
|
||||
m, _ = pressKey(m, tea.KeyDown)
|
||||
m, cmd := pressKey(m, tea.KeyEnter)
|
||||
settle(m, cmd)
|
||||
if len(f.switched) != 1 || f.switched[0] != " refs/remotes/origin/feat" {
|
||||
t.Fatalf("switched %q, want the second branch", f.switched)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSwitchAsksWhenSomethingIsLost(t *testing.T) {
|
||||
m, f := newSwitchModel(t, submodule.Loss{Files: 2, Modules: 1})
|
||||
m = openPicker(t, m)
|
||||
m, cmd := pressKey(m, tea.KeyEnter)
|
||||
var next tea.Cmd
|
||||
for _, msg := range run(cmd) {
|
||||
if l, ok := msg.(lossMsg); ok {
|
||||
m, next = m.Update(l)
|
||||
}
|
||||
}
|
||||
r := m.(Model).restore
|
||||
if next != nil || r == nil || r.phase != restoreAsking {
|
||||
t.Fatalf("want a question, got %+v", r)
|
||||
}
|
||||
if v := m.View(); !strings.Contains(v, "Switch the superproject to main?") || !strings.Contains(v, "y switch") {
|
||||
t.Errorf("question not shown:\n%s", v)
|
||||
}
|
||||
if c, _ := press(m, "n"); c.(Model).restore != nil || len(f.switched) != 0 {
|
||||
t.Fatal("n must cancel")
|
||||
}
|
||||
m, cmd = press(m, "y")
|
||||
feed(m, cmd)
|
||||
if len(f.switched) != 1 {
|
||||
t.Fatalf("switched %q", f.switched)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSwitchEscClosesThePicker(t *testing.T) {
|
||||
m, f := newSwitchModel(t, submodule.Loss{})
|
||||
m = openPicker(t, m)
|
||||
m, _ = pressKey(m, tea.KeyEsc)
|
||||
if m.(Model).picker != nil || len(f.switched) != 0 {
|
||||
t.Fatal("esc must close without switching")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSwitchWithoutSwitcherSaysSo(t *testing.T) {
|
||||
var m tea.Model = New(context.Background(), mock.New())
|
||||
m, _ = m.Update(tea.WindowSizeMsg{Width: 120, Height: 30})
|
||||
repo, _ := mock.New().Tree(context.Background())
|
||||
m, _ = m.Update(treeLoadedMsg{repo: repo})
|
||||
m, _ = press(m, "b")
|
||||
if m.(Model).picker != nil {
|
||||
t.Fatal("the mock can't switch")
|
||||
}
|
||||
if !strings.Contains(m.View(), "can't switch branches") {
|
||||
t.Error("no flash")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSwitchReportsADetachedHead(t *testing.T) {
|
||||
m, f := newSwitchModel(t, submodule.Loss{})
|
||||
f.ends = ""
|
||||
m = openPicker(t, m)
|
||||
m, _ = pressKey(m, tea.KeyDown)
|
||||
m, cmd := pressKey(m, tea.KeyEnter)
|
||||
m = settle(m, cmd)
|
||||
if v := m.View(); !strings.Contains(v, "detached") {
|
||||
t.Errorf("flash lacks the detached note:\n%s", v)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user