1
0
mirror of https://github.com/jesseduffield/lazygit.git synced 2025-07-15 01:34:26 +02:00

With stacked branches, create fixup commit at the end of the branch it belongs to (#3892)

- **PR Description**

When working with stacked branches, and creating a fixup commit for a
commit in one of the lower branches of the stack, the fixup was created
at the top of the stack and the user needed to move it down to the right
branch manually. This is unnecessary extra work; create it at the end of
the right branch automatically.
This commit is contained in:
Stefan Haller
2024-09-15 11:21:34 +02:00
committed by GitHub
7 changed files with 172 additions and 23 deletions

View File

@ -246,16 +246,18 @@ func (self *ChangeTodoActionsInstruction) run(common *common.Common) error {
// Takes the hash of some commit, and the hash of a fixup commit that was created // Takes the hash of some commit, and the hash of a fixup commit that was created
// at the end of the branch, then moves the fixup commit down to right after the // at the end of the branch, then moves the fixup commit down to right after the
// original commit, changing its type to "fixup" // original commit, changing its type to "fixup" (only if ChangeToFixup is true)
type MoveFixupCommitDownInstruction struct { type MoveFixupCommitDownInstruction struct {
OriginalHash string OriginalHash string
FixupHash string FixupHash string
ChangeToFixup bool
} }
func NewMoveFixupCommitDownInstruction(originalHash string, fixupHash string) Instruction { func NewMoveFixupCommitDownInstruction(originalHash string, fixupHash string, changeToFixup bool) Instruction {
return &MoveFixupCommitDownInstruction{ return &MoveFixupCommitDownInstruction{
OriginalHash: originalHash, OriginalHash: originalHash,
FixupHash: fixupHash, FixupHash: fixupHash,
ChangeToFixup: changeToFixup,
} }
} }
@ -269,7 +271,7 @@ func (self *MoveFixupCommitDownInstruction) SerializedInstructions() string {
func (self *MoveFixupCommitDownInstruction) run(common *common.Common) error { func (self *MoveFixupCommitDownInstruction) run(common *common.Common) error {
return handleInteractiveRebase(common, func(path string) error { return handleInteractiveRebase(common, func(path string) error {
return utils.MoveFixupCommitDown(path, self.OriginalHash, self.FixupHash, getCommentChar()) return utils.MoveFixupCommitDown(path, self.OriginalHash, self.FixupHash, self.ChangeToFixup, getCommentChar())
}) })
} }

View File

@ -284,6 +284,11 @@ func (self *RebaseCommands) GitRebaseEditTodo(todosFileContent []byte) error {
return cmdObj.Run() return cmdObj.Run()
} }
func (self *RebaseCommands) getHashOfLastCommitMade() (string, error) {
cmdArgs := NewGitCmd("rev-parse").Arg("--verify", "HEAD").ToArgv()
return self.cmd.New(cmdArgs).RunWithOutput()
}
// AmendTo amends the given commit with whatever files are staged // AmendTo amends the given commit with whatever files are staged
func (self *RebaseCommands) AmendTo(commits []*models.Commit, commitIndex int) error { func (self *RebaseCommands) AmendTo(commits []*models.Commit, commitIndex int) error {
commit := commits[commitIndex] commit := commits[commitIndex]
@ -292,9 +297,7 @@ func (self *RebaseCommands) AmendTo(commits []*models.Commit, commitIndex int) e
return err return err
} }
// Get the hash of the commit we just created fixupHash, err := self.getHashOfLastCommitMade()
cmdArgs := NewGitCmd("rev-parse").Arg("--verify", "HEAD").ToArgv()
fixupHash, err := self.cmd.New(cmdArgs).RunWithOutput()
if err != nil { if err != nil {
return err return err
} }
@ -302,7 +305,20 @@ func (self *RebaseCommands) AmendTo(commits []*models.Commit, commitIndex int) e
return self.PrepareInteractiveRebaseCommand(PrepareInteractiveRebaseCommandOpts{ return self.PrepareInteractiveRebaseCommand(PrepareInteractiveRebaseCommandOpts{
baseHashOrRoot: getBaseHashOrRoot(commits, commitIndex+1), baseHashOrRoot: getBaseHashOrRoot(commits, commitIndex+1),
overrideEditor: true, overrideEditor: true,
instruction: daemon.NewMoveFixupCommitDownInstruction(commit.Hash, fixupHash), instruction: daemon.NewMoveFixupCommitDownInstruction(commit.Hash, fixupHash, true),
}).Run()
}
func (self *RebaseCommands) MoveFixupCommitDown(commits []*models.Commit, targetCommitIndex int) error {
fixupHash, err := self.getHashOfLastCommitMade()
if err != nil {
return err
}
return self.PrepareInteractiveRebaseCommand(PrepareInteractiveRebaseCommandOpts{
baseHashOrRoot: getBaseHashOrRoot(commits, targetCommitIndex+1),
overrideEditor: true,
instruction: daemon.NewMoveFixupCommitDownInstruction(commits[targetCommitIndex].Hash, fixupHash, false),
}).Run() }).Run()
} }

