mirror of
https://github.com/jesseduffield/lazygit.git
synced 2025-07-31 14:24:25 +03:00
Keep the same line selected after squashing fixup commits
This uses a bit of a heuristic that is hopefully correct most of the time.
This commit is contained in:
@ -2,6 +2,7 @@ package controllers
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/fsmiamoto/git-todo-parser/todo"
|
"github.com/fsmiamoto/git-todo-parser/todo"
|
||||||
"github.com/go-errors/errors"
|
"github.com/go-errors/errors"
|
||||||
@ -847,37 +848,89 @@ func (self *LocalCommitsController) squashFixupCommits() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (self *LocalCommitsController) squashAllFixupsAboveSelectedCommit(commit *models.Commit) error {
|
func (self *LocalCommitsController) squashAllFixupsAboveSelectedCommit(commit *models.Commit) error {
|
||||||
return self.squashFixupsImpl(commit)
|
return self.squashFixupsImpl(commit, self.context().GetSelectedLineIdx())
|
||||||
}
|
}
|
||||||
|
|
||||||
func (self *LocalCommitsController) squashAllFixupsInCurrentBranch() error {
|
func (self *LocalCommitsController) squashAllFixupsInCurrentBranch() error {
|
||||||
commit, err := self.findCommitForSquashFixupsInCurrentBranch()
|
commit, rebaseStartIdx, err := self.findCommitForSquashFixupsInCurrentBranch()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return self.c.Error(err)
|
return self.c.Error(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return self.squashFixupsImpl(commit)
|
return self.squashFixupsImpl(commit, rebaseStartIdx)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (self *LocalCommitsController) squashFixupsImpl(commit *models.Commit) error {
|
func (self *LocalCommitsController) squashFixupsImpl(commit *models.Commit, rebaseStartIdx int) error {
|
||||||
return self.c.WithWaitingStatus(self.c.Tr.SquashingStatus, func(gocui.Task) error {
|
selectionOffset := countSquashableCommitsAbove(self.c.Model().Commits, self.context().GetSelectedLineIdx(), rebaseStartIdx)
|
||||||
|
return self.c.WithWaitingStatusSync(self.c.Tr.SquashingStatus, func() error {
|
||||||
self.c.LogAction(self.c.Tr.Actions.SquashAllAboveFixupCommits)
|
self.c.LogAction(self.c.Tr.Actions.SquashAllAboveFixupCommits)
|
||||||
err := self.c.Git().Rebase.SquashAllAboveFixupCommits(commit)
|
err := self.c.Git().Rebase.SquashAllAboveFixupCommits(commit)
|
||||||
return self.c.Helpers().MergeAndRebase.CheckMergeOrRebase(err)
|
self.context().MoveSelectedLine(-selectionOffset)
|
||||||
|
return self.c.Helpers().MergeAndRebase.CheckMergeOrRebaseWithRefreshOptions(
|
||||||
|
err, types.RefreshOptions{Mode: types.SYNC})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (self *LocalCommitsController) findCommitForSquashFixupsInCurrentBranch() (*models.Commit, error) {
|
func (self *LocalCommitsController) findCommitForSquashFixupsInCurrentBranch() (*models.Commit, int, error) {
|
||||||
commits := self.c.Model().Commits
|
commits := self.c.Model().Commits
|
||||||
_, index, ok := lo.FindIndexOf(commits, func(c *models.Commit) bool {
|
_, index, ok := lo.FindIndexOf(commits, func(c *models.Commit) bool {
|
||||||
return c.IsMerge() || c.Status == models.StatusMerged
|
return c.IsMerge() || c.Status == models.StatusMerged
|
||||||
})
|
})
|
||||||
|
|
||||||
if !ok || index == 0 {
|
if !ok || index == 0 {
|
||||||
return nil, errors.New(self.c.Tr.CannotSquashCommitsInCurrentBranch)
|
return nil, -1, errors.New(self.c.Tr.CannotSquashCommitsInCurrentBranch)
|
||||||
}
|
}
|
||||||
|
|
||||||
return commits[index-1], nil
|
return commits[index-1], index - 1, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Anticipate how many commits above the selectedIdx are going to get squashed
|
||||||
|
// by the SquashAllAboveFixupCommits call, so that we can adjust the selection
|
||||||
|
// afterwards. Let's hope we're matching git's behavior correctly here.
|
||||||
|
func countSquashableCommitsAbove(commits []*models.Commit, selectedIdx int, rebaseStartIdx int) int {
|
||||||
|
result := 0
|
||||||
|
|
||||||
|
// For each commit _above_ the selection, ...
|
||||||
|
for i, commit := range commits[0:selectedIdx] {
|
||||||
|
// ... see if it is a fixup commit, and get the base subject it applies to
|
||||||
|
if baseSubject, isFixup := isFixupCommit(commit.Name); isFixup {
|
||||||
|
// Then, for each commit after the fixup, up to and including the
|
||||||
|
// rebase start commit, see if we find the base commit
|
||||||
|
for _, baseCommit := range commits[i+1 : rebaseStartIdx+1] {
|
||||||
|
if strings.HasPrefix(baseCommit.Name, baseSubject) {
|
||||||
|
result++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check whether the given subject line is the subject of a fixup commit, and
|
||||||
|
// returns (trimmedSubject, true) if so (where trimmedSubject is the subject
|
||||||
|
// with all fixup prefixes removed), or (subject, false) if not.
|
||||||
|
func isFixupCommit(subject string) (string, bool) {
|
||||||
|
prefixes := []string{"fixup! ", "squash! ", "amend! "}
|
||||||
|
trimPrefix := func(s string) (string, bool) {
|
||||||
|
for _, prefix := range prefixes {
|
||||||
|
if strings.HasPrefix(s, prefix) {
|
||||||
|
return strings.TrimPrefix(s, prefix), true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return s, false
|
||||||
|
}
|
||||||
|
|
||||||
|
if subject, wasTrimmed := trimPrefix(subject); wasTrimmed {
|
||||||
|
for {
|
||||||
|
// handle repeated prefixes like "fixup! amend! fixup! Subject"
|
||||||
|
if subject, wasTrimmed = trimPrefix(subject); !wasTrimmed {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return subject, true
|
||||||
|
}
|
||||||
|
|
||||||
|
return subject, false
|
||||||
}
|
}
|
||||||
|
|
||||||
func (self *LocalCommitsController) createTag(commit *models.Commit) error {
|
func (self *LocalCommitsController) createTag(commit *models.Commit) error {
|
||||||
@ -1070,7 +1123,7 @@ func (self *LocalCommitsController) canFindCommitForQuickStart() *types.Disabled
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (self *LocalCommitsController) canFindCommitForSquashFixupsInCurrentBranch() *types.DisabledReason {
|
func (self *LocalCommitsController) canFindCommitForSquashFixupsInCurrentBranch() *types.DisabledReason {
|
||||||
if _, err := self.findCommitForSquashFixupsInCurrentBranch(); err != nil {
|
if _, _, err := self.findCommitForSquashFixupsInCurrentBranch(); err != nil {
|
||||||
return &types.DisabledReason{Text: err.Error()}
|
return &types.DisabledReason{Text: err.Error()}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
141
pkg/gui/controllers/local_commits_controller_test.go
Normal file
141
pkg/gui/controllers/local_commits_controller_test.go
Normal file
@ -0,0 +1,141 @@
|
|||||||
|
package controllers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/jesseduffield/lazygit/pkg/commands/models"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Test_countSquashableCommitsAbove(t *testing.T) {
|
||||||
|
scenarios := []struct {
|
||||||
|
name string
|
||||||
|
commits []*models.Commit
|
||||||
|
selectedIdx int
|
||||||
|
rebaseStartIdx int
|
||||||
|
expectedResult int
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "no squashable commits",
|
||||||
|
commits: []*models.Commit{
|
||||||
|
{Name: "abc"},
|
||||||
|
{Name: "def"},
|
||||||
|
{Name: "ghi"},
|
||||||
|
},
|
||||||
|
selectedIdx: 2,
|
||||||
|
rebaseStartIdx: 2,
|
||||||
|
expectedResult: 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "some squashable commits, including for the selected commit",
|
||||||
|
commits: []*models.Commit{
|
||||||
|
{Name: "fixup! def"},
|
||||||
|
{Name: "fixup! ghi"},
|
||||||
|
{Name: "abc"},
|
||||||
|
{Name: "def"},
|
||||||
|
{Name: "ghi"},
|
||||||
|
},
|
||||||
|
selectedIdx: 4,
|
||||||
|
rebaseStartIdx: 4,
|
||||||
|
expectedResult: 2,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "base commit is below rebase start",
|
||||||
|
commits: []*models.Commit{
|
||||||
|
{Name: "fixup! def"},
|
||||||
|
{Name: "abc"},
|
||||||
|
{Name: "def"},
|
||||||
|
},
|
||||||
|
selectedIdx: 1,
|
||||||
|
rebaseStartIdx: 1,
|
||||||
|
expectedResult: 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "base commit does not exist at all",
|
||||||
|
commits: []*models.Commit{
|
||||||
|
{Name: "fixup! xyz"},
|
||||||
|
{Name: "abc"},
|
||||||
|
{Name: "def"},
|
||||||
|
},
|
||||||
|
selectedIdx: 2,
|
||||||
|
rebaseStartIdx: 2,
|
||||||
|
expectedResult: 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "selected commit is in the middle of fixups",
|
||||||
|
commits: []*models.Commit{
|
||||||
|
{Name: "fixup! def"},
|
||||||
|
{Name: "abc"},
|
||||||
|
{Name: "fixup! ghi"},
|
||||||
|
{Name: "def"},
|
||||||
|
{Name: "ghi"},
|
||||||
|
},
|
||||||
|
selectedIdx: 1,
|
||||||
|
rebaseStartIdx: 4,
|
||||||
|
expectedResult: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "selected commit is after rebase start",
|
||||||
|
commits: []*models.Commit{
|
||||||
|
{Name: "fixup! def"},
|
||||||
|
{Name: "abc"},
|
||||||
|
{Name: "def"},
|
||||||
|
{Name: "ghi"},
|
||||||
|
},
|
||||||
|
selectedIdx: 3,
|
||||||
|
rebaseStartIdx: 2,
|
||||||
|
expectedResult: 1,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, s := range scenarios {
|
||||||
|
t.Run(s.name, func(t *testing.T) {
|
||||||
|
assert.Equal(t, s.expectedResult, countSquashableCommitsAbove(s.commits, s.selectedIdx, s.rebaseStartIdx))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_isFixupCommit(t *testing.T) {
|
||||||
|
scenarios := []struct {
|
||||||
|
subject string
|
||||||
|
expectedTrimmedSubject string
|
||||||
|
expectedIsFixup bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
subject: "Bla",
|
||||||
|
expectedTrimmedSubject: "Bla",
|
||||||
|
expectedIsFixup: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
subject: "fixup Bla",
|
||||||
|
expectedTrimmedSubject: "fixup Bla",
|
||||||
|
expectedIsFixup: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
subject: "fixup! Bla",
|
||||||
|
expectedTrimmedSubject: "Bla",
|
||||||
|
expectedIsFixup: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
subject: "fixup! fixup! Bla",
|
||||||
|
expectedTrimmedSubject: "Bla",
|
||||||
|
expectedIsFixup: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
subject: "amend! squash! Bla",
|
||||||
|
expectedTrimmedSubject: "Bla",
|
||||||
|
expectedIsFixup: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
subject: "fixup!",
|
||||||
|
expectedTrimmedSubject: "fixup!",
|
||||||
|
expectedIsFixup: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, s := range scenarios {
|
||||||
|
t.Run(s.subject, func(t *testing.T) {
|
||||||
|
trimmedSubject, isFixupCommit := isFixupCommit(s.subject)
|
||||||
|
assert.Equal(t, s.expectedTrimmedSubject, trimmedSubject)
|
||||||
|
assert.Equal(t, s.expectedIsFixup, isFixupCommit)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
@ -46,10 +46,9 @@ var SquashFixupsAbove = NewIntegrationTest(NewIntegrationTestArgs{
|
|||||||
}).
|
}).
|
||||||
Lines(
|
Lines(
|
||||||
Contains("commit 03"),
|
Contains("commit 03"),
|
||||||
Contains("commit 02"),
|
Contains("commit 02").IsSelected(),
|
||||||
Contains("commit 01").IsSelected(), // wrong, we want the previous line
|
Contains("commit 01"),
|
||||||
).
|
)
|
||||||
SelectPreviousItem()
|
|
||||||
|
|
||||||
t.Views().Main().
|
t.Views().Main().
|
||||||
Content(Contains("fixup content"))
|
Content(Contains("fixup content"))
|
||||||
|
@ -45,11 +45,10 @@ var SquashFixupsInCurrentBranch = NewIntegrationTest(NewIntegrationTestArgs{
|
|||||||
}).
|
}).
|
||||||
Lines(
|
Lines(
|
||||||
Contains("commit 02"),
|
Contains("commit 02"),
|
||||||
Contains("commit 01"),
|
Contains("commit 01").IsSelected(),
|
||||||
Contains("fixup! master commit").IsSelected(), // wrong, we want the previous line
|
Contains("fixup! master commit"),
|
||||||
Contains("master commit"),
|
Contains("master commit"),
|
||||||
).
|
)
|
||||||
NavigateToLine(Contains("commit 01"))
|
|
||||||
|
|
||||||
t.Views().Main().
|
t.Views().Main().
|
||||||
Content(Contains("fixup content"))
|
Content(Contains("fixup content"))
|
||||||
|
Reference in New Issue
Block a user