1
0
mirror of https://github.com/jesseduffield/lazygit.git synced 2025-07-28 16:02:01 +03:00

work towards more interactive rebase options

This commit is contained in:
Jesse Duffield
2019-02-19 23:36:29 +11:00
parent 935f774834
commit 0228e25084
9 changed files with 263 additions and 201 deletions

View File

@ -5,6 +5,7 @@ import (
"io/ioutil" "io/ioutil"
"os" "os"
"path/filepath" "path/filepath"
"strings"
"github.com/heroku/rollrus" "github.com/heroku/rollrus"
"github.com/jesseduffield/lazygit/pkg/commands" "github.com/jesseduffield/lazygit/pkg/commands"
@ -120,10 +121,20 @@ func (app *App) Run() error {
return app.Gui.RunWithSubprocesses() 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 { func (app *App) Rebase() error {
app.Log.Error("Lazygit invokved as interactive rebase demon") app.Log.Info("Lazygit invoked as interactive rebase demon")
app.Log.Info("args: ", os.Args)
ioutil.WriteFile(".git/rebase-merge/git-rebase-todo", []byte(os.Getenv("LAZYGIT_REBASE_TODO")), 0644) 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 return nil
} }

View File

@ -2,6 +2,7 @@ package commands
import ( import (
"github.com/fatih/color" "github.com/fatih/color"
"github.com/jesseduffield/lazygit/pkg/utils"
) )
// Commit : A git commit // Commit : A git commit
@ -10,6 +11,7 @@ type Commit struct {
Name string Name string
Status string // one of "unpushed", "pushed", "merged", or "rebasing" Status string // one of "unpushed", "pushed", "merged", or "rebasing"
DisplayString string DisplayString string
Action string // one of "", "pick", "edit", "squash", "reword", "drop", "fixup"
} }
// GetDisplayStrings is a function. // GetDisplayStrings is a function.
@ -19,6 +21,7 @@ func (c *Commit) GetDisplayStrings(isFocused bool) []string {
green := color.New(color.FgGreen) green := color.New(color.FgGreen)
white := color.New(color.FgWhite) white := color.New(color.FgWhite)
blue := color.New(color.FgBlue) blue := color.New(color.FgBlue)
cyan := color.New(color.FgCyan)
var shaColor *color.Color var shaColor *color.Color
switch c.Status { switch c.Status {
@ -34,5 +37,10 @@ func (c *Commit) GetDisplayStrings(isFocused bool) []string {
shaColor = white 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)}
} }

View File

@ -2,6 +2,7 @@ package commands
import ( import (
"fmt" "fmt"
"io/ioutil"
"os" "os"
"os/exec" "os/exec"
"strings" "strings"
@ -360,46 +361,6 @@ func (c *GitCommand) Push(branchName string, force bool, ask func(string) string
return c.OSCommand.DetectUnamePass(cmd, ask) 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 // CatFile obtains the content of a file
func (c *GitCommand) CatFile(fileName string) (string, error) { func (c *GitCommand) CatFile(fileName string) (string, error) {
return c.OSCommand.RunCommandWithOutput(fmt.Sprintf("cat %s", c.OSCommand.Quote(fileName))) 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)) 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 // By default we skip the editor in the case where a commit will be made
func (c *GitCommand) GenericMerge(commandType string, command string) error { func (c *GitCommand) GenericMerge(commandType string, command string) error {
gitCommand := fmt.Sprintf("git %s %s --%s", c.OSCommand.Platform.skipEditorArg, commandType, command) 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 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 { func (c *GitCommand) MoveCommitDown(commits []*Commit, index int) error {
@ -633,7 +594,7 @@ func (c *GitCommand) MoveCommitDown(commits []*Commit, index int) error {
todo := "" todo := ""
orderedCommits := append(commits[0:index], commits[index+1], commits[index]) orderedCommits := append(commits[0:index], commits[index+1], commits[index])
for _, commit := range orderedCommits { 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) 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 return err
} }
// TODO: decide whether to autostash when action == editing
cmd, err := c.PrepareInteractiveRebaseCommand(commits[index+1].Sha, todo, true) cmd, err := c.PrepareInteractiveRebaseCommand(commits[index+1].Sha, todo, true)
if err != nil { if err != nil {
return err return err
@ -659,7 +619,7 @@ func (c *GitCommand) InteractiveRebase(commits []*Commit, index int, action stri
return c.OSCommand.RunPreparedCommand(cmd) 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 ex, err := os.Executable() // get the executable path for git to use
if err != nil { if err != nil {
ex = os.Args[0] // fallback to the first call argument if needed 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" debug = "TRUE"
} }
autoStashFlag := "" splitCmd := str.ToArgv(fmt.Sprintf("git rebase --interactive --autostash %s", baseSha))
if autoStash {
autoStashFlag = "--autostash"
}
splitCmd := str.ToArgv(fmt.Sprintf("git rebase --interactive %s %s", autoStashFlag, baseSha))
cmd := exec.Command(splitCmd[0], splitCmd[1:]...) cmd := exec.Command(splitCmd[0], splitCmd[1:]...)
@ -690,6 +645,10 @@ func (c *GitCommand) PrepareInteractiveRebaseCommand(baseSha string, todo string
"GIT_SEQUENCE_EDITOR="+ex, "GIT_SEQUENCE_EDITOR="+ex,
) )
if overrideEditor {
cmd.Env = append(cmd.Env, "EDITOR="+ex)
}
return cmd, nil return cmd, nil
} }
@ -697,7 +656,11 @@ func (c *GitCommand) HardReset(baseSha string) error {
return c.OSCommand.RunCommand("git reset --hard " + baseSha) 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 { if len(commits) <= index+1 {
// assuming they aren't picking the bottom commit // assuming they aren't picking the bottom commit
// TODO: support more than say 30 commits and ensure this logic is correct, and i18n // 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 { if i == index {
a = action a = action
} }
todo = a + " " + commit.Sha + "\n" + todo todo = a + " " + commit.Sha + " " + commit.Name + "\n" + todo
} }
return todo, nil 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))
}

View File

@ -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. // TestGitCommandCatFile is a function.
func TestGitCommandCatFile(t *testing.T) { func TestGitCommandCatFile(t *testing.T) {
gitCmd := newDummyGitCommand() gitCmd := newDummyGitCommand()

View File

@ -42,13 +42,20 @@ func NewCommitListBuilder(log *logrus.Entry, gitCommand *commands.GitCommand, os
// GetCommits obtains the commits of the current branch // GetCommits obtains the commits of the current branch
func (c *CommitListBuilder) GetCommits() ([]*commands.Commit, error) { func (c *CommitListBuilder) GetCommits() ([]*commands.Commit, error) {
commits := []*commands.Commit{} commits := []*commands.Commit{}
// here we want to also prepend the commits that we're in the process of rebasing var rebasingCommits []*commands.Commit
rebasingCommits, err := c.getRebasingCommits() rebaseMode, err := c.GitCommand.RebaseMode()
if err != nil { if err != nil {
return nil, err return nil, err
} }
if len(rebasingCommits) > 0 { if rebaseMode != "" {
commits = append(commits, rebasingCommits...) // 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() unpushedCommits := c.getUnpushedCommits()
@ -67,7 +74,7 @@ func (c *CommitListBuilder) GetCommits() ([]*commands.Commit, error) {
DisplayString: strings.Join(splitLine, " "), DisplayString: strings.Join(splitLine, " "),
}) })
} }
if len(rebasingCommits) > 0 { if rebaseMode != "" {
currentCommit := commits[len(rebasingCommits)] currentCommit := commits[len(rebasingCommits)]
blue := color.New(color.FgYellow) blue := color.New(color.FgYellow)
youAreHere := blue.Sprint("<-- YOU ARE HERE ---") 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 // getRebasingCommits obtains the commits that we're in the process of rebasing
func (c *CommitListBuilder) getRebasingCommits() ([]*commands.Commit, error) { func (c *CommitListBuilder) getRebasingCommits(rebaseMode string) ([]*commands.Commit, error) {
rebaseMode, err := c.GitCommand.RebaseMode()
if err != nil {
return nil, err
}
switch rebaseMode { switch rebaseMode {
case "normal": case "normal":
return c.getNormalRebasingCommits() return c.getNormalRebasingCommits()
@ -147,45 +150,28 @@ func (c *CommitListBuilder) getNormalRebasingCommits() ([]*commands.Commit, erro
// in the rebase: // in the rebase:
func (c *CommitListBuilder) getInteractiveRebasingCommits() ([]*commands.Commit, error) { func (c *CommitListBuilder) getInteractiveRebasingCommits() ([]*commands.Commit, error) {
bytesContent, err := ioutil.ReadFile(".git/rebase-merge/git-rebase-todo") bytesContent, err := ioutil.ReadFile(".git/rebase-merge/git-rebase-todo")
var content []string if err != nil {
if err == nil { c.Log.Info(fmt.Sprintf("error occured reading git-rebase-todo: %s", err.Error()))
content = strings.Split(string(bytesContent), "\n") // we assume an error means the file doesn't exist so we just return
if len(content) > 0 && content[len(content)-1] == "" { return nil, nil
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")
} }
commits := []*commands.Commit{} commits := []*commands.Commit{}
for _, todoLine := range content { lines := strings.Split(string(bytesContent), "\n")
commit := c.extractCommit(todoLine, backupContent) for _, line := range lines {
if commit != nil { if line == "" {
commits = append([]*commands.Commit{commit}, commits...) 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 return nil, 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
} }
// assuming the file starts like this: // assuming the file starts like this:

View File

@ -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 { 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 { if len(gui.State.Commits) <= 1 {
return gui.createErrorPanel(g, gui.Tr.SLocalize("YouNoCommitsToSquash")) return gui.createErrorPanel(g, gui.Tr.SLocalize("YouNoCommitsToSquash"))
} }
commit := gui.getSelectedCommit(g)
if commit == nil { applied, err := gui.handleMidRebaseCommand("squash")
return errors.New(gui.Tr.SLocalize("NoCommitsThisBranch")) if err != nil {
return err
} }
if err := gui.GitCommand.SquashPreviousTwoCommits(commit.Name); err != nil { if applied {
return gui.createErrorPanel(g, err.Error()) return nil
} }
if err := gui.refreshCommits(g); err != nil {
panic(err) 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")
gui.refreshStatus(g) return gui.handleGenericMergeCommandResult(err)
return gui.handleCommitSelect(g, v) }, nil)
return nil
} }
// TODO: move to files panel // 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 { if len(gui.State.Commits) <= 1 {
return gui.createErrorPanel(g, gui.Tr.SLocalize("YouNoCommitsToSquash")) 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] if applied {
commit := gui.getSelectedCommit(g) return nil
if commit == nil {
return gui.createErrorPanel(g, gui.Tr.SLocalize("NoCommitsThisBranch"))
} }
message := gui.Tr.SLocalize("SureFixupThisCommit")
gui.createConfirmationPanel(g, v, gui.Tr.SLocalize("Fixup"), message, func(g *gocui.Gui, v *gocui.View) error { gui.createConfirmationPanel(g, v, gui.Tr.SLocalize("Fixup"), gui.Tr.SLocalize("SureFixupThisCommit"), func(g *gocui.Gui, v *gocui.View) error {
if err := gui.GitCommand.SquashFixupCommit(branch.Name, commit.Sha); err != nil { err := gui.GitCommand.InteractiveRebase(gui.State.Commits, gui.State.Panels.Commits.SelectedLine, "fixup")
return gui.createErrorPanel(g, err.Error()) return gui.handleGenericMergeCommandResult(err)
}
if err := gui.refreshCommits(g); err != nil {
panic(err)
}
return gui.refreshStatus(g)
}, nil) }, nil)
return nil return nil
} }
func (gui *Gui) handleRenameCommit(g *gocui.Gui, v *gocui.View) error { 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 { if gui.State.Panels.Commits.SelectedLine != 0 {
return gui.createErrorPanel(g, gui.Tr.SLocalize("OnlyRenameTopCommit")) 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 { 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) subProcess, err := gui.GitCommand.RewordCommit(gui.State.Commits, gui.State.Panels.Commits.SelectedLine)
if err != nil { if err != nil {
return gui.createErrorPanel(gui.g, err.Error()) return gui.createErrorPanel(gui.g, err.Error())
@ -198,7 +208,29 @@ func (gui *Gui) handleRenameCommitEditor(g *gocui.Gui, v *gocui.View) error {
return nil 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 { 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 // 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 { 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") 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 { 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) 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)
}

View File

@ -480,3 +480,16 @@ func (gui *Gui) anyFilesWithMergeConflicts() bool {
} }
return false 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)
}

View File

@ -82,6 +82,12 @@ func (gui *Gui) GetInitialKeybindings() []*Binding {
Key: gocui.KeyCtrlD, Key: gocui.KeyCtrlD,
Modifier: gocui.ModNone, Modifier: gocui.ModNone,
Handler: gui.scrollDownMain, Handler: gui.scrollDownMain,
}, {
ViewName: "",
Key: 'm',
Modifier: gocui.ModNone,
Handler: gui.handleCreateRebaseOptionsMenu,
Description: gui.Tr.SLocalize("ViewMergeRebaseOptions"),
}, { }, {
ViewName: "", ViewName: "",
Key: 'P', Key: 'P',
@ -160,12 +166,6 @@ func (gui *Gui) GetInitialKeybindings() []*Binding {
Modifier: gocui.ModNone, Modifier: gocui.ModNone,
Handler: gui.handleFileRemove, Handler: gui.handleFileRemove,
Description: gui.Tr.SLocalize("removeFile"), 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", ViewName: "files",
Key: 'e', Key: 'e',
@ -192,10 +192,16 @@ func (gui *Gui) GetInitialKeybindings() []*Binding {
Description: gui.Tr.SLocalize("refreshFiles"), Description: gui.Tr.SLocalize("refreshFiles"),
}, { }, {
ViewName: "files", ViewName: "files",
Key: 'S', Key: 's',
Modifier: gocui.ModNone, Modifier: gocui.ModNone,
Handler: gui.handleStashSave, Handler: gui.handleStashSave,
Description: gui.Tr.SLocalize("stashFiles"), Description: gui.Tr.SLocalize("stashFiles"),
}, {
ViewName: "files",
Key: 'S',
Modifier: gocui.ModNone,
Handler: gui.handleSoftReset,
Description: gui.Tr.SLocalize("softReset"),
}, { }, {
ViewName: "files", ViewName: "files",
Key: 'a', Key: 'a',
@ -270,7 +276,7 @@ func (gui *Gui) GetInitialKeybindings() []*Binding {
Description: gui.Tr.SLocalize("rebaseBranch"), Description: gui.Tr.SLocalize("rebaseBranch"),
}, { }, {
ViewName: "branches", ViewName: "branches",
Key: 'm', Key: 'M',
Modifier: gocui.ModNone, Modifier: gocui.ModNone,
Handler: gui.handleMerge, Handler: gui.handleMerge,
Description: gui.Tr.SLocalize("mergeIntoCurrentBranch"), Description: gui.Tr.SLocalize("mergeIntoCurrentBranch"),
@ -334,6 +340,24 @@ func (gui *Gui) GetInitialKeybindings() []*Binding {
Modifier: gocui.ModNone, Modifier: gocui.ModNone,
Handler: gui.handleCommitEdit, Handler: gui.handleCommitEdit,
Description: gui.Tr.SLocalize("editCommit"), 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", ViewName: "stash",
Key: gocui.KeySpace, Key: gocui.KeySpace,

View File

@ -90,6 +90,9 @@ func addEnglish(i18nObject *i18n.Bundle) error {
}, &i18n.Message{ }, &i18n.Message{
ID: "stashFiles", ID: "stashFiles",
Other: "stash files", Other: "stash files",
}, &i18n.Message{
ID: "softReset",
Other: "soft reset to last commit",
}, &i18n.Message{ }, &i18n.Message{
ID: "open", ID: "open",
Other: "open", Other: "open",
@ -167,7 +170,13 @@ func addEnglish(i18nObject *i18n.Bundle) error {
Other: "This file has no merge conflicts", Other: "This file has no merge conflicts",
}, &i18n.Message{ }, &i18n.Message{
ID: "SureResetHardHead", 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{ }, &i18n.Message{
ID: "SureTo", ID: "SureTo",
Other: "Are you sure you want to {{.deleteVerb}} {{.fileName}} (you will lose your changes)?", Other: "Are you sure you want to {{.deleteVerb}} {{.fileName}} (you will lose your changes)?",
@ -276,6 +285,18 @@ func addEnglish(i18nObject *i18n.Bundle) error {
}, &i18n.Message{ }, &i18n.Message{
ID: "SureFixupThisCommit", ID: "SureFixupThisCommit",
Other: "Are you sure you want to fixup this commit? The commit beneath will be squashed up into this one", 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{ }, &i18n.Message{
ID: "OnlyRenameTopCommit", ID: "OnlyRenameTopCommit",
Other: "Can only rename topmost commit", Other: "Can only rename topmost commit",
@ -295,6 +316,9 @@ func addEnglish(i18nObject *i18n.Bundle) error {
}, &i18n.Message{ }, &i18n.Message{
ID: "editCommit", ID: "editCommit",
Other: "edit commit", // TODO: other languages Other: "edit commit", // TODO: other languages
}, &i18n.Message{
ID: "amendToCommit",
Other: "amend commit with staged changes", // TODO: other languages
}, &i18n.Message{ }, &i18n.Message{
ID: "renameCommitEditor", ID: "renameCommitEditor",
Other: "rename commit with editor", Other: "rename commit with editor",