View File

@ -895,6 +895,10 @@ func (self *LocalCommitsController) createFixupCommit(commit *models.Commit) err
return err return err
} }
if err := self.moveFixupCommitToOwnerStackedBranch(commit); err != nil {
return err
}
self.context().MoveSelectedLine(1) self.context().MoveSelectedLine(1)
return self.c.Refresh(types.RefreshOptions{Mode: types.SYNC}) return self.c.Refresh(types.RefreshOptions{Mode: types.SYNC})
}) })
@ -924,6 +928,50 @@ func (self *LocalCommitsController) createFixupCommit(commit *models.Commit) err
}) })
} }
func (self *LocalCommitsController) moveFixupCommitToOwnerStackedBranch(targetCommit *models.Commit) error {
if self.c.Git().Version.IsOlderThan(2, 38, 0) {
// Git 2.38.0 introduced the `rebase.updateRefs` config option. Don't
// move the commit down with older versions, as it would break the stack.
return nil
}
if self.c.Git().Status.WorkingTreeState() != enums.REBASE_MODE_NONE {
// Can't move commits while rebasing
return nil
}
if targetCommit.Status == models.StatusMerged {
// Target commit is already on main. It's a bit questionable that we
// allow creating a fixup commit for it in the first place, but we
// always did, so why restrict that now; however, it doesn't make sense
// to move the created fixup commit down in that case.
return nil
}
if !self.c.Git().Config.GetRebaseUpdateRefs() {
// If the user has disabled rebase.updateRefs, we don't move the fixup
// because this would break the stack of branches (presumably they like
// to manage it themselves manually, or something).
return nil
}
headOfOwnerBranchIdx := -1
for i := self.context().GetSelectedLineIdx(); i > 0; i-- {
if lo.SomeBy(self.c.Model().Branches, func(b *models.Branch) bool {
return b.CommitHash == self.c.Model().Commits[i].Hash
}) {
headOfOwnerBranchIdx = i
break
}
}
if headOfOwnerBranchIdx == -1 {
return nil
}
return self.c.Git().Rebase.MoveFixupCommitDown(self.c.Model().Commits, headOfOwnerBranchIdx)
}
func (self *LocalCommitsController) createAmendCommit(commit *models.Commit, includeFileChanges bool) error { func (self *LocalCommitsController) createAmendCommit(commit *models.Commit, includeFileChanges bool) error {
commitMessage, err := self.c.Git().Commit.GetCommitMessage(commit.Hash) commitMessage, err := self.c.Git().Commit.GetCommitMessage(commit.Hash)
if err != nil { if err != nil {
@ -947,6 +995,10 @@ func (self *LocalCommitsController) createAmendCommit(commit *models.Commit, inc
return err return err
} }
if err := self.moveFixupCommitToOwnerStackedBranch(commit); err != nil {
return err
}
self.context().MoveSelectedLine(1) self.context().MoveSelectedLine(1)
return self.c.Refresh(types.RefreshOptions{Mode: types.SYNC}) return self.c.Refresh(types.RefreshOptions{Mode: types.SYNC})
}) })

View File

@ -0,0 +1,53 @@
package commit
import (
"github.com/jesseduffield/lazygit/pkg/config"
. "github.com/jesseduffield/lazygit/pkg/integration/components"
)
var CreateFixupCommitInBranchStack = NewIntegrationTest(NewIntegrationTestArgs{
Description: "Create a fixup commit in a stack of branches, verify that it is created at the end of the branch it belongs to",
ExtraCmdArgs: []string{},
Skip: false,
GitVersion: AtLeast("2.38.0"),
SetupConfig: func(config *config.AppConfig) {},
SetupRepo: func(shell *Shell) {
shell.NewBranch("branch1")
shell.EmptyCommit("branch1 commit 1")
shell.EmptyCommit("branch1 commit 2")
shell.EmptyCommit("branch1 commit 3")
shell.NewBranch("branch2")
shell.EmptyCommit("branch2 commit 1")
shell.EmptyCommit("branch2 commit 2")
shell.CreateFileAndAdd("fixup-file", "fixup content")
shell.SetConfig("rebase.updateRefs", "true")
},
Run: func(t *TestDriver, keys config.KeybindingConfig) {
t.Views().Commits().
Focus().
Lines(
Contains("CI ◯ branch2 commit 2"),
Contains("CI ◯ branch2 commit 1"),
Contains("CI ◯ * branch1 commit 3"),
Contains("CI ◯ branch1 commit 2"),
Contains("CI ◯ branch1 commit 1"),
).
NavigateToLine(Contains("branch1 commit 2")).
Press(keys.Commits.CreateFixupCommit).
Tap(func() {
t.ExpectPopup().Menu().
Title(Equals("Create fixup commit")).
Select(Contains("fixup! commit")).
Confirm()
}).
Lines(
Contains("CI ◯ branch2 commit 2"),
Contains("CI ◯ branch2 commit 1"),
Contains("CI ◯ * fixup! branch1 commit 2"),
Contains("CI ◯ branch1 commit 3"),
Contains("CI ◯ branch1 commit 2"),
Contains("CI ◯ branch1 commit 1"),
)
},
})

