summaryrefslogtreecommitdiffstats
path: root/pkg/commands/rebasing.go
diff options
context:
space:
mode:
Diffstat (limited to 'pkg/commands/rebasing.go')
-rw-r--r--pkg/commands/rebasing.go287
1 files changed, 287 insertions, 0 deletions
diff --git a/pkg/commands/rebasing.go b/pkg/commands/rebasing.go
new file mode 100644
index 000000000..30e87c5dc
--- /dev/null
+++ b/pkg/commands/rebasing.go
@@ -0,0 +1,287 @@
+package commands
+
+import (
+ "fmt"
+ "io/ioutil"
+ "os"
+ "os/exec"
+ "path/filepath"
+ "strings"
+
+ "github.com/go-errors/errors"
+ "github.com/jesseduffield/lazygit/pkg/models"
+ "github.com/mgutz/str"
+)
+
+func (c *GitCommand) RewordCommit(commits []*models.Commit, index int) (*exec.Cmd, error) {
+ todo, sha, err := c.GenerateGenericRebaseTodo(commits, index, "reword")
+ if err != nil {
+ return nil, err
+ }
+
+ return c.PrepareInteractiveRebaseCommand(sha, todo, false)
+}
+
+func (c *GitCommand) MoveCommitDown(commits []*models.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
+ return errors.New(c.Tr.SLocalize("NoRoom"))
+ }
+
+ todo := ""
+ orderedCommits := append(commits[0:index], commits[index+1], commits[index])
+ for _, commit := range orderedCommits {
+ todo = "pick " + commit.Sha + " " + commit.Name + "\n" + todo
+ }
+
+ cmd, err := c.PrepareInteractiveRebaseCommand(commits[index+2].Sha, todo, true)
+ if err != nil {
+ return err
+ }
+
+ return c.OSCommand.RunPreparedCommand(cmd)
+}
+
+func (c *GitCommand) InteractiveRebase(commits []*models.Commit, index int, action string) error {
+ todo, sha, err := c.GenerateGenericRebaseTodo(commits, index, action)
+ if err != nil {
+ return err
+ }
+
+ cmd, err := c.PrepareInteractiveRebaseCommand(sha, todo, true)
+ if err != nil {
+ return err
+ }
+
+ return c.OSCommand.RunPreparedCommand(cmd)
+}
+
+// PrepareInteractiveRebaseCommand returns the cmd for an interactive rebase
+// we tell git to run lazygit to edit the todo list, and we pass the client
+// lazygit a todo string to write to the todo file
+func (c *GitCommand) PrepareInteractiveRebaseCommand(baseSha string, todo string, overrideEditor bool) (*exec.Cmd, error) {
+ ex := c.OSCommand.GetLazygitPath()
+
+ debug := "FALSE"
+ if c.OSCommand.Config.GetDebug() {
+ debug = "TRUE"
+ }
+
+ cmdStr := fmt.Sprintf("git rebase --interactive --autostash --keep-empty %s", baseSha)
+ c.Log.WithField("command", cmdStr).Info("RunCommand")
+ splitCmd := str.ToArgv(cmdStr)
+
+ cmd := c.OSCommand.Command(splitCmd[0], splitCmd[1:]...)
+
+ gitSequenceEditor := ex
+ if todo == "" {
+ gitSequenceEditor = "true"
+ }
+
+ cmd.Env = os.Environ()
+ cmd.Env = append(
+ cmd.Env,
+ "LAZYGIT_CLIENT_COMMAND=INTERACTIVE_REBASE",
+ "LAZYGIT_REBASE_TODO="+todo,
+ "DEBUG="+debug,
+ "LANG=en_US.UTF-8", // Force using EN as language
+ "LC_ALL=en_US.UTF-8", // Force using EN as language
+ "GIT_SEQUENCE_EDITOR="+gitSequenceEditor,
+ )
+
+ if overrideEditor {
+ cmd.Env = append(cmd.Env, "GIT_EDITOR="+ex)
+ }
+
+ return cmd, nil
+}
+
+func (c *GitCommand) GenerateGenericRebaseTodo(commits []*models.Commit, actionIndex int, action string) (string, string, error) {
+ baseIndex := actionIndex + 1
+
+ if len(commits) <= baseIndex {
+ return "", "", errors.New(c.Tr.SLocalize("CannotRebaseOntoFirstCommit"))
+ }
+
+ if action == "squash" || action == "fixup" {
+ baseIndex++
+
+ if len(commits) <= baseIndex {
+ return "", "", errors.New(c.Tr.SLocalize("CannotSquashOntoSecondCommit"))
+ }
+ }
+
+ todo := ""
+ for i, commit := range commits[0:baseIndex] {
+ var commitAction string
+ if i == actionIndex {
+ commitAction = action
+ } else if commit.IsMerge {
+ // your typical interactive rebase will actually drop merge commits by default. Damn git CLI, you scary!
+ // doing this means we don't need to worry about rebasing over merges which always causes problems.
+ // you typically shouldn't be doing rebases that pass over merge commits anyway.
+ commitAction = "drop"
+ } else {
+ commitAction = "pick"
+ }
+ todo = commitAction + " " + commit.Sha + " " + commit.Name + "\n" + todo
+ }
+
+ return todo, commits[baseIndex].Sha, nil
+}
+
+// AmendTo amends the given commit with whatever files are staged
+func (c *GitCommand) AmendTo(sha string) error {
+ if err := c.CreateFixupCommit(sha); err != nil {
+ return err
+ }
+
+ return c.SquashAllAboveFixupCommits(sha)
+}
+
+// EditRebaseTodo sets the action at a given index in the git-rebase-todo file
+func (c *GitCommand) EditRebaseTodo(index int, action string) error {
+ fileName := filepath.Join(c.DotGitDir, "rebase-merge/git-rebase-todo")
+ bytes, err := ioutil.ReadFile(fileName)
+ if err != nil {
+ return err
+ }
+
+ content := strings.Split(string(bytes), "\n")
+ commitCount := c.getTodoCommitCount(content)
+
+ // we have the most recent commit at the bottom whereas the todo file has
+ // it at the bottom, so we need to subtract our index from the commit count
+ contentIndex := commitCount - 1 - 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)
+}
+
+func (c *GitCommand) getTodoCommitCount(content []string) int {
+ // count lines that are not blank and are not comments
+ commitCount := 0
+ for _, line := range content {
+ if line != "" && !strings.HasPrefix(line, "#") {
+ commitCount++
+ }
+ }
+ return commitCount
+}
+
+// MoveTodoDown moves a rebase todo item down by one position
+func (c *GitCommand) MoveTodoDown(index int) error {
+ fileName := filepath.Join(c.DotGitDir, "rebase-merge/git-rebase-todo")
+ bytes, err := ioutil.ReadFile(fileName)
+ if err != nil {
+ return err
+ }
+
+ content := strings.Split(string(bytes), "\n")
+ commitCount := c.getTodoCommitCount(content)
+ contentIndex := commitCount - 1 - index
+
+ rearrangedContent := append(content[0:contentIndex-1], content[contentIndex], content[contentIndex-1])
+ rearrangedContent = append(rearrangedContent, content[contentIndex+1:]...)
+ result := strings.Join(rearrangedContent, "\n")
+
+ return ioutil.WriteFile(fileName, []byte(result), 0644)
+}
+
+// SquashAllAboveFixupCommits squashes all fixup! commits above the given one
+func (c *GitCommand) SquashAllAboveFixupCommits(sha string) error {
+ return c.runSkipEditorCommand(
+ fmt.Sprintf(
+ "git rebase --interactive --autostash --autosquash %s^",
+ sha,
+ ),
+ )
+}
+
+// BeginInteractiveRebaseForCommit starts an interactive rebase to edit the current
+// commit and pick all others. After this you'll want to call `c.GenericMergeOrRebaseAction("rebase", "continue")`
+func (c *GitCommand) BeginInteractiveRebaseForCommit(commits []*models.Commit, commitIndex int) error {
+ if len(commits)-1 < commitIndex {
+ return errors.New("index outside of range of commits")
+ }
+
+ // we can make this GPG thing possible it just means we need to do this in two parts:
+ // one where we handle the possibility of a credential request, and the other
+ // where we continue the rebase
+ if c.usingGpg() {
+ return errors.New(c.Tr.SLocalize("DisabledForGPG"))
+ }
+
+ todo, sha, err := c.GenerateGenericRebaseTodo(commits, commitIndex, "edit")
+ if err != nil {
+ return err
+ }
+
+ cmd, err := c.PrepareInteractiveRebaseCommand(sha, todo, true)
+ if err != nil {
+ return err
+ }
+
+ if err := c.OSCommand.RunPreparedCommand(cmd); err != nil {
+ return err
+ }
+
+ return nil
+}
+
+// RebaseBranch interactive rebases onto a branch
+func (c *GitCommand) RebaseBranch(branchName string) error {
+ cmd, err := c.PrepareInteractiveRebaseCommand(branchName, "", false)
+ if err != nil {
+ return err
+ }
+
+ return c.OSCommand.RunPreparedCommand(cmd)
+}
+
+// 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) GenericMergeOrRebaseAction(commandType string, command string) error {
+ err := c.runSkipEditorCommand(
+ fmt.Sprintf(
+ "git %s --%s",
+ commandType,
+ command,
+ ),
+ )
+ if err != nil {
+ if !strings.Contains(err.Error(), "no rebase in progress") {
+ return err
+ }
+ c.Log.Warn(err)
+ }
+
+ // sometimes we need to do a sequence of things in a rebase but the user needs to
+ // fix merge conflicts along the way. When this happens we queue up the next step
+ // so that after the next successful rebase continue we can continue from where we left off
+ if commandType == "rebase" && command == "continue" && c.onSuccessfulContinue != nil {
+ f := c.onSuccessfulContinue
+ c.onSuccessfulContinue = nil
+ return f()
+ }
+ if command == "abort" {
+ c.onSuccessfulContinue = nil
+ }
+ return nil
+}
+
+func (c *GitCommand) runSkipEditorCommand(command string) error {
+ cmd := c.OSCommand.ExecutableFromString(command)
+ lazyGitPath := c.OSCommand.GetLazygitPath()
+ cmd.Env = append(
+ cmd.Env,
+ "LAZYGIT_CLIENT_COMMAND=EXIT_IMMEDIATELY",
+ "GIT_EDITOR="+lazyGitPath,
+ "EDITOR="+lazyGitPath,
+ "VISUAL="+lazyGitPath,
+ )
+ return c.OSCommand.RunExecutable(cmd)
+}