mirror of
https://github.com/jesseduffield/lazygit.git
synced 2025-06-06 23:46:13 +02:00
vastly improve the logic for undo and redo
This commit is contained in:
parent
32d3e497c3
commit
d105e2690a
@ -1,10 +1,9 @@
|
|||||||
package gui
|
package gui
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"regexp"
|
|
||||||
|
|
||||||
"github.com/jesseduffield/gocui"
|
"github.com/jesseduffield/gocui"
|
||||||
"github.com/jesseduffield/lazygit/pkg/commands"
|
"github.com/jesseduffield/lazygit/pkg/commands"
|
||||||
|
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Quick summary of how this all works:
|
// Quick summary of how this all works:
|
||||||
@ -18,110 +17,136 @@ import (
|
|||||||
// two user actions, meaning we end up undoing reflog entry C. Redoing works in a similar way.
|
// two user actions, meaning we end up undoing reflog entry C. Redoing works in a similar way.
|
||||||
|
|
||||||
const (
|
const (
|
||||||
USER_ACTION = iota
|
CHECKOUT = iota
|
||||||
UNDO
|
COMMIT
|
||||||
REDO
|
REBASE
|
||||||
|
CURRENT_REBASE
|
||||||
)
|
)
|
||||||
|
|
||||||
type reflogAction struct {
|
type reflogAction struct {
|
||||||
regexStr string
|
kind int // one of CHECKOUT, REBASE, and COMMIT
|
||||||
action func(match []string, commitSha string, waitingStatus string, envVars []string, isUndo bool) error
|
from string
|
||||||
kind int
|
to string
|
||||||
}
|
}
|
||||||
|
|
||||||
func (gui *Gui) reflogActions() []reflogAction {
|
// Here we're going through the reflog and maintaining a counter that represents how many
|
||||||
return []reflogAction{
|
// undos/redos/user actions we've seen. when we hit a user action we call the callback specifying
|
||||||
{
|
// what the counter is up to and the nature of the action.
|
||||||
regexStr: `^checkout: moving from ([\S]+) to ([\S]+)`,
|
// We can't take you from a non-interactive rebase state into an interactive rebase state, so if we hit
|
||||||
kind: USER_ACTION,
|
// a 'finish' or an 'abort' entry, we ignore everything else until we find the corresponding 'start' entry.
|
||||||
action: func(match []string, commitSha string, waitingStatus string, envVars []string, isUndo bool) error {
|
// If we find ourselves already in an interactive rebase and we've hit the start entry,
|
||||||
branchName := match[2]
|
// we can't really do an undo because there's no way to redo back into the rebase.
|
||||||
if isUndo {
|
// instead we just ask the user if they want to abort the rebase instead.
|
||||||
branchName = match[1]
|
func (gui *Gui) parseReflogForActions(onUserAction func(counter int, action reflogAction) (bool, error)) error {
|
||||||
}
|
|
||||||
return gui.handleCheckoutRef(branchName, handleCheckoutRefOptions{
|
|
||||||
WaitingStatus: waitingStatus,
|
|
||||||
EnvVars: envVars,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
regexStr: `^commit|^rebase -i \(start\)|^reset: moving to|^pull`,
|
|
||||||
kind: USER_ACTION,
|
|
||||||
action: func(match []string, commitSha string, waitingStatus string, envVars []string, isUndo bool) error {
|
|
||||||
return gui.handleHardResetWithAutoStash(commitSha, handleHardResetWithAutoStashOptions{EnvVars: envVars, WaitingStatus: waitingStatus})
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
regexStr: `^\[lazygit undo\]`,
|
|
||||||
kind: UNDO,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
regexStr: `^\[lazygit redo\]`,
|
|
||||||
kind: REDO,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (gui *Gui) reflogUndo(g *gocui.Gui, v *gocui.View) error {
|
|
||||||
return gui.iterateUserActions(func(match []string, reflogCommits []*commands.Commit, reflogIdx int, action reflogAction, counter int) (bool, error) {
|
|
||||||
if counter == -1 {
|
|
||||||
prevCommitSha := ""
|
|
||||||
if len(reflogCommits)-1 >= reflogIdx+1 {
|
|
||||||
prevCommitSha = reflogCommits[reflogIdx+1].Sha
|
|
||||||
}
|
|
||||||
return true, action.action(match, prevCommitSha, gui.Tr.SLocalize("UndoingStatus"), []string{"GIT_REFLOG_ACTION=[lazygit undo]"}, true)
|
|
||||||
} else {
|
|
||||||
return false, nil
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (gui *Gui) reflogRedo(g *gocui.Gui, v *gocui.View) error {
|
|
||||||
return gui.iterateUserActions(func(match []string, reflogCommits []*commands.Commit, reflogIdx int, action reflogAction, counter int) (bool, error) {
|
|
||||||
if counter == 0 {
|
|
||||||
return true, action.action(match, reflogCommits[reflogIdx].Sha, gui.Tr.SLocalize("RedoingStatus"), []string{"GIT_REFLOG_ACTION=[lazygit redo]"}, false)
|
|
||||||
} else if counter < 0 {
|
|
||||||
return true, nil
|
|
||||||
} else {
|
|
||||||
return false, nil
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (gui *Gui) iterateUserActions(onUserAction func(match []string, reflogCommits []*commands.Commit, reflogIdx int, action reflogAction, counter int) (bool, error)) error {
|
|
||||||
reflogCommits := gui.State.ReflogCommits
|
|
||||||
|
|
||||||
counter := 0
|
counter := 0
|
||||||
for i, reflogCommit := range reflogCommits {
|
reflogCommits := gui.State.ReflogCommits
|
||||||
for _, action := range gui.reflogActions() {
|
rebaseFinishCommitSha := ""
|
||||||
re := regexp.MustCompile(action.regexStr)
|
var action *reflogAction
|
||||||
match := re.FindStringSubmatch(reflogCommit.Name)
|
for reflogCommitIdx, reflogCommit := range reflogCommits {
|
||||||
if len(match) == 0 {
|
action = nil
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
switch action.kind {
|
prevCommitSha := ""
|
||||||
case UNDO:
|
if len(reflogCommits)-1 >= reflogCommitIdx+1 {
|
||||||
|
prevCommitSha = reflogCommits[reflogCommitIdx+1].Sha
|
||||||
|
}
|
||||||
|
|
||||||
|
if rebaseFinishCommitSha == "" {
|
||||||
|
if ok, _ := utils.FindStringSubmatch(reflogCommit.Name, `^\[lazygit undo\]`); ok {
|
||||||
counter++
|
counter++
|
||||||
case REDO:
|
} else if ok, _ := utils.FindStringSubmatch(reflogCommit.Name, `^\[lazygit redo\]`); ok {
|
||||||
counter--
|
counter--
|
||||||
case USER_ACTION:
|
} else if ok, _ := utils.FindStringSubmatch(reflogCommit.Name, `^rebase -i \(abort\)|^rebase -i \(finish\)`); ok {
|
||||||
counter--
|
rebaseFinishCommitSha = reflogCommit.Sha
|
||||||
shouldReturn, err := onUserAction(match, reflogCommits, i, action, counter)
|
} else if ok, match := utils.FindStringSubmatch(reflogCommit.Name, `^checkout: moving from ([\S]+) to ([\S]+)`); ok {
|
||||||
if err != nil {
|
action = &reflogAction{kind: CHECKOUT, from: match[1], to: match[2]}
|
||||||
return err
|
} else if ok, _ := utils.FindStringSubmatch(reflogCommit.Name, `^commit|^reset: moving to|^pull`); ok {
|
||||||
}
|
action = &reflogAction{kind: COMMIT, from: prevCommitSha, to: reflogCommit.Sha}
|
||||||
if shouldReturn {
|
} else if ok, _ := utils.FindStringSubmatch(reflogCommit.Name, `^rebase -i \(start\)`); ok {
|
||||||
return nil
|
// if we're here then we must be currently inside an interactive rebase
|
||||||
}
|
action = &reflogAction{kind: CURRENT_REBASE}
|
||||||
|
}
|
||||||
|
} else if ok, _ := utils.FindStringSubmatch(reflogCommit.Name, `^rebase -i \(start\)`); ok {
|
||||||
|
action = &reflogAction{kind: REBASE, from: prevCommitSha, to: rebaseFinishCommitSha}
|
||||||
|
}
|
||||||
|
|
||||||
|
if action != nil {
|
||||||
|
ok, err := onUserAction(counter, *action)
|
||||||
|
if ok {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
counter--
|
||||||
|
if action.kind == REBASE {
|
||||||
|
rebaseFinishCommitSha = ""
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (gui *Gui) reflogUndo(g *gocui.Gui, v *gocui.View) error {
|
||||||
|
undoEnvVars := []string{"GIT_REFLOG_ACTION=[lazygit undo]"}
|
||||||
|
undoingStatus := gui.Tr.SLocalize("UndoingStatus")
|
||||||
|
|
||||||
|
return gui.parseReflogForActions(func(counter int, action reflogAction) (bool, error) {
|
||||||
|
if counter != 0 {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
switch action.kind {
|
||||||
|
case COMMIT, REBASE:
|
||||||
|
return true, gui.handleHardResetWithAutoStash(action.from, handleHardResetWithAutoStashOptions{
|
||||||
|
EnvVars: undoEnvVars,
|
||||||
|
WaitingStatus: undoingStatus,
|
||||||
|
})
|
||||||
|
case CURRENT_REBASE:
|
||||||
|
return true, gui.createConfirmationPanel(g, v, true, gui.Tr.SLocalize("AbortRebase"), gui.Tr.SLocalize("UndoOutOfRebaseWarning"), func(g *gocui.Gui, v *gocui.View) error {
|
||||||
|
return gui.genericMergeCommand("abort")
|
||||||
|
}, nil)
|
||||||
|
case CHECKOUT:
|
||||||
|
return true, gui.handleCheckoutRef(action.from, handleCheckoutRefOptions{
|
||||||
|
EnvVars: undoEnvVars,
|
||||||
|
WaitingStatus: undoingStatus,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
gui.Log.Error("didn't match on the user action when trying to undo")
|
||||||
|
return true, nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (gui *Gui) reflogRedo(g *gocui.Gui, v *gocui.View) error {
|
||||||
|
redoEnvVars := []string{"GIT_REFLOG_ACTION=[lazygit redo]"}
|
||||||
|
redoingStatus := gui.Tr.SLocalize("RedoingStatus")
|
||||||
|
|
||||||
|
return gui.parseReflogForActions(func(counter int, action reflogAction) (bool, error) {
|
||||||
|
// if we're redoing and the counter is zero, we just return
|
||||||
|
if counter == 0 {
|
||||||
|
return true, nil
|
||||||
|
} else if counter > 1 {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
switch action.kind {
|
||||||
|
case COMMIT, REBASE:
|
||||||
|
return true, gui.handleHardResetWithAutoStash(action.to, handleHardResetWithAutoStashOptions{
|
||||||
|
EnvVars: redoEnvVars,
|
||||||
|
WaitingStatus: redoingStatus,
|
||||||
|
})
|
||||||
|
case CURRENT_REBASE:
|
||||||
|
// no idea if this is even possible but you certainly can't redo into the end of a rebase if you're still in the rebase
|
||||||
|
return true, nil
|
||||||
|
case CHECKOUT:
|
||||||
|
return true, gui.handleCheckoutRef(action.to, handleCheckoutRefOptions{
|
||||||
|
EnvVars: redoEnvVars,
|
||||||
|
WaitingStatus: redoingStatus,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
gui.Log.Error("didn't match on the user action when trying to redo")
|
||||||
|
return true, nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
type handleHardResetWithAutoStashOptions struct {
|
type handleHardResetWithAutoStashOptions struct {
|
||||||
WaitingStatus string
|
WaitingStatus string
|
||||||
EnvVars []string
|
EnvVars []string
|
||||||
|
@ -1056,6 +1056,12 @@ func addEnglish(i18nObject *i18n.Bundle) error {
|
|||||||
}, &i18n.Message{
|
}, &i18n.Message{
|
||||||
ID: "prevTab",
|
ID: "prevTab",
|
||||||
Other: "previous tab",
|
Other: "previous tab",
|
||||||
|
}, &i18n.Message{
|
||||||
|
ID: "AbortRebase",
|
||||||
|
Other: "Abort rebase",
|
||||||
|
}, &i18n.Message{
|
||||||
|
ID: "UndoOutOfRebaseWarning",
|
||||||
|
Other: "If you undo at this point, you won't be able to re-enter this rebase by pressing redo. Abort rebase?",
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -316,3 +316,9 @@ func TruncateWithEllipsis(str string, limit int) string {
|
|||||||
remainingLength := limit - len(ellipsis)
|
remainingLength := limit - len(ellipsis)
|
||||||
return str[0:remainingLength] + "..."
|
return str[0:remainingLength] + "..."
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func FindStringSubmatch(str string, regexpStr string) (bool, []string) {
|
||||||
|
re := regexp.MustCompile(regexpStr)
|
||||||
|
match := re.FindStringSubmatch(str)
|
||||||
|
return len(match) > 0, match
|
||||||
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user