View File

@ -87,6 +87,7 @@ var tests = []*components.IntegrationTest{
commit.CommitWithNonMatchingBranchName, commit.CommitWithNonMatchingBranchName,
commit.CommitWithPrefix, commit.CommitWithPrefix,
commit.CreateAmendCommit, commit.CreateAmendCommit,
commit.CreateFixupCommitInBranchStack,
commit.CreateTag, commit.CreateTag,
commit.DiscardOldFileChanges, commit.DiscardOldFileChanges,
commit.FindBaseCommitForFixup, commit.FindBaseCommitForFixup,

View File

@ -221,13 +221,13 @@ func moveTodosUp(todos []todo.Todo, todosToMove []Todo) ([]todo.Todo, error) {
return todos, nil return todos, nil
} }
func MoveFixupCommitDown(fileName string, originalHash string, fixupHash string, commentChar byte) error { func MoveFixupCommitDown(fileName string, originalHash string, fixupHash string, changeToFixup bool, commentChar byte) error {
todos, err := ReadRebaseTodoFile(fileName, commentChar) todos, err := ReadRebaseTodoFile(fileName, commentChar)
if err != nil { if err != nil {
return err return err
} }
newTodos, err := moveFixupCommitDown(todos, originalHash, fixupHash) newTodos, err := moveFixupCommitDown(todos, originalHash, fixupHash, changeToFixup)
if err != nil { if err != nil {
return err return err
} }
@ -235,7 +235,7 @@ func MoveFixupCommitDown(fileName string, originalHash string, fixupHash string,
return WriteRebaseTodoFile(fileName, newTodos, commentChar) return WriteRebaseTodoFile(fileName, newTodos, commentChar)
} }
func moveFixupCommitDown(todos []todo.Todo, originalHash string, fixupHash string) ([]todo.Todo, error) { func moveFixupCommitDown(todos []todo.Todo, originalHash string, fixupHash string, changeToFixup bool) ([]todo.Todo, error) {
isOriginal := func(t todo.Todo) bool { isOriginal := func(t todo.Todo) bool {
return (t.Command == todo.Pick || t.Command == todo.Merge) && equalHash(t.Commit, originalHash) return (t.Command == todo.Pick || t.Command == todo.Merge) && equalHash(t.Commit, originalHash)
} }
@ -259,7 +259,9 @@ func moveFixupCommitDown(todos []todo.Todo, originalHash string, fixupHash strin
newTodos := MoveElement(todos, fixupIndex, originalIndex+1) newTodos := MoveElement(todos, fixupIndex, originalIndex+1)
if changeToFixup {
newTodos[originalIndex+1].Command = todo.Fixup newTodos[originalIndex+1].Command = todo.Fixup
}
return newTodos, nil return newTodos, nil
} }

View File

@ -266,23 +266,40 @@ func TestRebaseCommands_moveFixupCommitDown(t *testing.T) {
todos []todo.Todo todos []todo.Todo
originalHash string originalHash string
fixupHash string fixupHash string
changeToFixup bool
expectedTodos []todo.Todo expectedTodos []todo.Todo
expectedErr error expectedErr error
}{ }{
{ {
name: "fixup commit is the last commit", name: "fixup commit is the last commit (change to fixup)",
todos: []todo.Todo{ todos: []todo.Todo{
{Command: todo.Pick, Commit: "original"}, {Command: todo.Pick, Commit: "original"},
{Command: todo.Pick, Commit: "fixup"}, {Command: todo.Pick, Commit: "fixup"},
}, },
originalHash: "original", originalHash: "original",
fixupHash: "fixup", fixupHash: "fixup",
changeToFixup: true,
expectedTodos: []todo.Todo{ expectedTodos: []todo.Todo{
{Command: todo.Pick, Commit: "original"}, {Command: todo.Pick, Commit: "original"},
{Command: todo.Fixup, Commit: "fixup"}, {Command: todo.Fixup, Commit: "fixup"},
}, },
expectedErr: nil, expectedErr: nil,
}, },
{
name: "fixup commit is the last commit (don't change to fixup)",
todos: []todo.Todo{
{Command: todo.Pick, Commit: "original"},
{Command: todo.Pick, Commit: "fixup"},
},
originalHash: "original",
fixupHash: "fixup",
changeToFixup: false,
expectedTodos: []todo.Todo{
{Command: todo.Pick, Commit: "original"},
{Command: todo.Pick, Commit: "fixup"},
},
expectedErr: nil,
},
{ {
name: "fixup commit is separated from original commit", name: "fixup commit is separated from original commit",
todos: []todo.Todo{ todos: []todo.Todo{
@ -292,6 +309,7 @@ func TestRebaseCommands_moveFixupCommitDown(t *testing.T) {
}, },
originalHash: "original", originalHash: "original",
fixupHash: "fixup", fixupHash: "fixup",
changeToFixup: true,
expectedTodos: []todo.Todo{ expectedTodos: []todo.Todo{
{Command: todo.Pick, Commit: "original"}, {Command: todo.Pick, Commit: "original"},
{Command: todo.Fixup, Commit: "fixup"}, {Command: todo.Fixup, Commit: "fixup"},
@ -308,6 +326,7 @@ func TestRebaseCommands_moveFixupCommitDown(t *testing.T) {
}, },
originalHash: "original", originalHash: "original",
fixupHash: "fixup", fixupHash: "fixup",
changeToFixup: true,
expectedTodos: []todo.Todo{ expectedTodos: []todo.Todo{
{Command: todo.Merge, Commit: "original"}, {Command: todo.Merge, Commit: "original"},
{Command: todo.Fixup, Commit: "fixup"}, {Command: todo.Fixup, Commit: "fixup"},
@ -324,6 +343,7 @@ func TestRebaseCommands_moveFixupCommitDown(t *testing.T) {
}, },
originalHash: "original", originalHash: "original",
fixupHash: "fixup", fixupHash: "fixup",
changeToFixup: true,
expectedTodos: nil, expectedTodos: nil,
expectedErr: errors.New("Expected exactly one original hash, found 2"), expectedErr: errors.New("Expected exactly one original hash, found 2"),
}, },
@ -336,6 +356,7 @@ func TestRebaseCommands_moveFixupCommitDown(t *testing.T) {
}, },
originalHash: "original", originalHash: "original",
fixupHash: "fixup", fixupHash: "fixup",
changeToFixup: true,
expectedTodos: nil, expectedTodos: nil,
expectedErr: errors.New("Expected exactly one fixup hash, found 2"), expectedErr: errors.New("Expected exactly one fixup hash, found 2"),
}, },
@ -346,6 +367,7 @@ func TestRebaseCommands_moveFixupCommitDown(t *testing.T) {
}, },
originalHash: "original", originalHash: "original",
fixupHash: "fixup", fixupHash: "fixup",
changeToFixup: true,
expectedTodos: nil, expectedTodos: nil,
expectedErr: errors.New("Expected exactly one fixup hash, found 0"), expectedErr: errors.New("Expected exactly one fixup hash, found 0"),
}, },
@ -356,6 +378,7 @@ func TestRebaseCommands_moveFixupCommitDown(t *testing.T) {
}, },
originalHash: "original", originalHash: "original",
fixupHash: "fixup", fixupHash: "fixup",
changeToFixup: true,
expectedTodos: nil, expectedTodos: nil,
expectedErr: errors.New("Expected exactly one original hash, found 0"), expectedErr: errors.New("Expected exactly one original hash, found 0"),
}, },
@ -363,7 +386,7 @@ func TestRebaseCommands_moveFixupCommitDown(t *testing.T) {
for _, scenario := range scenarios { for _, scenario := range scenarios {
t.Run(scenario.name, func(t *testing.T) { t.Run(scenario.name, func(t *testing.T) {
actualTodos, actualErr := moveFixupCommitDown(scenario.todos, scenario.originalHash, scenario.fixupHash) actualTodos, actualErr := moveFixupCommitDown(scenario.todos, scenario.originalHash, scenario.fixupHash, scenario.changeToFixup)
if scenario.expectedErr == nil { if scenario.expectedErr == nil {
assert.NoError(t, actualErr) assert.NoError(t, actualErr)