summaryrefslogtreecommitdiffstats
path: root/pkg
diff options
context:
space:
mode:
authorJesse Duffield <jessedduffield@gmail.com>2019-02-19 23:36:29 +1100
committerJesse Duffield <jessedduffield@gmail.com>2019-02-19 23:36:36 +1100
commit0228e250847d042730038281f435b0b21d992c42 (patch)
tree9f15ef6444c6215852a301a3286dcddf8a2b0cb3 /pkg
parent935f77483443a12ab159e19a958dfdf61a947b36 (diff)
work towards more interactive rebase options
Diffstat (limited to 'pkg')
-rw-r--r--pkg/app/app.go17
-rw-r--r--pkg/commands/commit.go10
-rw-r--r--pkg/commands/git.go99
-rw-r--r--pkg/commands/git_test.go64
-rw-r--r--pkg/git/commit_list_builder.go72
-rw-r--r--pkg/gui/commits_panel.go125
-rw-r--r--pkg/gui/files_panel.go13
-rw-r--r--pkg/gui/keybindings.go40
-rw-r--r--pkg/i18n/english.go26
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{