From d44638130c8d07c648b45796cc6b0dff221c7d82 Mon Sep 17 00:00:00 2001 From: Jesse Duffield Date: Mon, 18 Feb 2019 23:27:54 +1100 Subject: add various interactive rebase commands --- pkg/app/app.go | 2 +- pkg/commands/git.go | 120 +++++++++++++++++++++++++++++++++++------------ pkg/commands/os.go | 16 +++++++ pkg/gui/commits_panel.go | 38 +++++++++++++-- pkg/gui/keybindings.go | 24 ++++++++++ pkg/gui/status_panel.go | 4 +- pkg/i18n/english.go | 13 +++++ 7 files changed, 178 insertions(+), 39 deletions(-) (limited to 'pkg') diff --git a/pkg/app/app.go b/pkg/app/app.go index e1f6654af..3b6c0fec2 100644 --- a/pkg/app/app.go +++ b/pkg/app/app.go @@ -121,7 +121,7 @@ func (app *App) Run() error { } func (app *App) Rebase() error { - app.Log.Error("TEST") + app.Log.Error("Lazygit invokved as interactive rebase demon") ioutil.WriteFile(".git/rebase-merge/git-rebase-todo", []byte(os.Getenv("LAZYGIT_REBASE_TODO")), 0644) diff --git a/pkg/commands/git.go b/pkg/commands/git.go index 51e943ba6..8bc1f0913 100644 --- a/pkg/commands/git.go +++ b/pkg/commands/git.go @@ -276,7 +276,7 @@ func (c *GitCommand) RebaseBranch(onto string) error { return err } - return c.OSCommand.RunCommand(fmt.Sprintf("git rebase %s %s ", onto, curBranch)) + return c.OSCommand.RunCommand(fmt.Sprintf("git rebase --autoStash %s %s ", onto, curBranch)) } // Fetch fetch git repo @@ -462,12 +462,22 @@ func (c *GitCommand) IsInMergeState() (bool, error) { return strings.Contains(output, "conclude merge") || strings.Contains(output, "unmerged paths"), nil } -func (c *GitCommand) IsInRebaseState() (bool, error) { +// RebaseMode returns "" for non-rebase mode, "normal" for normal rebase +// and "interactive" for interactive rebase +func (c *GitCommand) RebaseMode() (string, error) { exists, err := c.OSCommand.FileExists(".git/rebase-apply") if err != nil { - return false, err + return "", err + } + if exists { + return "normal", nil + } + exists, err = c.OSCommand.FileExists(".git/rebase-merge") + if exists { + return "interactive", err + } else { + return "", err } - return exists, nil } // RemoveFile directly @@ -537,14 +547,21 @@ func (c *GitCommand) getMergeBase() (string, error) { // GetRebasingCommits obtains the commits that we're in the process of rebasing func (c *GitCommand) GetRebasingCommits() ([]*Commit, error) { - rebasing, err := c.IsInRebaseState() + rebaseMode, err := c.RebaseMode() if err != nil { return nil, err } - if !rebasing { + switch rebaseMode { + case "normal": + return c.GetNormalRebasingCommits() + case "interactive": + return c.GetInteractiveRebasingCommits() + default: return nil, nil } +} +func (c *GitCommand) GetNormalRebasingCommits() ([]*Commit, error) { rewrittenCount := 0 bytesContent, err := ioutil.ReadFile(".git/rebase-apply/rewritten") if err == nil { @@ -585,6 +602,10 @@ func (c *GitCommand) GetRebasingCommits() ([]*Commit, error) { return commits, nil } +func (c *GitCommand) GetInteractiveRebasingCommits() ([]*Commit, error) { + return nil, nil +} + // assuming the file starts like this: // From e93d4193e6dd45ca9cf3a5a273d7ba6cd8b8fb20 Mon Sep 17 00:00:00 2001 // From: Lazygit Tester @@ -780,27 +801,55 @@ func (c *GitCommand) GenericMerge(commandType string, command string) error { return c.OSCommand.RunCommand(gitCommand) } -func (c *GitCommand) InteractiveRebase(commits []*Commit, index int, action string) (*exec.Cmd, error) { - ex, err := os.Executable() // get the executable path for git to use +func (c *GitCommand) RewordCommit(commits []*Commit, index int) (*exec.Cmd, error) { + todo, err := c.GenerateGenericRebaseTodo(commits, index, "reword") if err != nil { - ex = os.Args[0] // fallback to the first call argument if needed + return nil, err } - // assume for now they won't pick the bottom commit - c.Log.Warn(len(commits)) - c.Log.Warn(index) - if len(commits) <= index+1 { + return c.PrepareInteractiveRebaseCommand(commits[index+1].Sha, todo) +} + +func (c *GitCommand) MoveCommitDown(commits []*Commit, index int) error { + // we must ensure that we have at least two commits after the selected one + if len(commits) <= index+2 { + // assuming they aren't picking the bottom commit // TODO: support more than say 30 commits and ensure this logic is correct, and i18n - return nil, errors.New("You cannot interactive rebase onto the first commit") + return errors.New("Not enough room") } todo := "" - for i, commit := range commits[0 : index+1] { - a := "pick" - if i == index { - a = action - } - todo += a + " " + commit.Sha + "\n" + orderedCommits := append(commits[0:index], commits[index+1], commits[index]) + for _, commit := range orderedCommits { + todo = "pick " + commit.Sha + "\n" + todo + } + + cmd, err := c.PrepareInteractiveRebaseCommand(commits[index+2].Sha, todo) + if err != nil { + return err + } + + return c.OSCommand.RunPreparedCommand(cmd) +} + +func (c *GitCommand) InteractiveRebase(commits []*Commit, index int, action string) error { + todo, err := c.GenerateGenericRebaseTodo(commits, index, action) + if err != nil { + return err + } + + cmd, err := c.PrepareInteractiveRebaseCommand(commits[index+1].Sha, todo) + if err != nil { + return err + } + + return c.OSCommand.RunPreparedCommand(cmd) +} + +func (c *GitCommand) PrepareInteractiveRebaseCommand(baseSha string, todo string) (*exec.Cmd, error) { + ex, err := os.Executable() // get the executable path for git to use + if err != nil { + ex = os.Args[0] // fallback to the first call argument if needed } debug := "FALSE" @@ -808,7 +857,7 @@ func (c *GitCommand) InteractiveRebase(commits []*Commit, index int, action stri debug = "TRUE" } - splitCmd := str.ToArgv(fmt.Sprintf("git rebase --interactive %s", commits[index+1].Sha)) + splitCmd := str.ToArgv(fmt.Sprintf("git rebase --autoStash --interactive %s", baseSha)) cmd := exec.Command(splitCmd[0], splitCmd[1:]...) @@ -823,18 +872,27 @@ func (c *GitCommand) InteractiveRebase(commits []*Commit, index int, action stri "GIT_SEQUENCE_EDITOR="+ex, ) - if true { - return cmd, nil + return cmd, nil +} + +func (c *GitCommand) HardReset(baseSha string) error { + return c.OSCommand.RunCommand("git reset --hard " + baseSha) +} + +func (v *GitCommand) GenerateGenericRebaseTodo(commits []*Commit, index int, action string) (string, error) { + if len(commits) <= index+1 { + // assuming they aren't picking the bottom commit + // TODO: support more than say 30 commits and ensure this logic is correct, and i18n + return "", errors.New("You cannot interactive rebase onto the first commit") } - out, err := cmd.CombinedOutput() - outString := string(out) - c.Log.Info(outString) - if err != nil { - if len(outString) == 0 { - return nil, err + todo := "" + for i, commit := range commits[0 : index+1] { + a := "pick" + if i == index { + a = action } - return nil, errors.New(outString) + todo = a + " " + commit.Sha + "\n" + todo } - return nil, nil + return todo, nil } diff --git a/pkg/commands/os.go b/pkg/commands/os.go index c404f4de1..e5ae0a041 100644 --- a/pkg/commands/os.go +++ b/pkg/commands/os.go @@ -247,3 +247,19 @@ func (c *OSCommand) FileExists(path string) (bool, error) { } return true, nil } + +// RunPreparedCommand takes a pointer to an exec.Cmd and runs it +// this is useful if you need to give your command some environment variables +// before running it +func (c *OSCommand) RunPreparedCommand(cmd *exec.Cmd) error { + out, err := cmd.CombinedOutput() + outString := string(out) + c.Log.Info(outString) + if err != nil { + if len(outString) == 0 { + return err + } + return errors.New(outString) + } + return nil +} diff --git a/pkg/gui/commits_panel.go b/pkg/gui/commits_panel.go index 4bf0d4362..374f7ccf0 100644 --- a/pkg/gui/commits_panel.go +++ b/pkg/gui/commits_panel.go @@ -181,17 +181,45 @@ func (gui *Gui) handleRenameCommit(g *gocui.Gui, v *gocui.View) error { } func (gui *Gui) handleRenameCommitEditor(g *gocui.Gui, v *gocui.View) error { - subProcess, err := gui.GitCommand.InteractiveRebase(gui.State.Commits, gui.State.Panels.Commits.SelectedLine, "reword") + subProcess, err := gui.GitCommand.RewordCommit(gui.State.Commits, gui.State.Panels.Commits.SelectedLine) if err != nil { - return err + return gui.createErrorPanel(gui.g, err.Error()) } if subProcess != nil { gui.SubProcess = subProcess - // g.Update(func(g *gocui.Gui) error { - // return gui.Errors.ErrSubProcess - // }) return gui.Errors.ErrSubProcess } return nil } + +func (gui *Gui) handleCommitDelete(g *gocui.Gui, v *gocui.View) error { + // TODO: i18n + return gui.createConfirmationPanel(gui.g, v, "Delete Commit", "Are you sure you want to delete this commit?", func(*gocui.Gui, *gocui.View) error { + err := gui.GitCommand.InteractiveRebase(gui.State.Commits, gui.State.Panels.Commits.SelectedLine, "drop") + return gui.handleGenericMergeCommandResult(err) + }, nil) +} + +func (gui *Gui) handleCommitMoveDown(g *gocui.Gui, v *gocui.View) error { + gui.State.Panels.Commits.SelectedLine++ + + err := gui.GitCommand.MoveCommitDown(gui.State.Commits, gui.State.Panels.Commits.SelectedLine-1) + return gui.handleGenericMergeCommandResult(err) +} + +func (gui *Gui) handleCommitMoveUp(g *gocui.Gui, v *gocui.View) error { + if gui.State.Panels.Commits.SelectedLine == 0 { + return gui.createErrorPanel(gui.g, "You cannot move the topmost commit up") // TODO: i18n + } + + gui.State.Panels.Commits.SelectedLine-- + + err := gui.GitCommand.MoveCommitDown(gui.State.Commits, gui.State.Panels.Commits.SelectedLine) + return gui.handleGenericMergeCommandResult(err) +} + +func (gui *Gui) handleCommitEdit(g *gocui.Gui, v *gocui.View) error { + err := gui.GitCommand.InteractiveRebase(gui.State.Commits, gui.State.Panels.Commits.SelectedLine, "edit") + return gui.handleGenericMergeCommandResult(err) +} diff --git a/pkg/gui/keybindings.go b/pkg/gui/keybindings.go index df6bd8ee3..c0becfe2b 100644 --- a/pkg/gui/keybindings.go +++ b/pkg/gui/keybindings.go @@ -310,6 +310,30 @@ func (gui *Gui) GetInitialKeybindings() []*Binding { Modifier: gocui.ModNone, Handler: gui.handleCommitFixup, Description: gui.Tr.SLocalize("fixupCommit"), + }, { + ViewName: "commits", + Key: 'd', + Modifier: gocui.ModNone, + Handler: gui.handleCommitDelete, + Description: gui.Tr.SLocalize("deleteCommit"), + }, { + ViewName: "commits", + Key: 'J', + Modifier: gocui.ModNone, + Handler: gui.handleCommitMoveDown, + Description: gui.Tr.SLocalize("moveDownCommit"), + }, { + ViewName: "commits", + Key: 'K', + Modifier: gocui.ModNone, + Handler: gui.handleCommitMoveUp, + Description: gui.Tr.SLocalize("moveUpCommit"), + }, { + ViewName: "commits", + Key: 'e', + Modifier: gocui.ModNone, + Handler: gui.handleCommitEdit, + Description: gui.Tr.SLocalize("editCommit"), }, { ViewName: "stash", Key: gocui.KeySpace, diff --git a/pkg/gui/status_panel.go b/pkg/gui/status_panel.go index 06b94d722..a6bed8f83 100644 --- a/pkg/gui/status_panel.go +++ b/pkg/gui/status_panel.go @@ -94,11 +94,11 @@ func (gui *Gui) updateWorkTreeState() error { gui.State.WorkingTreeState = "merging" return nil } - rebasing, err := gui.GitCommand.IsInRebaseState() + rebaseMode, err := gui.GitCommand.RebaseMode() if err != nil { return err } - if rebasing { + if rebaseMode != "" { gui.State.WorkingTreeState = "rebasing" return nil } diff --git a/pkg/i18n/english.go b/pkg/i18n/english.go index d7b509861..5fef83176 100644 --- a/pkg/i18n/english.go +++ b/pkg/i18n/english.go @@ -283,6 +283,19 @@ func addEnglish(i18nObject *i18n.Bundle) error { ID: "renameCommit", Other: "rename commit", }, &i18n.Message{ + + ID: "deleteCommit", + Other: "delete commit", // TODO: other languages + }, &i18n.Message{ + ID: "moveDownCommit", + Other: "move commit down one", // TODO: other languages + }, &i18n.Message{ + ID: "moveUpCommit", + Other: "move commit up one", // TODO: other languages + }, &i18n.Message{ + ID: "editCommit", + Other: "edit commit", // TODO: other languages + }, &i18n.Message{ ID: "renameCommitEditor", Other: "rename commit with editor", }, &i18n.Message{ -- cgit v1.2.3