Update a pin that is behind with u; switch fast-forwards

`u` moves the pin of the selection to the tip of its ref when the pin is
only behind it: a switch to that branch with exactly the tip the row
shows, nothing fetched; if the ref moved meanwhile, nothing happens. On
the superproject it fast-forwards the branch to its upstream and restores
the submodules (git pull --ff-only from the last fetch).

Switch now fast-forwards a local branch that is only behind the chosen
remote branch, unless another worktree has it checked out, and keeps a
checked-out branch that tracks it under another name. A branch with
commits of its own is still never moved.

Stage pin is dropped: switching to the branch already stages it. The
detail pane's palette wraps instead of cutting off keys. Add D29 and F59.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-24 22:39:18 +02:00
co-authored by Claude Opus 5.5
parent 99f1ba2bec
commit fbd233f58d
16 changed files with 686 additions and 105 deletions
+41 -12
View File
@@ -28,13 +28,15 @@ type restoreState struct {
to submodule.RestoreTarget
loss submodule.Loss
branch *submodule.Branch // set for a branch switch, which reuses this flow
pinUpdate bool // a switch to the pin's ref (u); branch is set by the check
}
// lossMsg is the result of the check before a restore.
type lossMsg struct {
path string
loss submodule.Loss
err error
path string
loss submodule.Loss
err error
branch *submodule.Branch // the branch a pin update switches to
}
type restoreDoneMsg struct {
@@ -86,7 +88,7 @@ func (m Model) checkRestore() (tea.Model, tea.Cmd) {
} else {
loss, err = m.provider.(submodule.Restorer).RestoreLoss(ctx, path, to)
}
return lossMsg{path, loss, err}
return lossMsg{path: path, loss: loss, err: err}
}
return m, tea.Batch(check, m.tick())
}
@@ -115,11 +117,18 @@ func (m Model) lossChecked(msg lossMsg) (tea.Model, tea.Cmd) {
return m, nil
}
if msg.err != nil {
verb := "check"
if m.restore.pinUpdate {
verb = "update"
}
m.restore = nil
m.notice = "can't check " + restoreName(m.byPath[msg.path]) + ": " + msg.err.Error()
m.notice = "can't " + verb + " " + restoreName(m.byPath[msg.path]) + ": " + msg.err.Error()
m.layout()
return m, nil
}
if msg.branch != nil {
m.restore.branch = msg.branch
}
m.restore.loss = msg.loss
if msg.loss.Empty() {
return m.runRestore()
@@ -165,15 +174,15 @@ func (m Model) restoreDone(msg restoreDoneMsg) (tea.Model, tea.Cmd) {
var cmds []tea.Cmd
switch {
case r != nil && r.branch != nil && msg.err != nil:
m.notice = "switch of " + r.name + " to " + r.branch.Name + " failed: " + msg.err.Error()
m.notice = r.noun() + " of " + r.name + " to " + r.branch.Name + " failed: " + msg.err.Error()
case r != nil && r.branch != nil:
to := r.branch.Name
if msg.branch != "" {
to = msg.branch
} else if r.branch.Remote {
to += " (detached: local " + r.branch.Local + " is elsewhere)"
to += " (detached: local " + r.branch.Local + " can't be fast-forwarded)"
}
cmds = append(cmds, m.footer.Flash("switched "+r.name+" to "+to))
cmds = append(cmds, m.footer.Flash(r.verbEd()+" "+r.name+" to "+to))
default:
name := "the superproject"
if r != nil {
@@ -202,22 +211,42 @@ func (r *restoreState) toText() string {
return ""
}
// verb and verbIng word the operation: a switch is shown like a restore, but
// names its branch.
// verb, verbIng, verbEd and noun word the operation: a switch and a pin
// update are shown like a restore, but name their branch.
func (r *restoreState) verb() string {
if r.branch != nil {
switch {
case r.pinUpdate:
return "Update"
case r.branch != nil:
return "Switch"
}
return "Restore"
}
func (r *restoreState) verbIng() string {
if r.branch != nil {
switch {
case r.pinUpdate:
return "updating"
case r.branch != nil:
return "switching"
}
return "restoring"
}
func (r *restoreState) verbEd() string {
if r.pinUpdate {
return "updated"
}
return "switched"
}
func (r *restoreState) noun() string {
if r.pinUpdate {
return "update"
}
return "switch"
}
func restoreName(s *submodule.Submodule) string {
if s == nil || s.IsRoot() {
return "the superproject"