1
0
mirror of https://github.com/jesseduffield/lazygit.git synced 2025-05-29 23:17:32 +02:00

use reflog undo history pointer

This commit is contained in:
Jesse Duffield 2020-03-21 11:56:34 +11:00
parent b1b0219f04
commit f80d15062b
8 changed files with 84 additions and 27 deletions

View File

@ -1106,15 +1106,15 @@ func (c *GitCommand) FetchRemote(remoteName string) error {
} }
func (c *GitCommand) GetReflogCommits() ([]*Commit, error) { func (c *GitCommand) GetReflogCommits() ([]*Commit, error) {
output, err := c.OSCommand.RunCommandWithOutput("git reflog --abbrev=20") output, err := c.OSCommand.RunCommandWithOutput("git reflog --abbrev=20 --date=iso")
if err != nil { if err != nil {
// assume error means we have no reflog // assume error means we have no reflog
return []*Commit{}, nil return []*Commit{}, nil
} }
lines := strings.Split(strings.TrimSpace(output), "\n") lines := strings.Split(strings.TrimSpace(output), "\n")
commits := make([]*Commit, 0) commits := make([]*Commit, 0, len(lines))
re := regexp.MustCompile(`(\w+).*HEAD@\{\d+\}: (.*)`) re := regexp.MustCompile(`(\w+).*HEAD@\{([^\}]+)\}: (.*)`)
for _, line := range lines { for _, line := range lines {
match := re.FindStringSubmatch(line) match := re.FindStringSubmatch(line)
if len(match) <= 1 { if len(match) <= 1 {
@ -1123,7 +1123,8 @@ func (c *GitCommand) GetReflogCommits() ([]*Commit, error) {
commit := &Commit{ commit := &Commit{
Sha: match[1], Sha: match[1],
Name: match[2], Name: match[3],
Date: match[2],
Status: "reflog", Status: "reflog",
} }

View File

@ -106,7 +106,7 @@ func (gui *Gui) handleBranchPress(g *gocui.Gui, v *gocui.View) error {
return gui.createErrorPanel(g, gui.Tr.SLocalize("AlreadyCheckedOutBranch")) return gui.createErrorPanel(g, gui.Tr.SLocalize("AlreadyCheckedOutBranch"))
} }
branch := gui.getSelectedBranch() branch := gui.getSelectedBranch()
return gui.handleCheckoutRef(branch.Name) return gui.handleCheckoutRef(branch.Name, nil)
} }
func (gui *Gui) handleCreatePullRequestPress(g *gocui.Gui, v *gocui.View) error { func (gui *Gui) handleCreatePullRequestPress(g *gocui.Gui, v *gocui.View) error {
@ -143,7 +143,7 @@ func (gui *Gui) handleForceCheckout(g *gocui.Gui, v *gocui.View) error {
}, nil) }, nil)
} }
func (gui *Gui) handleCheckoutRef(ref string) error { func (gui *Gui) handleCheckoutRef(ref string, onDone func()) error {
if err := gui.GitCommand.Checkout(ref, false); err != nil { if err := gui.GitCommand.Checkout(ref, false); err != nil {
// note, this will only work for english-language git commands. If we force git to use english, and the error isn't this one, then the user will receive an english command they may not understand. I'm not sure what the best solution to this is. Running the command once in english and a second time in the native language is one option // note, this will only work for english-language git commands. If we force git to use english, and the error isn't this one, then the user will receive an english command they may not understand. I'm not sure what the best solution to this is. Running the command once in english and a second time in the native language is one option
@ -156,6 +156,9 @@ func (gui *Gui) handleCheckoutRef(ref string) error {
if err := gui.GitCommand.Checkout(ref, false); err != nil { if err := gui.GitCommand.Checkout(ref, false); err != nil {
return gui.createErrorPanel(g, err.Error()) return gui.createErrorPanel(g, err.Error())
} }
if onDone != nil {
onDone()
}
// checkout successful so we select the new branch // checkout successful so we select the new branch
gui.State.Panels.Branches.SelectedLine = 0 gui.State.Panels.Branches.SelectedLine = 0
@ -175,6 +178,10 @@ func (gui *Gui) handleCheckoutRef(ref string) error {
} }
} }
if onDone != nil {
onDone()
}
gui.State.Panels.Branches.SelectedLine = 0 gui.State.Panels.Branches.SelectedLine = 0
gui.State.Panels.Commits.SelectedLine = 0 gui.State.Panels.Commits.SelectedLine = 0
return gui.refreshSidePanels(gui.g) return gui.refreshSidePanels(gui.g)
@ -182,7 +189,7 @@ func (gui *Gui) handleCheckoutRef(ref string) error {
func (gui *Gui) handleCheckoutByName(g *gocui.Gui, v *gocui.View) error { func (gui *Gui) handleCheckoutByName(g *gocui.Gui, v *gocui.View) error {
return gui.createPromptPanel(g, v, gui.Tr.SLocalize("BranchName")+":", "", func(g *gocui.Gui, v *gocui.View) error { return gui.createPromptPanel(g, v, gui.Tr.SLocalize("BranchName")+":", "", func(g *gocui.Gui, v *gocui.View) error {
return gui.handleCheckoutRef(gui.trimmedContent(v)) return gui.handleCheckoutRef(gui.trimmedContent(v), nil)
}) })
} }

View File

@ -540,7 +540,7 @@ func (gui *Gui) handleCheckoutCommit(g *gocui.Gui, v *gocui.View) error {
} }
return gui.createConfirmationPanel(g, gui.getCommitsView(), true, gui.Tr.SLocalize("checkoutCommit"), gui.Tr.SLocalize("SureCheckoutThisCommit"), func(g *gocui.Gui, v *gocui.View) error { return gui.createConfirmationPanel(g, gui.getCommitsView(), true, gui.Tr.SLocalize("checkoutCommit"), gui.Tr.SLocalize("SureCheckoutThisCommit"), func(g *gocui.Gui, v *gocui.View) error {
return gui.handleCheckoutRef(commit.Sha) return gui.handleCheckoutRef(commit.Sha, nil)
}, nil) }, nil)
} }

View File

@ -214,6 +214,21 @@ type guiState struct {
PrevMainWidth int PrevMainWidth int
PrevMainHeight int PrevMainHeight int
OldInformation string OldInformation string
Undo UndoState
}
// we facilitate 'undo' actions via parsing the reflog and doing the reverse
// of the most recent entry. In order to to multiple undo's in a row we need to
// keep track of where we are in the reflog. We do that via a key which is the
// concatenation of the reflog's timestamp and message.
// We also store the index of that reflog entry so that if we end up with a
// different entry at that index we know the user must have done something
// themselves (e.g. checked out a branch) to cause new reflog entries to be created
// meaning we can reset the undo state.
type UndoState struct {
ReflogKey string
ReflogIdx int
} }
// for now the split view will always be on // for now the split view will always be on

