1
0
mirror of https://github.com/jesseduffield/lazygit.git synced 2025-01-10 04:07:18 +02:00
lazygit/pkg/gui/controllers/helpers/fixup_helper.go
Jesse Duffield 54bd94ad24 Add SetSelection function for list contexts and use it in most places
The only time we should call SetSelectedLineIdx is when we are happy for a
select range to be retained which means things like moving the selected line
index to top top/bottom or up/down a page as the user navigates.

But in every other case we should now call SetSelection because that will
set the selected index and cancel the range which is almost always what we
want.
2024-01-19 10:47:21 +11:00

198 lines
5.3 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.SetSelection(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()
}