mirror of
https://github.com/jesseduffield/lazygit.git
synced 2025-04-13 11:50:28 +02:00
The algorithm works by blaming the deleted lines, so if a hunk contains only added lines, we can only hope that it also belongs in the same commit. Warn the user about this. Note: the warning might be overly agressive, we'll have to see if this is annoying. The reason is that it depends on the diff context size whether added lines go into their own hunk or are grouped together with other added or deleted lines into one hunk. However, our algorithm uses a diff context size of 0, because that makes it easiest to parse the diff; this results in hunks having only added lines more often than what the user sees. For example, moving a line of code down by two lines will likely result in a single hunk for the user, but in two hunks for our algorithm. On the other hand, being this strict makes the warning consistent. We could consider using the user's diff context size in the algorithm, but then it would depend on the current context size whether the warning appears, which could be confusing. Plus, it would make the algorithm quite a bit more complicated.
198 lines
5.4 KiB
Go
198 lines
5.4 KiB
Go
package helpers
|
|
|
|
import (
|
|
"regexp"
|
|
"strings"
|
|
"sync"
|
|
|
|
"github.com/jesseduffield/generics/set"
|
|
"github.com/jesseduffield/lazygit/pkg/commands/models"
|
|
"github.com/jesseduffield/lazygit/pkg/gui/types"
|
|
"github.com/jesseduffield/lazygit/pkg/utils"
|
|
"github.com/samber/lo"
|
|
)
|
|
|
|
type FixupHelper struct {
|
|
c *HelperCommon
|
|
}
|
|
|
|
func NewFixupHelper(
|
|
c *HelperCommon,
|
|
) *FixupHelper {
|
|
return &FixupHelper{
|
|
c: c,
|
|
}
|
|
}
|
|
|
|
type deletedLineInfo struct {
|
|
filename string
|
|
startLineIdx int
|
|
numLines int
|
|
}
|
|
|
|
func (self *FixupHelper) HandleFindBaseCommitForFixupPress() error {
|
|
diff, hasStagedChanges, err := self.getDiff()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if diff == "" {
|
|
return self.c.ErrorMsg(self.c.Tr.NoChangedFiles)
|
|
}
|
|
|
|
deletedLineInfos, hasHunksWithOnlyAddedLines := self.parseDiff(diff)
|
|
if len(deletedLineInfos) == 0 {
|
|
return self.c.ErrorMsg(self.c.Tr.NoDeletedLinesInDiff)
|
|
}
|
|
|
|
shas := self.blameDeletedLines(deletedLineInfos)
|
|
|
|
if len(shas) == 0 {
|
|
// This should never happen
|
|
return self.c.ErrorMsg(self.c.Tr.NoBaseCommitsFound)
|
|
}
|
|
if len(shas) > 1 {
|
|
subjects, err := self.c.Git().Commit.GetShasAndCommitMessagesFirstLine(shas)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
message := lo.Ternary(hasStagedChanges,
|
|
self.c.Tr.MultipleBaseCommitsFoundStaged,
|
|
self.c.Tr.MultipleBaseCommitsFoundUnstaged)
|
|
return self.c.ErrorMsg(message + "\n\n" + subjects)
|
|
}
|
|
|
|
commit, index, ok := lo.FindIndexOf(self.c.Model().Commits, func(commit *models.Commit) bool {
|
|
return commit.Sha == shas[0]
|
|
})
|
|
if !ok {
|
|
commits := self.c.Model().Commits
|
|
if commits[len(commits)-1].Status == models.StatusMerged {
|
|
// If the commit is not found, it's most likely because it's already
|
|
// merged, and more than 300 commits away. Check if the last known
|
|
// commit is already merged; if so, show the "already merged" error.
|
|
return self.c.ErrorMsg(self.c.Tr.BaseCommitIsAlreadyOnMainBranch)
|
|
}
|
|
// If we get here, the current branch must have more then 300 commits. Unlikely...
|
|
return self.c.ErrorMsg(self.c.Tr.BaseCommitIsNotInCurrentView)
|
|
}
|
|
if commit.Status == models.StatusMerged {
|
|
return self.c.ErrorMsg(self.c.Tr.BaseCommitIsAlreadyOnMainBranch)
|
|
}
|
|
|
|
doIt := func() error {
|
|
if !hasStagedChanges {
|
|
if err := self.c.Git().WorkingTree.StageAll(); err != nil {
|
|
return err
|
|
}
|
|
_ = self.c.Refresh(types.RefreshOptions{Mode: types.SYNC, Scope: []types.RefreshableView{types.FILES}})
|
|
}
|
|
|
|
self.c.Contexts().LocalCommits.SetSelectedLineIdx(index)
|
|
return self.c.PushContext(self.c.Contexts().LocalCommits)
|
|
}
|
|
|
|
if hasHunksWithOnlyAddedLines {
|
|
return self.c.Confirm(types.ConfirmOpts{
|
|
Title: self.c.Tr.FindBaseCommitForFixup,
|
|
Prompt: self.c.Tr.HunksWithOnlyAddedLinesWarning,
|
|
HandleConfirm: func() error {
|
|
return doIt()
|
|
},
|
|
})
|
|
}
|
|
|
|
return doIt()
|
|
}
|
|
|
|
func (self *FixupHelper) getDiff() (string, bool, error) {
|
|
args := []string{"-U0", "--ignore-submodules=all", "HEAD", "--"}
|
|
|
|
// Try staged changes first
|
|
hasStagedChanges := true
|
|
diff, err := self.c.Git().Diff.DiffIndexCmdObj(append([]string{"--cached"}, args...)...).RunWithOutput()
|
|
|
|
if err == nil && diff == "" {
|
|
hasStagedChanges = false
|
|
// If there are no staged changes, try unstaged changes
|
|
diff, err = self.c.Git().Diff.DiffIndexCmdObj(args...).RunWithOutput()
|
|
}
|
|
|
|
return diff, hasStagedChanges, err
|
|
}
|
|
|
|
func (self *FixupHelper) parseDiff(diff string) ([]*deletedLineInfo, bool) {
|
|
lines := strings.Split(strings.TrimSuffix(diff, "\n"), "\n")
|
|
|
|
deletedLineInfos := []*deletedLineInfo{}
|
|
hasHunksWithOnlyAddedLines := false
|
|
|
|
hunkHeaderRegexp := regexp.MustCompile(`@@ -(\d+)(?:,\d+)? \+\d+(?:,\d+)? @@`)
|
|
|
|
var filename string
|
|
var currentLineInfo *deletedLineInfo
|
|
finishHunk := func() {
|
|
if currentLineInfo != nil {
|
|
if currentLineInfo.numLines > 0 {
|
|
deletedLineInfos = append(deletedLineInfos, currentLineInfo)
|
|
} else {
|
|
hasHunksWithOnlyAddedLines = true
|
|
}
|
|
}
|
|
}
|
|
for _, line := range lines {
|
|
if strings.HasPrefix(line, "diff --git") {
|
|
finishHunk()
|
|
currentLineInfo = nil
|
|
} else if strings.HasPrefix(line, "--- ") {
|
|
// For some reason, the line ends with a tab character if the file
|
|
// name contains spaces
|
|
filename = strings.TrimRight(line[6:], "\t")
|
|
} else if strings.HasPrefix(line, "@@ ") {
|
|
finishHunk()
|
|
match := hunkHeaderRegexp.FindStringSubmatch(line)
|
|
startIdx := utils.MustConvertToInt(match[1])
|
|
currentLineInfo = &deletedLineInfo{filename, startIdx, 0}
|
|
} else if currentLineInfo != nil && line[0] == '-' {
|
|
currentLineInfo.numLines++
|
|
}
|
|
}
|
|
finishHunk()
|
|
|
|
return deletedLineInfos, hasHunksWithOnlyAddedLines
|
|
}
|
|
|
|
// returns the list of commit hashes that introduced the lines which have now been deleted
|
|
func (self *FixupHelper) blameDeletedLines(deletedLineInfos []*deletedLineInfo) []string {
|
|
var wg sync.WaitGroup
|
|
shaChan := make(chan string)
|
|
|
|
for _, info := range deletedLineInfos {
|
|
wg.Add(1)
|
|
go func(info *deletedLineInfo) {
|
|
defer wg.Done()
|
|
|
|
blameOutput, err := self.c.Git().Blame.BlameLineRange(info.filename, "HEAD", info.startLineIdx, info.numLines)
|
|
if err != nil {
|
|
self.c.Log.Errorf("Error blaming file '%s': %v", info.filename, err)
|
|
return
|
|
}
|
|
blameLines := strings.Split(strings.TrimSuffix(blameOutput, "\n"), "\n")
|
|
for _, line := range blameLines {
|
|
shaChan <- strings.Split(line, " ")[0]
|
|
}
|
|
}(info)
|
|
}
|
|
|
|
go func() {
|
|
wg.Wait()
|
|
close(shaChan)
|
|
}()
|
|
|
|
result := set.New[string]()
|
|
for sha := range shaChan {
|
|
result.Add(sha)
|
|
}
|
|
|
|
return result.ToSlice()
|
|
}
|