View File

@ -85,7 +85,7 @@ func (gui *Gui) handleCheckoutReflogCommit(g *gocui.Gui, v *gocui.View) error {
} }
err := gui.createConfirmationPanel(g, gui.getCommitsView(), true, gui.Tr.SLocalize("checkoutCommit"), gui.Tr.SLocalize("SureCheckoutThisCommit"), func(g *gocui.Gui, v *gocui.View) error { err := gui.createConfirmationPanel(g, gui.getCommitsView(), true, gui.Tr.SLocalize("checkoutCommit"), gui.Tr.SLocalize("SureCheckoutThisCommit"), func(g *gocui.Gui, v *gocui.View) error {
return gui.handleCheckoutRef(commit.Sha) return gui.handleCheckoutRef(commit.Sha, nil)
}, nil) }, nil)
if err != nil { if err != nil {
return err return err
@ -104,29 +104,58 @@ func (gui *Gui) handleCreateReflogResetMenu(g *gocui.Gui, v *gocui.View) error {
type reflogAction struct { type reflogAction struct {
regexStr string regexStr string
action func(match []string, commitSha string, prevCommitSha string) (bool, error) action func(match []string, commitSha string, prevCommitSha string, onDone func()) (bool, error)
}
func (gui *Gui) reflogKey(reflogCommit *commands.Commit) string {
return reflogCommit.Date + reflogCommit.Name
}
func (gui *Gui) idxOfUndoReflogKey(key string) int {
for i, reflogCommit := range gui.State.ReflogCommits {
if gui.reflogKey(reflogCommit) == key {
return i
}
}
return -1
}
func (gui *Gui) setUndoReflogKey(key string) {
gui.State.Undo.ReflogKey = key
// adding one because this is called before we actually refresh the reflog on our end
// so the index will soon change.
gui.State.Undo.ReflogIdx = gui.idxOfUndoReflogKey(key) + 1
} }
func (gui *Gui) reflogUndo(g *gocui.Gui, v *gocui.View) error { func (gui *Gui) reflogUndo(g *gocui.Gui, v *gocui.View) error {
reflogCommits := gui.State.ReflogCommits
reflogActions := []reflogAction{ reflogActions := []reflogAction{
{ {
regexStr: `^checkout: moving from ([\S]+)`, regexStr: `^checkout: moving from ([\S]+)`,
action: func(match []string, commitSha string, prevCommitSha string) (bool, error) { action: func(match []string, commitSha string, prevCommitSha string, onDone func()) (bool, error) {
if len(match) <= 1 { if len(match) <= 1 {
return false, nil return false, nil
} }
return true, gui.handleCheckoutRef(match[1]) return true, gui.handleCheckoutRef(match[1], onDone)
}, },
}, },
{ {
regexStr: `^commit|^rebase -i \(start\)`, regexStr: `^commit|^rebase -i \(start\)`,
action: func(match []string, commitSha string, prevCommitSha string) (bool, error) { action: func(match []string, commitSha string, prevCommitSha string, onDone func()) (bool, error) {
return true, gui.handleHardResetWithAutoStash(prevCommitSha) return true, gui.handleHardResetWithAutoStash(prevCommitSha, onDone)
}, },
}, },
} }
for i, reflogCommit := range gui.State.ReflogCommits { // if the index of the previous reflog entry has changed, we need to start from the beginning, because it means there's been user input.
startIndex := gui.State.Undo.ReflogIdx
if gui.idxOfUndoReflogKey(gui.State.Undo.ReflogKey) != gui.State.Undo.ReflogIdx {
startIndex = 0
}
for offsetIdx, reflogCommit := range reflogCommits[startIndex:] {
i := offsetIdx + startIndex
for _, action := range reflogActions { for _, action := range reflogActions {
re := regexp.MustCompile(action.regexStr) re := regexp.MustCompile(action.regexStr)
match := re.FindStringSubmatch(reflogCommit.Name) match := re.FindStringSubmatch(reflogCommit.Name)
@ -134,24 +163,29 @@ func (gui *Gui) reflogUndo(g *gocui.Gui, v *gocui.View) error {
continue continue
} }
prevCommitSha := "" prevCommitSha := ""
if len(gui.State.ReflogCommits)-1 >= i+1 { if len(reflogCommits)-1 >= i+1 {
prevCommitSha = gui.State.ReflogCommits[i+1].Sha prevCommitSha = reflogCommits[i+1].Sha
} }
done, err := action.action(match, reflogCommit.Sha, prevCommitSha) nextKey := gui.reflogKey(gui.State.ReflogCommits[i+1])
if err != nil { onDone := func() {
return err gui.setUndoReflogKey(nextKey)
} }
if done {
return nil isMatchingAction, err := action.action(match, reflogCommit.Sha, prevCommitSha, onDone)
if !isMatchingAction {
continue
} }
return err
} }
} }
return nil return nil
} }
func (gui *Gui) handleHardResetWithAutoStash(commitSha string) error { // only to be used in the undo flow for now
func (gui *Gui) handleHardResetWithAutoStash(commitSha string, onDone func()) error {
// if we have any modified tracked files we need to ask the user if they want us to stash for them // if we have any modified tracked files we need to ask the user if they want us to stash for them
dirtyWorkingTree := false dirtyWorkingTree := false
for _, file := range gui.State.Files { for _, file := range gui.State.Files {
@ -170,6 +204,7 @@ func (gui *Gui) handleHardResetWithAutoStash(commitSha string) error {
if err := gui.resetToRef(commitSha, "hard"); err != nil { if err := gui.resetToRef(commitSha, "hard"); err != nil {
return gui.createErrorPanel(g, err.Error()) return gui.createErrorPanel(g, err.Error())
} }
onDone()
if err := gui.GitCommand.StashDo(0, "pop"); err != nil { if err := gui.GitCommand.StashDo(0, "pop"); err != nil {
if err := gui.refreshSidePanels(g); err != nil { if err := gui.refreshSidePanels(g); err != nil {
@ -184,6 +219,6 @@ func (gui *Gui) handleHardResetWithAutoStash(commitSha string) error {
if err := gui.resetToRef(commitSha, "hard"); err != nil { if err := gui.resetToRef(commitSha, "hard"); err != nil {
return gui.createErrorPanel(gui.g, err.Error()) return gui.createErrorPanel(gui.g, err.Error())
} }
onDone()
return gui.refreshSidePanels(gui.g) return gui.refreshSidePanels(gui.g)
} }

View File

@ -76,7 +76,7 @@ func (gui *Gui) handleCheckoutRemoteBranch(g *gocui.Gui, v *gocui.View) error {
if remoteBranch == nil { if remoteBranch == nil {
return nil return nil
} }
if err := gui.handleCheckoutRef(remoteBranch.RemoteName + "/" + remoteBranch.Name); err != nil { if err := gui.handleCheckoutRef(remoteBranch.RemoteName+"/"+remoteBranch.Name, nil); err != nil {
return err return err
} }
return gui.switchBranchesPanelContext("local-branches") return gui.switchBranchesPanelContext("local-branches")

View File

@ -81,7 +81,7 @@ func (gui *Gui) handleCheckoutTag(g *gocui.Gui, v *gocui.View) error {
if tag == nil { if tag == nil {
return nil return nil
} }
if err := gui.handleCheckoutRef(tag.Name); err != nil { if err := gui.handleCheckoutRef(tag.Name, nil); err != nil {
return err return err
} }
return gui.switchBranchesPanelContext("local-branches") return gui.switchBranchesPanelContext("local-branches")

View File

@ -81,7 +81,6 @@ func (gui *Gui) getManager(view *gocui.View) *tasks.ViewBufferManager {
}, },
func() { func() {
gui.g.Update(func(*gocui.Gui) error { gui.g.Update(func(*gocui.Gui) error {
gui.Log.Warn("updating view")
return nil return nil
}) })
}) })