diff options
author | Jesse Duffield <jessedduffield@gmail.com> | 2019-02-19 23:36:29 +1100 |
---|---|---|
committer | Jesse Duffield <jessedduffield@gmail.com> | 2019-02-19 23:36:36 +1100 |
commit | 0228e250847d042730038281f435b0b21d992c42 (patch) | |
tree | 9f15ef6444c6215852a301a3286dcddf8a2b0cb3 | |
parent | 935f77483443a12ab159e19a958dfdf61a947b36 (diff) |
work towards more interactive rebase options
-rw-r--r-- | pkg/app/app.go | 17 | ||||
-rw-r--r-- | pkg/commands/commit.go | 10 | ||||
-rw-r--r-- | pkg/commands/git.go | 99 | ||||
-rw-r--r-- | pkg/commands/git_test.go | 64 | ||||
-rw-r--r-- | pkg/git/commit_list_builder.go | 72 | ||||
-rw-r--r-- | pkg/gui/commits_panel.go | 125 | ||||
-rw-r--r-- | pkg/gui/files_panel.go | 13 | ||||
-rw-r--r-- | pkg/gui/keybindings.go | 40 | ||||
-rw-r--r-- | pkg/i18n/english.go | 26 |
9 files changed, 264 insertions, 202 deletions
diff --git a/pkg/app/app.go b/pkg/app/app.go index 3b6c0fec2..d85d007e5 100644 --- a/pkg/app/app.go +++ b/pkg/app/app.go @@ -5,6 +5,7 @@ import ( "io/ioutil" "os" "path/filepath" + "strings" "github.com/heroku/rollrus" "github.com/jesseduffield/lazygit/pkg/commands" @@ -120,10 +121,20 @@ func (app *App) Run() error { return app.Gui.RunWithSubprocesses() } +// Rebase contains logic for when we've been run in demon mode, meaning we've +// given lazygit as a command for git to call e.g. to edit a file func (app *App) Rebase() error { - app.Log.Error("Lazygit invokved as interactive rebase demon") - - ioutil.WriteFile(".git/rebase-merge/git-rebase-todo", []byte(os.Getenv("LAZYGIT_REBASE_TODO")), 0644) + app.Log.Info("Lazygit invoked as interactive rebase demon") + app.Log.Info("args: ", os.Args) + + if strings.HasSuffix(os.Args[1], "git-rebase-todo") { + ioutil.WriteFile(os.Args[1], []byte(os.Getenv("LAZYGIT_REBASE_TODO")), 0644) + } else if strings.HasSuffix(os.Args[1], ".git/COMMIT_EDITMSG") { + // if we are rebasing and squashing, we'll see a COMMIT_EDITMSG + // but in this case we don't need to edit it, so we'll just return + } else { + app.Log.Info("Lazygit demon did not match on any use cases") + } return nil } diff --git a/pkg/commands/commit.go b/pkg/commands/commit.go index 6ae6acd02..1df6afbd0 100644 --- a/pkg/commands/commit.go +++ b/pkg/commands/commit.go @@ -2,6 +2,7 @@ package commands import ( "github.com/fatih/color" + "github.com/jesseduffield/lazygit/pkg/utils" ) // Commit : A git commit @@ -10,6 +11,7 @@ type Commit struct { Name string Status string // one of "unpushed", "pushed", "merged", or "rebasing" DisplayString string + Action string // one of "", "pick", "edit", "squash", "reword", "drop", "fixup" } // GetDisplayStrings is a function. @@ -19,6 +21,7 @@ func (c *Commit) GetDisplayStrings(isFocused bool) []string { green := color.New(color.FgGreen) white := color.New(color.FgWhite) blue := color.New(color.FgBlue) + cyan := color.New(color.FgCyan) var shaColor *color.Color switch c.Status { @@ -34,5 +37,10 @@ func (c *Commit) GetDisplayStrings(isFocused bool) []string { shaColor = white } - return []string{shaColor.Sprint(c.Sha), white.Sprint(c.Name)} + actionString := "" + if c.Action != "" { + actionString = cyan.Sprint(utils.WithPadding(c.Action, 7)) + " " + } + + return []string{shaColor.Sprint(c.Sha), actionString + white.Sprint(c.Name)} } diff --git a/pkg/commands/git.go b/pkg/commands/git.go index 845b81ff2..fa6e4c5fe 100644 --- a/pkg/commands/git.go +++ b/pkg/commands/git.go @@ -2,6 +2,7 @@ package commands import ( "fmt" + "io/ioutil" "os" "os/exec" "strings" @@ -360,46 +361,6 @@ func (c *GitCommand) Push(branchName string, force bool, ask func(string) string return c.OSCommand.DetectUnamePass(cmd, ask) } -// SquashPreviousTwoCommits squashes a commit down to the one below it -// retaining the message of the higher commit -func (c *GitCommand) SquashPreviousTwoCommits(message string) error { - // TODO: test this - if err := c.OSCommand.RunCommand("git reset --soft HEAD^"); err != nil { - return err - } - // TODO: if password is required, we need to return a subprocess - return c.OSCommand.RunCommand(fmt.Sprintf("git commit --amend -m %s", c.OSCommand.Quote(message))) -} - -// SquashFixupCommit squashes a 'FIXUP' commit into the commit beneath it, -// retaining the commit message of the lower commit -func (c *GitCommand) SquashFixupCommit(branchName string, shaValue string) error { - commands := []string{ - fmt.Sprintf("git checkout -q %s", shaValue), - fmt.Sprintf("git reset --soft %s^", shaValue), - fmt.Sprintf("git commit --amend -C %s^", shaValue), - fmt.Sprintf("git rebase --onto HEAD %s %s", shaValue, branchName), - } - for _, command := range commands { - c.Log.Info(command) - - if output, err := c.OSCommand.RunCommandWithOutput(command); err != nil { - ret := output - // We are already in an error state here so we're just going to append - // the output of these commands - output, _ := c.OSCommand.RunCommandWithOutput(fmt.Sprintf("git branch -d %s", shaValue)) - ret += output - output, _ = c.OSCommand.RunCommandWithOutput(fmt.Sprintf("git checkout %s", branchName)) - ret += output - - c.Log.Info(ret) - return errors.New(ret) - } - } - - return nil -} - // CatFile obtains the content of a file func (c *GitCommand) CatFile(fileName string) (string, error) { return c.OSCommand.RunCommandWithOutput(fmt.Sprintf("cat %s", c.OSCommand.Quote(fileName))) @@ -606,7 +567,7 @@ func (c *GitCommand) FastForward(branchName string) error { return c.OSCommand.RunCommand(fmt.Sprintf("git fetch %s %s:%s", upstream, branchName, branchName)) } -// GenericMerge takes a commandType of "merging" or "rebasing" and a command of "abort", "skip" or "continue" +// GenericMerge takes a commandType of "merge" or "rebase" and a command of "abort", "skip" or "continue" // By default we skip the editor in the case where a commit will be made func (c *GitCommand) GenericMerge(commandType string, command string) error { gitCommand := fmt.Sprintf("git %s %s --%s", c.OSCommand.Platform.skipEditorArg, commandType, command) @@ -619,7 +580,7 @@ func (c *GitCommand) RewordCommit(commits []*Commit, index int) (*exec.Cmd, erro return nil, err } - return c.PrepareInteractiveRebaseCommand(commits[index+1].Sha, todo, true) + return c.PrepareInteractiveRebaseCommand(commits[index+1].Sha, todo, false) } func (c *GitCommand) MoveCommitDown(commits []*Commit, index int) error { @@ -633,7 +594,7 @@ func (c *GitCommand) MoveCommitDown(commits []*Commit, index int) error { todo := "" orderedCommits := append(commits[0:index], commits[index+1], commits[index]) for _, commit := range orderedCommits { - todo = "pick " + commit.Sha + "\n" + todo + todo = "pick " + commit.Sha + " " + commit.Name + "\n" + todo } cmd, err := c.PrepareInteractiveRebaseCommand(commits[index+2].Sha, todo, true) @@ -650,7 +611,6 @@ func (c *GitCommand) InteractiveRebase(commits []*Commit, index int, action stri return err } - // TODO: decide whether to autostash when action == editing cmd, err := c.PrepareInteractiveRebaseCommand(commits[index+1].Sha, todo, true) if err != nil { return err @@ -659,7 +619,7 @@ func (c *GitCommand) InteractiveRebase(commits []*Commit, index int, action stri return c.OSCommand.RunPreparedCommand(cmd) } -func (c *GitCommand) PrepareInteractiveRebaseCommand(baseSha string, todo string, autoStash bool) (*exec.Cmd, error) { +func (c *GitCommand) PrepareInteractiveRebaseCommand(baseSha string, todo string, overrideEditor bool) (*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 @@ -670,12 +630,7 @@ func (c *GitCommand) PrepareInteractiveRebaseCommand(baseSha string, todo string debug = "TRUE" } - autoStashFlag := "" - if autoStash { - autoStashFlag = "--autostash" - } - - splitCmd := str.ToArgv(fmt.Sprintf("git rebase --interactive %s %s", autoStashFlag, baseSha)) + splitCmd := str.ToArgv(fmt.Sprintf("git rebase --interactive --autostash %s", baseSha)) cmd := exec.Command(splitCmd[0], splitCmd[1:]...) @@ -690,6 +645,10 @@ func (c *GitCommand) PrepareInteractiveRebaseCommand(baseSha string, todo string "GIT_SEQUENCE_EDITOR="+ex, ) + if overrideEditor { + cmd.Env = append(cmd.Env, "EDITOR="+ex) + } + return cmd, nil } @@ -697,7 +656,11 @@ 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) { +func (c *GitCommand) SoftReset(baseSha string) error { + return c.OSCommand.RunCommand("git reset --soft " + baseSha) +} + +func (c *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 @@ -710,7 +673,37 @@ func (v *GitCommand) GenerateGenericRebaseTodo(commits []*Commit, index int, act if i == index { a = action } - todo = a + " " + commit.Sha + "\n" + todo + todo = a + " " + commit.Sha + " " + commit.Name + "\n" + todo } return todo, nil } + +// AmendTo amends the given commit with whatever files are staged +func (c *GitCommand) AmendTo(sha string) error { + if err := c.OSCommand.RunCommand(fmt.Sprintf("git commit --fixup=%s", sha)); err != nil { + return err + } + return c.OSCommand.RunCommand(fmt.Sprintf("git %s rebase --interactive --autostash --autosquash %s^", c.OSCommand.Platform.skipEditorArg, sha)) +} + +func (c *GitCommand) EditRebaseTodo(index int, action string) error { + fileName := ".git/rebase-merge/git-rebase-todo" + bytes, err := ioutil.ReadFile(fileName) + if err != nil { + return err + } + + content := strings.Split(string(bytes), "\n") + + contentIndex := len(content) - 2 - index + splitLine := strings.Split(content[contentIndex], " ") + content[contentIndex] = action + " " + strings.Join(splitLine[1:], " ") + result := strings.Join(content, "\n") + + return ioutil.WriteFile(fileName, []byte(result), 0644) +} + +// Revert reverts the selected commit by sha +func (c *GitCommand) Revert(sha string) error { + return c.OSCommand.RunCommand(fmt.Sprintf("git revert %s", sha)) +} diff --git a/pkg/commands/git_test.go b/pkg/commands/git_test.go index 4159683ca..5af2bfe51 100644 --- a/pkg/commands/git_test.go +++ b/pkg/commands/git_test.go @@ -1152,70 +1152,6 @@ func TestGitCommandSquashPreviousTwoCommits(t *testing.T) { } } -// TestGitCommandSquashFixupCommit is a function. -func TestGitCommandSquashFixupCommit(t *testing.T) { - type scenario struct { - testName string - command func() (func(string, ...string) *exec.Cmd, *[][]string) - test func(*[][]string, error) - } - - scenarios := []scenario{ - { - "An error occurred with one of the sub git command", - func() (func(string, ...string) *exec.Cmd, *[][]string) { - cmdsCalled := [][]string{} - return func(cmd string, args ...string) *exec.Cmd { - cmdsCalled = append(cmdsCalled, args) - if len(args) > 0 && args[0] == "checkout" { - return exec.Command("test") - } - - return exec.Command("echo") - }, &cmdsCalled - }, - func(cmdsCalled *[][]string, err error) { - assert.NotNil(t, err) - assert.Len(t, *cmdsCalled, 3) - assert.EqualValues(t, *cmdsCalled, [][]string{ - {"checkout", "-q", "6789abcd"}, - {"branch", "-d", "6789abcd"}, - {"checkout", "test"}, - }) - }, - }, - { - "Squash fixup succeeded", - func() (func(string, ...string) *exec.Cmd, *[][]string) { - cmdsCalled := [][]string{} - return func(cmd string, args ...string) *exec.Cmd { - cmdsCalled = append(cmdsCalled, args) - return exec.Command("echo") - }, &cmdsCalled - }, - func(cmdsCalled *[][]string, err error) { - assert.Nil(t, err) - assert.Len(t, *cmdsCalled, 4) - assert.EqualValues(t, *cmdsCalled, [][]string{ - {"checkout", "-q", "6789abcd"}, - {"reset", "--soft", "6789abcd^"}, - {"commit", "--amend", "-C", "6789abcd^"}, - {"rebase", "--onto", "HEAD", "6789abcd", "test"}, - }) - }, - }, - } - - for _, s := range scenarios { - t.Run(s.testName, func(t *testing.T) { - var cmdsCalled *[][]string - gitCmd := newDummyGitCommand() - gitCmd.OSCommand.command, cmdsCalled = s.command() - s.test(cmdsCalled, gitCmd.SquashFixupCommit("test", "6789abcd")) - }) - } -} - // TestGitCommandCatFile is a function. func TestGitCommandCatFile(t *testing.T) { gitCmd := newDummyGitCommand() diff --git a/pkg/git/commit_list_builder.go b/pkg/git/commit_list_builder.go index 703dbd990..df44908a7 100644 --- a/pkg/git/commit_list_builder.go +++ b/pkg/git/commit_list_builder.go @@ -42,13 +42,20 @@ func NewCommitListBuilder(log *logrus.Entry, gitCommand *commands.GitCommand, os // GetCommits obtains the commits of the current branch func (c *CommitListBuilder) GetCommits() ([]*commands.Commit, error) { commits := []*commands.Commit{} - // here we want to also prepend the commits that we're in the process of rebasing - rebasingCommits, err := c.getRebasingCommits() + var rebasingCommits []*commands.Commit + rebaseMode, err := c.GitCommand.RebaseMode() if err != nil { return nil, err } - if len(rebasingCommits) > 0 { - commits = append(commits, rebasingCommits...) + if rebaseMode != "" { + // here we want to also prepend the commits that we're in the process of rebasing + rebasingCommits, err = c.getRebasingCommits(rebaseMode) + if err != nil { + return nil, err + } + if len(rebasingCommits) > 0 { + commits = append(commits, rebasingCommits...) + } } unpushedCommits := c.getUnpushedCommits() @@ -67,7 +74,7 @@ func (c *CommitListBuilder) GetCommits() ([]*commands.Commit, error) { DisplayString: strings.Join(splitLine, " "), }) } - if len(rebasingCommits) > 0 { + if rebaseMode != "" { currentCommit := commits[len(rebasingCommits)] blue := color.New(color.FgYellow) youAreHere := blue.Sprint("<-- YOU ARE HERE ---") @@ -77,11 +84,7 @@ func (c *CommitListBuilder) GetCommits() ([]*commands.Commit, error) { } // getRebasingCommits obtains the commits that we're in the process of rebasing -func (c *CommitListBuilder) getRebasingCommits() ([]*commands.Commit, error) { - rebaseMode, err := c.GitCommand.RebaseMode() - if err != nil { - return nil, err - } +func (c *CommitListBuilder) getRebasingCommits(rebaseMode string) ([]*commands.Commit, error) { switch rebaseMode { case "normal": return c.getNormalRebasingCommits() @@ -147,45 +150,28 @@ func (c *CommitListBuilder) getNormalRebasingCommits() ([]*commands.Commit, erro // in the rebase: func (c *CommitListBuilder) getInteractiveRebasingCommits() ([]*commands.Commit, error) { bytesContent, err := ioutil.ReadFile(".git/rebase-merge/git-rebase-todo") - var content []string - if err == nil { - content = strings.Split(string(bytesContent), "\n") - if len(content) > 0 && content[len(content)-1] == "" { - content = content[0 : len(content)-1] - } - } - - // for each of them, grab the matching commit name in the backup - bytesContent, err = ioutil.ReadFile(".git/rebase-merge/git-rebase-todo.backup") - var backupContent []string - if err == nil { - backupContent = strings.Split(string(bytesContent), "\n") + if err != nil { + c.Log.Info(fmt.Sprintf("error occured reading git-rebase-todo: %s", err.Error())) + // we assume an error means the file doesn't exist so we just return + return nil, nil } commits := []*commands.Commit{} - for _, todoLine := range content { - commit := c.extractCommit(todoLine, backupContent) - if commit != nil { - commits = append([]*commands.Commit{commit}, commits...) + lines := strings.Split(string(bytesContent), "\n") + for _, line := range lines { + if line == "" { + return commits, nil } + splitLine := strings.Split(line, " ") + commits = append([]*commands.Commit{&commands.Commit{ + Sha: splitLine[1][0:7], + Name: strings.Join(splitLine[2:], " "), + Status: "rebasing", + Action: splitLine[0], + }}, commits...) } - return commits, nil -} - -func (c *CommitListBuilder) extractCommit(todoLine string, backupContent []string) *commands.Commit { - for _, backupLine := range backupContent { - split := strings.Split(todoLine, " ") - prefix := strings.Join(split[0:2], " ") - if strings.HasPrefix(backupLine, prefix) { - return &commands.Commit{ - Sha: split[2], - Name: strings.TrimPrefix(backupLine, prefix+" "), - Status: "rebasing", - } - } - } - return nil + return nil, nil } // assuming the file starts like this: diff --git a/pkg/gui/commits_panel.go b/pkg/gui/commits_panel.go index f1d443731..a68b71bb1 100644 --- a/pkg/gui/commits_panel.go +++ b/pkg/gui/commits_panel.go @@ -115,24 +115,23 @@ func (gui *Gui) handleResetToCommit(g *gocui.Gui, commitView *gocui.View) error } func (gui *Gui) handleCommitSquashDown(g *gocui.Gui, v *gocui.View) error { - if gui.State.Panels.Commits.SelectedLine != 0 { - return gui.createErrorPanel(g, gui.Tr.SLocalize("OnlySquashTopmostCommit")) - } if len(gui.State.Commits) <= 1 { return gui.createErrorPanel(g, gui.Tr.SLocalize("YouNoCommitsToSquash")) } - commit := gui.getSelectedCommit(g) - if commit == nil { - return errors.New(gui.Tr.SLocalize("NoCommitsThisBranch")) - } - if err := gui.GitCommand.SquashPreviousTwoCommits(commit.Name); err != nil { - return gui.createErrorPanel(g, err.Error()) + + applied, err := gui.handleMidRebaseCommand("squash") + if err != nil { + return err } - if err := gui.refreshCommits(g); err != nil { - panic(err) + if applied { + return nil } - gui.refreshStatus(g) - return gui.handleCommitSelect(g, v) + + gui.createConfirmationPanel(g, v, gui.Tr.SLocalize("Squash"), gui.Tr.SLocalize("SureSquashThisCommit"), func(g *gocui.Gui, v *gocui.View) error { + err := gui.GitCommand.InteractiveRebase(gui.State.Commits, gui.State.Panels.Commits.SelectedLine, "squash") + return gui.handleGenericMergeCommandResult(err) + }, nil) + return nil } // TODO: move to files panel @@ -149,28 +148,31 @@ func (gui *Gui) handleCommitFixup(g *gocui.Gui, v *gocui.View) error { if len(gui.State.Commits) <= 1 { return gui.createErrorPanel(g, gui.Tr.SLocalize("YouNoCommitsToSquash")) } - if gui.anyUnStagedChanges(gui.State.Files) { - return gui.createErrorPanel(g, gui.Tr.SLocalize("CantFixupWhileUnstagedChanges")) + + applied, err := gui.handleMidRebaseCommand("fixup") + if err != nil { + return err } - branch := gui.State.Branches[0] - commit := gui.getSelectedCommit(g) - if commit == nil { - return gui.createErrorPanel(g, gui.Tr.SLocalize("NoCommitsThisBranch")) + if applied { + return nil } - message := gui.Tr.SLocalize("SureFixupThisCommit") - gui.createConfirmationPanel(g, v, gui.Tr.SLocalize("Fixup"), message, func(g *gocui.Gui, v *gocui.View) error { - if err := gui.GitCommand.SquashFixupCommit(branch.Name, commit.Sha); err != nil { - return gui.createErrorPanel(g, err.Error()) - } - if err := gui.refreshCommits(g); err != nil { - panic(err) - } - return gui.refreshStatus(g) + + gui.createConfirmationPanel(g, v, gui.Tr.SLocalize("Fixup"), gui.Tr.SLocalize("SureFixupThisCommit"), func(g *gocui.Gui, v *gocui.View) error { + err := gui.GitCommand.InteractiveRebase(gui.State.Commits, gui.State.Panels.Commits.SelectedLine, "fixup") + return gui.handleGenericMergeCommandResult(err) }, nil) return nil } func (gui *Gui) handleRenameCommit(g *gocui.Gui, v *gocui.View) error { + applied, err := gui.handleMidRebaseCommand("reword") + if err != nil { + return err + } + if applied { + return nil + } + if gui.State.Panels.Commits.SelectedLine != 0 { return gui.createErrorPanel(g, gui.Tr.SLocalize("OnlyRenameTopCommit")) } @@ -186,6 +188,14 @@ func (gui *Gui) handleRenameCommit(g *gocui.Gui, v *gocui.View) error { } func (gui *Gui) handleRenameCommitEditor(g *gocui.Gui, v *gocui.View) error { + applied, err := gui.handleMidRebaseCommand("reword") + if err != nil { + return err + } + if applied { + return nil + } + subProcess, err := gui.GitCommand.RewordCommit(gui.State.Commits, gui.State.Panels.Commits.SelectedLine) if err != nil { return gui.createErrorPanel(gui.g, err.Error()) @@ -198,7 +208,29 @@ func (gui *Gui) handleRenameCommitEditor(g *gocui.Gui, v *gocui.View) error { return nil } +// handleMidRebaseCommand sees if the selected commit is in fact a rebasing +// commit meaning you are trying to edit the todo file rather than actually +// begin a rebase. It then updates the todo file with that action +func (gui *Gui) handleMidRebaseCommand(action string) (bool, error) { + selectedCommit := gui.State.Commits[gui.State.Panels.Commits.SelectedLine] + if selectedCommit.Status != "rebasing" { + return false, nil + } + if err := gui.GitCommand.EditRebaseTodo(gui.State.Panels.Commits.SelectedLine, action); err != nil { + return false, gui.createErrorPanel(gui.g, err.Error()) + } + return true, gui.refreshCommits(gui.g) +} + func (gui *Gui) handleCommitDelete(g *gocui.Gui, v *gocui.View) error { + applied, err := gui.handleMidRebaseCommand("drop") + if err != nil { + return err + } + if applied { + return nil + } + // 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") @@ -225,6 +257,41 @@ func (gui *Gui) handleCommitMoveUp(g *gocui.Gui, v *gocui.View) error { } func (gui *Gui) handleCommitEdit(g *gocui.Gui, v *gocui.View) error { - err := gui.GitCommand.InteractiveRebase(gui.State.Commits, gui.State.Panels.Commits.SelectedLine, "edit") + applied, err := gui.handleMidRebaseCommand("edit") + if err != nil { + return err + } + if applied { + return nil + } + + err = gui.GitCommand.InteractiveRebase(gui.State.Commits, gui.State.Panels.Commits.SelectedLine, "edit") return gui.handleGenericMergeCommandResult(err) } + +func (gui *Gui) handleCommitAmendTo(g *gocui.Gui, v *gocui.View) error { + err := gui.GitCommand.AmendTo(gui.State.Commits[gui.State.Panels.Commits.SelectedLine].Sha) + return gui.handleGenericMergeCommandResult(err) +} + +func (gui *Gui) handleCommitPick(g *gocui.Gui, v *gocui.View) error { + applied, err := gui.handleMidRebaseCommand("pick") + if err != nil { + return err + } + if applied { + return nil + } + + // at this point we aren't actually rebasing so we will interpret this as an + // attempt to pull. We might revoke this later after enabling configurable keybindings + return gui.pullFiles(g, v) +} + +func (gui *Gui) handleCommitRevert(g *gocui.Gui, v *gocui.View) error { + if err := gui.GitCommand.Revert(gui.State.Commits[gui.State.Panels.Commits.SelectedLine].Sha); err != nil { + return gui.createErrorPanel(gui.g, err.Error()) + } + gui.State.Panels.Commits.SelectedLine++ + return gui.refreshCommits(gui.g) +} diff --git a/pkg/gui/files_panel.go b/pkg/gui/files_panel.go index 491b931e5..c6f5cd7d5 100644 --- a/pkg/gui/files_panel.go +++ b/pkg/gui/files_panel.go @@ -480,3 +480,16 @@ func (gui *Gui) anyFilesWithMergeConflicts() bool { } return false } + +func (gui *Gui) handleSoftReset(g *gocui.Gui, v *gocui.View) error { + return gui.createConfirmationPanel(g, v, gui.Tr.SLocalize("SoftReset"), gui.Tr.SLocalize("ConfirmSoftReset"), func(g *gocui.Gui, v *gocui.View) error { + if err := gui.GitCommand.SoftReset("HEAD^"); err != nil { + return gui.createErrorPanel(g, err.Error()) + } + + if err := gui.refreshCommits(gui.g); err != nil { + return err + } + return gui.refreshFiles() + }, nil) +} diff --git a/pkg/gui/keybindings.go b/pkg/gui/keybindings.go index c0becfe2b..ab70616ea 100644 --- a/pkg/gui/keybindings.go +++ b/pkg/gui/keybindings.go @@ -84,6 +84,12 @@ func (gui *Gui) GetInitialKeybindings() []*Binding { Handler: gui.scrollDownMain, }, { ViewName: "", + Key: 'm', + Modifier: gocui.ModNone, + Handler: gui.handleCreateRebaseOptionsMenu, + Description: gui.Tr.SLocalize("ViewMergeRebaseOptions"), + }, { + ViewName: "", Key: 'P', Modifier: gocui.ModNone, Handler: gui.pushFiles, @@ -161,12 +167,6 @@ func (gui *Gui) GetInitialKeybindings() []*Binding { Handler: gui.handleFileRemove, Description: gui.Tr.SLocalize("removeFile"), }, { - ViewName: "files", // TODO: might make this for more views as well - Key: 'm', - Modifier: gocui.ModNone, - Handler: gui.handleCreateRebaseOptionsMenu, - Description: gui.Tr.SLocalize("ViewMergeRebaseOptions"), - }, { ViewName: "files", Key: 'e', Modifier: gocui.ModNone, @@ -192,12 +192,18 @@ func (gui *Gui) GetInitialKeybindings() []*Binding { Description: gui.Tr.SLocalize("refreshFiles"), }, { ViewName: "files", - Key: 'S', + Key: 's', Modifier: gocui.ModNone, Handler: gui.handleStashSave, Description: gui.Tr.SLocalize("stashFiles"), }, { ViewName: "files", + Key: 'S', + Modifier: gocui.ModNone, + Handler: gui.handleSoftReset, + Description: gui.Tr.SLocalize("softReset"), + }, { + ViewName: "files", Key: 'a', Modifier: gocui.ModNone, Handler: gui.handleStageAll, @@ -270,7 +276,7 @@ func (gui *Gui) GetInitialKeybindings() []*Binding { Description: gui.Tr.SLocalize("rebaseBranch"), }, { ViewName: "branches", - Key: 'm', + Key: 'M', Modifier: gocui.ModNone, Handler: gui.handleMerge, Description: gui.Tr.SLocalize("mergeIntoCurrentBranch"), @@ -335,6 +341,24 @@ func (gui *Gui) GetInitialKeybindings() []*Binding { Handler: gui.handleCommitEdit, Description: gui.Tr.SLocalize("editCommit"), }, { + ViewName: "commits", + Key: 'A', + Modifier: gocui.ModNone, + Handler: gui.handleCommitAmendTo, + Description: gui.Tr.SLocalize("amendToCommit"), + }, { + ViewName: "commits", + Key: 'p', + Modifier: gocui.ModNone, + Handler: gui.handleCommitPick, + Description: gui.Tr.SLocalize("pickCommit"), + }, { + ViewName: "commits", + Key: 't', + Modifier: gocui.ModNone, + Handler: gui.handleCommitRevert, + Description: gui.Tr.SLocalize("revertCommit"), + }, { ViewName: "stash", Key: gocui.KeySpace, Modifier: gocui.ModNone, diff --git a/pkg/i18n/english.go b/pkg/i18n/english.go index 5fef83176..88058b265 100644 --- a/pkg/i18n/english.go +++ b/pkg/i18n/english.go @@ -91,6 +91,9 @@ func addEnglish(i18nObject *i18n.Bundle) error { ID: "stashFiles", Other: "stash files", }, &i18n.Message{ + ID: "softReset", + Other: "soft reset to last commit", + }, &i18n.Message{ ID: "open", Other: "open", }, &i18n.Message{ @@ -167,7 +170,13 @@ func addEnglish(i18nObject *i18n.Bundle) error { Other: "This file has no merge conflicts", }, &i18n.Message{ ID: "SureResetHardHead", - Other: "Are you sure you want `reset --hard HEAD` and `clean -fd`? You may lose changes", + Other: "Are you sure you want to `reset --hard HEAD` and `clean -fd`? You may lose changes", + }, &i18n.Message{ + ID: "SoftReset", + Other: "Soft reset", + }, &i18n.Message{ + ID: "ConfirmSoftReset", + Other: "Are you sure you want to `reset --soft HEAD^`? The changes in your topmost commit will be placed in your working tree", }, &i18n.Message{ ID: "SureTo", Other: "Are you sure you want to {{.deleteVerb}} {{.fileName}} (you will lose your changes)?", @@ -277,6 +286,18 @@ func addEnglish(i18nObject *i18n.Bundle) error { ID: "SureFixupThisCommit", Other: "Are you sure you want to fixup this commit? The commit beneath will be squashed up into this one", }, &i18n.Message{ + ID: "SureSquashThisCommit", + Other: "Are you sure you want to squash this commit into the commit below?", // TODO: i18n + }, &i18n.Message{ + ID: "Squash", + Other: "Squash", // TODO: i18n + }, &i18n.Message{ + ID: "pickCommit", + Other: "pick commit (when mid-rebase)", // TODO: i18n + }, &i18n.Message{ + ID: "revertCommit", + Other: "revert commit", // TODO: i18n + }, &i18n.Message{ ID: "OnlyRenameTopCommit", Other: "Can only rename topmost commit", }, &i18n.Message{ @@ -296,6 +317,9 @@ func addEnglish(i18nObject *i18n.Bundle) error { ID: "editCommit", Other: "edit commit", // TODO: other languages }, &i18n.Message{ + ID: "amendToCommit", + Other: "amend commit with staged changes", // TODO: other languages + }, &i18n.Message{ ID: "renameCommitEditor", Other: "rename commit with editor", }, &i18n.Message{ |