Hard restore with X asks before dropping staged pins

X dropped staged pins without a question, including the selected
submodule's own pin, which the loss check didn't count. A staged pin is
a decision, and X sits one Shift away from x, so dropped staged pins now
count as a loss and make X ask (D31, amends D30).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-24 23:26:32 +02:00
co-authored by Claude Opus 5.5
parent 72500353cd
commit b205576759
9 changed files with 84 additions and 24 deletions
+22 -3
View File
@@ -35,10 +35,29 @@ func (p *Provider) RestoreLoss(ctx context.Context, path string, to submodule.Re
if err != nil {
return submodule.Loss{}, err
}
if dir == "" { // not initialized: nothing on disk to lose
return submodule.Loss{}, nil
var loss submodule.Loss
if dir != "" { // not initialized: nothing on disk to lose
loss = p.lossOf(ctx, dir, target, to.KeepsStaged())
}
return p.lossOf(ctx, dir, target, to.KeepsStaged()), nil
if to == submodule.RestoreHard && path != "" && p.pinStaged(ctx, path, target) {
// a hard restore unstages the submodule's own pin too
loss.StagedPins++
loss.Modules = max(loss.Modules, 1)
}
return loss, nil
}
// pinStaged reports whether the pin of path in its parent's index differs
// from committed, the pin a hard restore goes to.
func (p *Provider) pinStaged(ctx context.Context, path, committed string) bool {
p.mu.RLock()
in, ok := p.mods[path]
p.mu.RUnlock()
if !ok {
return false
}
staged, err := pinOf(ctx, in, submodule.RestoreStaged)
return err == nil && staged != committed
}
// Restore makes path ("" is the superproject) and all submodules below it
+23
View File
@@ -364,6 +364,29 @@ func TestRestoreHardOfASubmoduleUnstagesItsPin(t *testing.T) {
}
}
func TestRestoreLossOfAHardRestoreCountsTheOwnStagedPin(t *testing.T) {
p, _ := ownFixture(t)
committed, err := p.RestoreLoss(context.Background(), "libs/staged", submodule.RestoreCommitted)
if err != nil {
t.Fatal(err)
}
hard, err := p.RestoreLoss(context.Background(), "libs/staged", submodule.RestoreHard)
if err != nil {
t.Fatal(err)
}
if hard.StagedPins != committed.StagedPins+1 || hard.Modules == 0 {
t.Errorf("hard loss %+v, want the own staged pin on top of %+v", hard, committed)
}
// without a staged pin, a hard restore unstages nothing of its own
clean, err := p.RestoreLoss(context.Background(), "src/ui", submodule.RestoreHard)
if err != nil {
t.Fatal(err)
}
if clean.StagedPins != 0 {
t.Errorf("loss of src/ui %+v, want no staged pin", clean)
}
}
func TestRestoreLossCountsStagedPinsOfAMovingRepository(t *testing.T) {
p, root := ownFixture(t)
super := filepath.Join(root, "super")
+1 -7
View File
@@ -135,13 +135,7 @@ func (m Model) lossChecked(msg lossMsg) (tea.Model, tea.Cmd) {
m.restore.branch = msg.branch
}
m.restore.loss = msg.loss
// a hard restore is asked for to drop staged pins: alone they are no
// reason to ask, but are listed if something else is lost
lost := msg.loss
if m.restore.to == submodule.RestoreHard {
lost.StagedPins, lost.Modules = 0, 0
}
if lost.Empty() {
if msg.loss.Empty() {
return m.runRestore()
}
m.restore.phase = restoreAsking
+12 -5
View File
@@ -189,7 +189,7 @@ func TestRestoreOfAStagedPinAsksWhichPin(t *testing.T) {
}
}
func TestHardRestoreOfAStagedPinDoesNotAsk(t *testing.T) {
func TestHardRestoreAsksToDropStagedPins(t *testing.T) {
m, f := newRestoreModel(t, submodule.Loss{StagedPins: 2, Modules: 2})
mm := m.(Model)
var staged *submodule.Submodule
@@ -202,15 +202,22 @@ func TestHardRestoreOfAStagedPinDoesNotAsk(t *testing.T) {
mm.tree.Select(staged.Path)
m = mm
// neither which pin, nor y/n for staged pins alone
// not which pin, but y/n: staged pins alone are a reason to ask
m, cmd := press(m, "X")
var next tea.Cmd
for _, msg := range run(cmd) {
if l, ok := msg.(lossMsg); ok {
m, next = m.Update(l)
m, _ = m.Update(l)
}
}
feed(m, next)
r := m.(Model).restore
if r == nil || r.phase != restoreAsking {
t.Fatalf("want a question, got %+v", r)
}
if got := lossText(r.loss); got != "2 staged pins" {
t.Errorf("question lists %q, want the staged pins", got)
}
m, cmd = press(m, "y")
feed(m, cmd)
if len(f.to) != 1 || f.to[0] != submodule.RestoreHard || f.restored[0] != staged.Path {
t.Fatalf("restored %q to %v, want %s hard", f.restored, f.to, staged.Path)
}