1
0
mirror of https://github.com/jesseduffield/lazygit.git synced 2024-12-14 11:23:09 +02:00
lazygit/pkg/commands/git_commands/rebase.go

371 lines
12 KiB
Go
Raw Normal View History

2022-01-08 05:00:36 +02:00
package git_commands
2020-09-29 12:03:39 +02:00
import (
"fmt"
"io/ioutil"
"path/filepath"
"strings"
"github.com/go-errors/errors"
2022-03-20 07:19:27 +02:00
"github.com/jesseduffield/generics/slices"
"github.com/jesseduffield/lazygit/pkg/commands/models"
2021-12-07 12:59:36 +02:00
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
2020-09-29 12:03:39 +02:00
)
2022-01-02 01:34:33 +02:00
type RebaseCommands struct {
*GitCommon
commit *CommitCommands
workingTree *WorkingTreeCommands
2022-01-02 01:34:33 +02:00
onSuccessfulContinue func() error
}
func NewRebaseCommands(
gitCommon *GitCommon,
2022-01-02 01:34:33 +02:00
commitCommands *CommitCommands,
workingTreeCommands *WorkingTreeCommands,
) *RebaseCommands {
return &RebaseCommands{
GitCommon: gitCommon,
2022-01-02 01:34:33 +02:00
commit: commitCommands,
workingTree: workingTreeCommands,
}
}
2022-01-09 04:36:07 +02:00
func (self *RebaseCommands) RewordCommit(commits []*models.Commit, index int, message string) error {
if index == 0 {
// we've selected the top commit so no rebase is required
return self.commit.RewordLastCommit(message)
}
err := self.BeginInteractiveRebaseForCommit(commits, index)
if err != nil {
return err
}
// now the selected commit should be our head so we'll amend it with the new message
err = self.commit.RewordLastCommit(message)
if err != nil {
return err
}
return self.ContinueRebase()
}
func (self *RebaseCommands) RewordCommitInEditor(commits []*models.Commit, index int) (oscommands.ICmdObj, error) {
2022-03-20 07:19:27 +02:00
todo, sha, err := self.BuildSingleActionTodo(commits, index, "reword")
2020-09-29 12:03:39 +02:00
if err != nil {
return nil, err
}
2022-01-09 04:36:07 +02:00
return self.PrepareInteractiveRebaseCommand(sha, todo, false), nil
2020-09-29 12:03:39 +02:00
}
2022-01-02 01:34:33 +02:00
func (self *RebaseCommands) MoveCommitDown(commits []*models.Commit, index int) error {
2020-09-29 12:03:39 +02:00
// 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
2022-01-02 01:34:33 +02:00
return errors.New(self.Tr.NoRoom)
2020-09-29 12:03:39 +02:00
}
orderedCommits := append(commits[0:index], commits[index+1], commits[index])
2022-03-20 07:19:27 +02:00
todoLines := self.BuildTodoLinesSingleAction(orderedCommits, "pick")
return self.PrepareInteractiveRebaseCommand(commits[index+2].Sha, todoLines, true).Run()
2020-09-29 12:03:39 +02:00
}
2022-01-02 01:34:33 +02:00
func (self *RebaseCommands) InteractiveRebase(commits []*models.Commit, index int, action string) error {
2022-03-20 07:19:27 +02:00
todo, sha, err := self.BuildSingleActionTodo(commits, index, action)
2020-09-29 12:03:39 +02:00
if err != nil {
return err
}
2022-01-09 04:36:07 +02:00
return self.PrepareInteractiveRebaseCommand(sha, todo, true).Run()
2020-09-29 12:03:39 +02:00
}
// 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
2022-03-20 07:19:27 +02:00
func (self *RebaseCommands) PrepareInteractiveRebaseCommand(baseSha string, todoLines []TodoLine, overrideEditor bool) oscommands.ICmdObj {
todo := self.buildTodo(todoLines)
2022-01-05 03:01:59 +02:00
ex := oscommands.GetLazygitPath()
2020-09-29 12:03:39 +02:00
debug := "FALSE"
2022-01-02 01:34:33 +02:00
if self.Debug {
2020-09-29 12:03:39 +02:00
debug = "TRUE"
}
cmdStr := fmt.Sprintf("git rebase --interactive --autostash --keep-empty %s", baseSha)
2022-01-02 01:34:33 +02:00
self.Log.WithField("command", cmdStr).Info("RunCommand")
2020-09-29 12:03:39 +02:00
2022-01-02 01:34:33 +02:00
cmdObj := self.cmd.New(cmdStr)
2020-09-29 12:03:39 +02:00
gitSequenceEditor := ex
if todo == "" {
gitSequenceEditor = "true"
} else {
self.os.LogCommand(fmt.Sprintf("Creating TODO file for interactive rebase: \n\n%s", todo), false)
2020-09-29 12:03:39 +02:00
}
2021-12-07 12:59:36 +02:00
cmdObj.AddEnvVars(
2020-09-29 12:03:39 +02:00
"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 {
2021-12-07 12:59:36 +02:00
cmdObj.AddEnvVars("GIT_EDITOR=" + ex)
2020-09-29 12:03:39 +02:00
}
2022-01-09 04:36:07 +02:00
return cmdObj
2020-09-29 12:03:39 +02:00
}
2022-03-20 07:19:27 +02:00
// produces TodoLines where every commit is picked (or dropped for merge commits) except for the commit at the given index, which
// will have the given action applied to it.
func (self *RebaseCommands) BuildSingleActionTodo(commits []*models.Commit, actionIndex int, action string) ([]TodoLine, string, error) {
2020-09-29 12:03:39 +02:00
baseIndex := actionIndex + 1
if len(commits) <= baseIndex {
2022-03-20 07:19:27 +02:00
return nil, "", errors.New(self.Tr.CannotRebaseOntoFirstCommit)
2020-09-29 12:03:39 +02:00
}
if action == "squash" || action == "fixup" {
baseIndex++
if len(commits) <= baseIndex {
2022-03-20 07:19:27 +02:00
return nil, "", errors.New(self.Tr.CannotSquashOntoSecondCommit)
2020-09-29 12:03:39 +02:00
}
}
2022-03-20 07:19:27 +02:00
todoLines := self.BuildTodoLines(commits[0:baseIndex], func(commit *models.Commit, i int) string {
2020-09-29 12:03:39 +02:00
if i == actionIndex {
2022-03-20 07:19:27 +02:00
return action
2021-06-05 08:39:59 +02:00
} else if commit.IsMerge() {
2020-09-29 12:03:39 +02:00
// 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.
2022-03-20 07:19:27 +02:00
return "drop"
2020-09-29 12:03:39 +02:00
} else {
2022-03-20 07:19:27 +02:00
return "pick"
2020-09-29 12:03:39 +02:00
}
2022-03-20 07:19:27 +02:00
})
2020-09-29 12:03:39 +02:00
2022-03-20 07:19:27 +02:00
return todoLines, commits[baseIndex].Sha, nil
2020-09-29 12:03:39 +02:00
}
// AmendTo amends the given commit with whatever files are staged
2022-01-02 01:34:33 +02:00
func (self *RebaseCommands) AmendTo(sha string) error {
if err := self.commit.CreateFixupCommit(sha); err != nil {
2020-09-29 12:03:39 +02:00
return err
}
2022-01-02 01:34:33 +02:00
return self.SquashAllAboveFixupCommits(sha)
2020-09-29 12:03:39 +02:00
}
// EditRebaseTodo sets the action at a given index in the git-rebase-todo file
2022-01-02 01:34:33 +02:00
func (self *RebaseCommands) EditRebaseTodo(index int, action string) error {
fileName := filepath.Join(self.dotGitDir, "rebase-merge/git-rebase-todo")
2020-09-29 12:03:39 +02:00
bytes, err := ioutil.ReadFile(fileName)
if err != nil {
return err
}
content := strings.Split(string(bytes), "\n")
2022-01-02 01:34:33 +02:00
commitCount := self.getTodoCommitCount(content)
2020-09-29 12:03:39 +02:00
// 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")
2022-03-19 00:38:49 +02:00
return ioutil.WriteFile(fileName, []byte(result), 0o644)
2020-09-29 12:03:39 +02:00
}
2022-01-02 01:34:33 +02:00
func (self *RebaseCommands) getTodoCommitCount(content []string) int {
2020-09-29 12:03:39 +02:00
// 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
2022-01-02 01:34:33 +02:00
func (self *RebaseCommands) MoveTodoDown(index int) error {
fileName := filepath.Join(self.dotGitDir, "rebase-merge/git-rebase-todo")
2020-09-29 12:03:39 +02:00
bytes, err := ioutil.ReadFile(fileName)
if err != nil {
return err
}
content := strings.Split(string(bytes), "\n")
2022-01-02 01:34:33 +02:00
commitCount := self.getTodoCommitCount(content)
2020-09-29 12:03:39 +02:00
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")
2022-03-19 00:38:49 +02:00
return ioutil.WriteFile(fileName, []byte(result), 0o644)
2020-09-29 12:03:39 +02:00
}
// SquashAllAboveFixupCommits squashes all fixup! commits above the given one
2022-01-02 01:34:33 +02:00
func (self *RebaseCommands) SquashAllAboveFixupCommits(sha string) error {
return self.runSkipEditorCommand(
2022-01-07 11:33:34 +02:00
self.cmd.New(
fmt.Sprintf(
"git rebase --interactive --autostash --autosquash %s^",
sha,
),
2020-09-29 12:03:39 +02:00
),
)
}
// BeginInteractiveRebaseForCommit starts an interactive rebase to edit the current
2022-01-09 04:36:07 +02:00
// commit and pick all others. After this you'll want to call `self.ContinueRebase()
2022-01-02 01:34:33 +02:00
func (self *RebaseCommands) BeginInteractiveRebaseForCommit(commits []*models.Commit, commitIndex int) error {
2020-09-29 12:03:39 +02:00
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
2022-01-02 01:34:33 +02:00
if self.config.UsingGpg() {
return errors.New(self.Tr.DisabledForGPG)
2020-09-29 12:03:39 +02:00
}
2022-03-20 07:19:27 +02:00
todo, sha, err := self.BuildSingleActionTodo(commits, commitIndex, "edit")
2020-09-29 12:03:39 +02:00
if err != nil {
return err
}
2022-01-09 04:36:07 +02:00
return self.PrepareInteractiveRebaseCommand(sha, todo, true).Run()
2020-09-29 12:03:39 +02:00
}
// RebaseBranch interactive rebases onto a branch
2022-01-02 01:34:33 +02:00
func (self *RebaseCommands) RebaseBranch(branchName string) error {
2022-03-20 07:19:27 +02:00
return self.PrepareInteractiveRebaseCommand(branchName, nil, false).Run()
2020-09-29 12:03:39 +02:00
}
2022-01-07 11:33:34 +02:00
func (self *RebaseCommands) GenericMergeOrRebaseActionCmdObj(commandType string, command string) oscommands.ICmdObj {
return self.cmd.New("git " + commandType + " --" + command)
}
2022-01-09 04:36:07 +02:00
func (self *RebaseCommands) ContinueRebase() error {
return self.GenericMergeOrRebaseAction("rebase", "continue")
}
func (self *RebaseCommands) AbortRebase() error {
return self.GenericMergeOrRebaseAction("rebase", "abort")
}
2020-09-29 12:03:39 +02:00
// 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
2022-01-02 01:34:33 +02:00
func (self *RebaseCommands) GenericMergeOrRebaseAction(commandType string, command string) error {
2022-01-07 11:33:34 +02:00
err := self.runSkipEditorCommand(self.GenericMergeOrRebaseActionCmdObj(commandType, command))
2020-09-29 12:03:39 +02:00
if err != nil {
if !strings.Contains(err.Error(), "no rebase in progress") {
return err
}
2022-01-02 01:34:33 +02:00
self.Log.Warn(err)
2020-09-29 12:03:39 +02:00
}
// 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
2022-01-02 01:34:33 +02:00
if commandType == "rebase" && command == "continue" && self.onSuccessfulContinue != nil {
f := self.onSuccessfulContinue
self.onSuccessfulContinue = nil
2020-09-29 12:03:39 +02:00
return f()
}
if command == "abort" {
2022-01-02 01:34:33 +02:00
self.onSuccessfulContinue = nil
2020-09-29 12:03:39 +02:00
}
return nil
}
2022-01-07 11:33:34 +02:00
func (self *RebaseCommands) runSkipEditorCommand(cmdObj oscommands.ICmdObj) error {
2022-01-05 03:01:59 +02:00
lazyGitPath := oscommands.GetLazygitPath()
2021-12-29 05:33:38 +02:00
return cmdObj.
AddEnvVars(
"LAZYGIT_CLIENT_COMMAND=EXIT_IMMEDIATELY",
"GIT_EDITOR="+lazyGitPath,
"EDITOR="+lazyGitPath,
"VISUAL="+lazyGitPath,
).
Run()
2020-09-29 12:03:39 +02:00
}
2022-01-02 01:34:33 +02:00
// DiscardOldFileChanges discards changes to a file from an old commit
func (self *RebaseCommands) DiscardOldFileChanges(commits []*models.Commit, commitIndex int, fileName string) error {
if err := self.BeginInteractiveRebaseForCommit(commits, commitIndex); err != nil {
return err
}
// check if file exists in previous commit (this command returns an error if the file doesn't exist)
if err := self.cmd.New("git cat-file -e HEAD^:" + self.cmd.Quote(fileName)).Run(); err != nil {
if err := self.os.Remove(fileName); err != nil {
2022-01-02 01:34:33 +02:00
return err
}
if err := self.workingTree.StageFile(fileName); err != nil {
return err
}
} else if err := self.workingTree.CheckoutFile("HEAD^", fileName); err != nil {
return err
}
// amend the commit
err := self.commit.AmendHead()
if err != nil {
return err
}
// continue
2022-01-09 04:36:07 +02:00
return self.ContinueRebase()
2022-01-02 01:34:33 +02:00
}
// CherryPickCommits begins an interactive rebase with the given shas being cherry picked onto HEAD
func (self *RebaseCommands) CherryPickCommits(commits []*models.Commit) error {
2022-03-20 07:19:27 +02:00
todoLines := self.BuildTodoLinesSingleAction(commits, "pick")
return self.PrepareInteractiveRebaseCommand("HEAD", todoLines, false).Run()
}
func (self *RebaseCommands) buildTodo(todoLines []TodoLine) string {
lines := slices.Map(todoLines, func(todoLine TodoLine) string {
return todoLine.ToString()
})
return strings.Join(slices.Reverse(lines), "")
}
func (self *RebaseCommands) BuildTodoLines(commits []*models.Commit, f func(*models.Commit, int) string) []TodoLine {
return slices.MapWithIndex(commits, func(commit *models.Commit, i int) TodoLine {
return TodoLine{Action: f(commit, i), Commit: commit}
})
}
func (self *RebaseCommands) BuildTodoLinesSingleAction(commits []*models.Commit, action string) []TodoLine {
return self.BuildTodoLines(commits, func(commit *models.Commit, i int) string {
return action
})
}
type TodoLine struct {
Action string
Commit *models.Commit
}
2022-01-02 01:34:33 +02:00
2022-03-20 07:19:27 +02:00
func (self *TodoLine) ToString() string {
return self.Action + " " + self.Commit.Sha + " " + self.Commit.Name + "\n"
2022-01-02 01:34:33 +02:00
}