mirror of
https://github.com/jesseduffield/lazygit.git
synced 2025-07-28 16:02:01 +03:00
Add command to find base commit for creating a fixup
This commit is contained in:
@ -64,6 +64,12 @@ func (self *FilesController) GetKeybindings(opts types.KeybindingsOpts) []*types
|
||||
Handler: self.c.Helpers().WorkingTree.HandleCommitEditorPress,
|
||||
Description: self.c.Tr.CommitChangesWithEditor,
|
||||
},
|
||||
{
|
||||
Key: opts.GetKey(opts.Config.Files.FindBaseCommitForFixup),
|
||||
Handler: self.c.Helpers().FixupHelper.HandleFindBaseCommitForFixupPress,
|
||||
Description: self.c.Tr.FindBaseCommitForFixup,
|
||||
Tooltip: self.c.Tr.FindBaseCommitForFixupTooltip,
|
||||
},
|
||||
{
|
||||
Key: opts.GetKey(opts.Config.Universal.Edit),
|
||||
Handler: self.checkSelectedFileNode(self.edit),
|
||||
|
178
pkg/gui/controllers/helpers/fixup_helper.go
Normal file
178
pkg/gui/controllers/helpers/fixup_helper.go
Normal file
@ -0,0 +1,178 @@
|
||||
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 := 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)
|
||||
}
|
||||
|
||||
if !useIndex {
|
||||
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)
|
||||
}
|
||||
|
||||
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 {
|
||||
lines := strings.Split(strings.TrimSuffix(diff, "\n"), "\n")
|
||||
|
||||
deletedLineInfos := []*deletedLineInfo{}
|
||||
|
||||
hunkHeaderRegexp := regexp.MustCompile(`@@ -(\d+)(?:,\d+)? \+\d+(?:,\d+)? @@`)
|
||||
|
||||
var filename string
|
||||
var currentLineInfo *deletedLineInfo
|
||||
finishHunk := func() {
|
||||
if currentLineInfo != nil && currentLineInfo.numLines > 0 {
|
||||
deletedLineInfos = append(deletedLineInfos, currentLineInfo)
|
||||
}
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
// 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()
|
||||
}
|
@ -33,6 +33,7 @@ type Helpers struct {
|
||||
GPG *GpgHelper
|
||||
Upstream *UpstreamHelper
|
||||
AmendHelper *AmendHelper
|
||||
FixupHelper *FixupHelper
|
||||
Commits *CommitsHelper
|
||||
Snake *SnakeHelper
|
||||
// lives in context package because our contexts need it to render to main
|
||||
@ -70,6 +71,7 @@ func NewStubHelpers() *Helpers {
|
||||
GPG: &GpgHelper{},
|
||||
Upstream: &UpstreamHelper{},
|
||||
AmendHelper: &AmendHelper{},
|
||||
FixupHelper: &FixupHelper{},
|
||||
Commits: &CommitsHelper{},
|
||||
Snake: &SnakeHelper{},
|
||||
Diff: &DiffHelper{},
|
||||
|
Reference in New Issue
Block a user