mirror of
https://github.com/jesseduffield/lazygit.git
synced 2026-09-11 08:06:25 -04:00
341 lines
11 KiB
Go
341 lines
11 KiB
Go
package helpers
|
|
|
|
import (
|
|
"fmt"
|
|
"strings"
|
|
|
|
"github.com/jesseduffield/lazygit/pkg/commands/git_commands"
|
|
"github.com/jesseduffield/lazygit/pkg/commands/models"
|
|
"github.com/jesseduffield/lazygit/pkg/commands/patch"
|
|
"github.com/jesseduffield/lazygit/pkg/gui/context"
|
|
"github.com/jesseduffield/lazygit/pkg/gui/modes/diffing"
|
|
"github.com/jesseduffield/lazygit/pkg/gui/style"
|
|
"github.com/jesseduffield/lazygit/pkg/gui/types"
|
|
"github.com/jesseduffield/lazygit/pkg/utils"
|
|
"github.com/samber/lo"
|
|
)
|
|
|
|
type DiffHelper struct {
|
|
c *HelperCommon
|
|
// diffLineHelper says how a diff for the main view is to be produced, which depends
|
|
// on whether the focused main view could act on what a diff renderer would make of it.
|
|
diffLineHelper *DiffLineHelper
|
|
|
|
// Diffs of the messages of "amend!" commits, keyed by everything that
|
|
// shapes them: the two commits, and the diff renderer and width they were
|
|
// rendered by. An empty diff means that the commit doesn't change the
|
|
// message. Only accessed on the UI thread, while rendering the main view.
|
|
commitMessageDiffs map[string]string
|
|
}
|
|
|
|
func NewDiffHelper(c *HelperCommon, diffLineHelper *DiffLineHelper) *DiffHelper {
|
|
return &DiffHelper{
|
|
c: c,
|
|
diffLineHelper: diffLineHelper,
|
|
commitMessageDiffs: make(map[string]string),
|
|
}
|
|
}
|
|
|
|
func (self *DiffHelper) DiffArgs() []string {
|
|
output := []string{"--stat", "-p", self.c.Modes().Diffing.Ref}
|
|
|
|
right := self.currentDiffTerminal()
|
|
if right != "" {
|
|
output = append(output, right)
|
|
}
|
|
|
|
if self.c.Modes().Diffing.Reverse {
|
|
output = append(output, "-R")
|
|
}
|
|
|
|
output = append(output, "--")
|
|
|
|
file := self.currentlySelectedFilename()
|
|
if file != "" {
|
|
output = append(output, file)
|
|
} else if self.c.Modes().Filtering.Active() {
|
|
output = append(output, self.c.Modes().Filtering.GetPath())
|
|
}
|
|
|
|
return output
|
|
}
|
|
|
|
// Returns an update task that can be passed to RenderToMainViews to render a
|
|
// diff for the selected commit(s). We need to pass both the selected commit
|
|
// and the refRange for a range selection. If the refRange is nil (meaning that
|
|
// either there's no range, or it can't be diffed for some reason), then we want
|
|
// to fall back to rendering the diff for the single commit.
|
|
// In addition, we need to pass the list of all commits; this is needed for
|
|
// showing the commit message diff for "amend!" commits.
|
|
func (self *DiffHelper) GetUpdateTaskForRenderingCommitsDiff(
|
|
commits []*models.Commit,
|
|
commit *models.Commit,
|
|
refRange *types.RefRange,
|
|
) types.UpdateTask {
|
|
mode := self.diffLineHelper.MainViewDiffMode()
|
|
|
|
if refRange != nil {
|
|
from, to := refRange.From, refRange.To
|
|
args := []string{from.ParentRefName(), to.RefName(), "--stat", "-p"}
|
|
args = append(args, "--")
|
|
if filterPath := self.c.Modes().Filtering.GetPath(); filterPath != "" {
|
|
// If both refs are commits, filter by the union of their paths. This is useful for
|
|
// example when diffing a range of commits in filter-by-path mode across a rename.
|
|
fromCommit, ok1 := from.(*models.Commit)
|
|
toCommit, ok2 := to.(*models.Commit)
|
|
if ok1 && ok2 {
|
|
paths := append(self.FilterPathsForCommit(fromCommit), self.FilterPathsForCommit(toCommit)...)
|
|
args = append(args, lo.Uniq(paths)...)
|
|
} else {
|
|
// If either ref is not a commit (which is possible in sticky diff mode, when
|
|
// diffing against a branch or tag), we just filter by the filter path; that's the
|
|
// best we can do in this case.
|
|
args = append(args, filterPath)
|
|
}
|
|
}
|
|
cmdObj := self.c.Git().Diff.DiffCmdObj(args, mode)
|
|
prefix := style.FgYellow.Sprintf("%s %s-%s\n\n", self.c.Tr.ShowingDiffForRange, from.ShortRefName(), to.ShortRefName())
|
|
return types.NewMainViewDiffTaskWithPrefix(cmdObj.GetCmd(), prefix, mode)
|
|
}
|
|
|
|
cmdObj := self.c.Git().Commit.ShowCmdObj(commit.Hash(), self.FilterPathsForCommit(commit), mode)
|
|
return types.NewMainViewDiffTaskWithPrefix(cmdObj.GetCmd(), self.commitMessageDiffPrefix(commits, commit), mode)
|
|
}
|
|
|
|
// For an "amend!" commit, returns a diff of the commit message it sets against
|
|
// the message it replaces, to be shown above the commit's own diff. Returns an
|
|
// empty string for any other commit, and for an "amend!" commit that only
|
|
// changes the contents of the commit it applies to.
|
|
func (self *DiffHelper) commitMessageDiffPrefix(commits []*models.Commit, commit *models.Commit) string {
|
|
previousCommit, ok := findCommitWithPreviousMessage(commits, commit)
|
|
if !ok {
|
|
return ""
|
|
}
|
|
|
|
width, height := self.c.Contexts().Normal.GetView().InnerSize()
|
|
|
|
diff := self.commitMessageDiff(commit, previousCommit, width, height)
|
|
if diff == "" {
|
|
return ""
|
|
}
|
|
|
|
header := style.FgYellow.Sprintf("%s\n", utils.ResolvePlaceholderString(
|
|
self.c.Tr.CommitMessageChanges,
|
|
map[string]string{"hash": previousCommit.ShortHash()},
|
|
))
|
|
rule := strings.Repeat("─", width) + "\n"
|
|
|
|
return header + diff + rule
|
|
}
|
|
|
|
// The names the two messages are diffed under. A diff renderer shows them as the
|
|
// names of the files being diffed, so they are what tells the reader which side
|
|
// is which. They are not translated because they end up as file names, and git
|
|
// mangles paths outside of ASCII when it states them in a diff.
|
|
const (
|
|
oldMessageName = "old message"
|
|
newMessageName = "new message"
|
|
)
|
|
|
|
func (self *DiffHelper) commitMessageDiff(
|
|
commit *models.Commit, previousCommit *models.Commit, width int, height int,
|
|
) string {
|
|
// The diff renderer lays the diff out, and lays it out for the width it is
|
|
// shown at, so both belong in the key along with the two messages.
|
|
key := fmt.Sprintf("%s\x00%s\x00%d\x00%s",
|
|
commit.Hash(),
|
|
previousCommit.Hash(),
|
|
width,
|
|
self.c.State().GetDiffRendererConfigManager().Signature())
|
|
if diff, ok := self.commitMessageDiffs[key]; ok {
|
|
return diff
|
|
}
|
|
|
|
messages, err := self.c.Git().Commit.GetCommitMessages([]string{previousCommit.Hash(), commit.Hash()})
|
|
if err != nil {
|
|
self.c.Log.Error(err)
|
|
return ""
|
|
}
|
|
|
|
before := messageAfterAmending(messages[0])
|
|
after := messageAfterAmending(messages[1])
|
|
|
|
diff := ""
|
|
if before != after {
|
|
diff, err = self.c.Git().Diff.RenderedTextDiff(
|
|
git_commands.NamedText{Name: oldMessageName, Content: before},
|
|
git_commands.NamedText{Name: newMessageName, Content: after},
|
|
width, height)
|
|
if err != nil {
|
|
self.c.Log.Error(err)
|
|
return ""
|
|
}
|
|
}
|
|
|
|
self.commitMessageDiffs[key] = diff
|
|
return diff
|
|
}
|
|
|
|
// PlainDiffBetweenRefs returns the diff of the given files between two refs as git
|
|
// writes it, without colour or a diff renderer's involvement — what a panel showing
|
|
// a commit's diff hands out as the diff behind its rendering (see
|
|
// types.FocusedMainViewDiffSource). It honours diffing mode, so that the diff is of
|
|
// the same two ends the main view is showing.
|
|
func (self *DiffHelper) PlainDiffBetweenRefs(from string, to string, paths []string) string {
|
|
from, reverse := self.c.Modes().Diffing.GetFromAndReverseArgsForDiff(from)
|
|
// An error means there is no diff to be had, which for our purposes is the same
|
|
// as an empty one.
|
|
diff, _ := self.c.Git().WorkingTree.ShowFileDiffCmdObj(from, to, reverse, paths, git_commands.DiffModePlain).RunWithOutput()
|
|
return diff
|
|
}
|
|
|
|
func (self *DiffHelper) FilterPathsForCommit(commit *models.Commit) []string {
|
|
filterPath := self.c.Modes().Filtering.GetPath()
|
|
if filterPath != "" {
|
|
if len(commit.FilterPaths) > 0 {
|
|
return commit.FilterPaths
|
|
}
|
|
return []string{filterPath}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (self *DiffHelper) ExitDiffMode() error {
|
|
self.c.Modes().Diffing = diffing.New()
|
|
self.c.Refresh(types.RefreshOptions{})
|
|
return nil
|
|
}
|
|
|
|
func (self *DiffHelper) RenderDiff() {
|
|
args := self.DiffArgs()
|
|
cmdObj := self.c.Git().Diff.DiffCmdObj(args, git_commands.DiffModeRendered)
|
|
prefix := style.FgMagenta.Sprintf(
|
|
"%s %s\n\n",
|
|
self.c.Tr.ShowingGitDiff,
|
|
"git diff "+strings.Join(args, " "),
|
|
)
|
|
task := types.NewRunPtyTaskWithPrefix(cmdObj.GetCmd(), prefix)
|
|
|
|
self.c.RenderToMainViews(types.RefreshMainOpts{
|
|
Pair: self.c.MainViewPairs().Normal,
|
|
Main: &types.ViewUpdateOpts{
|
|
Title: "Diff",
|
|
SubTitle: self.IgnoringWhitespaceSubTitle(),
|
|
Task: task,
|
|
},
|
|
})
|
|
}
|
|
|
|
// CurrentDiffTerminals returns the current diff terminals of the currently selected item.
|
|
// in the case of a branch it returns both the branch and it's upstream name,
|
|
// which becomes an option when you bring up the diff menu, but when you're just
|
|
// flicking through branches it will be using the local branch name.
|
|
func (self *DiffHelper) CurrentDiffTerminals() []string {
|
|
c := self.c.Context().CurrentSide()
|
|
|
|
if c.GetKey() == "" {
|
|
return nil
|
|
}
|
|
|
|
switch v := c.(type) {
|
|
case types.DiffableContext:
|
|
return v.GetDiffTerminals()
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (self *DiffHelper) currentDiffTerminal() string {
|
|
names := self.CurrentDiffTerminals()
|
|
if len(names) == 0 {
|
|
return ""
|
|
}
|
|
return names[0]
|
|
}
|
|
|
|
func (self *DiffHelper) currentlySelectedFilename() string {
|
|
currentContext := self.c.Context().Current()
|
|
|
|
switch currentContext := currentContext.(type) {
|
|
case types.IListContext:
|
|
if lo.Contains([]types.ContextKey{context.FILES_CONTEXT_KEY, context.COMMIT_FILES_CONTEXT_KEY}, currentContext.GetKey()) {
|
|
return currentContext.GetSelectedItemId()
|
|
}
|
|
}
|
|
|
|
return ""
|
|
}
|
|
|
|
func (self *DiffHelper) WithDiffModeCheck(f func()) {
|
|
if self.c.Modes().Diffing.Active() {
|
|
self.RenderDiff()
|
|
} else {
|
|
f()
|
|
}
|
|
}
|
|
|
|
func (self *DiffHelper) IgnoringWhitespaceSubTitle() string {
|
|
if self.c.UserConfig().Git.IgnoreWhitespaceInDiffView {
|
|
return self.c.Tr.IgnoreWhitespaceDiffViewSubTitle
|
|
}
|
|
|
|
return ""
|
|
}
|
|
|
|
func (self *DiffHelper) OpenDiffToolForRef(selectedRef models.Ref) error {
|
|
to := selectedRef.RefName()
|
|
from, reverse := self.c.Modes().Diffing.GetFromAndReverseArgsForDiff("")
|
|
_, err := self.c.RunSubprocess(self.c.Git().Diff.OpenDiffToolCmdObj(
|
|
git_commands.DiffToolCmdOptions{
|
|
Filepath: ".",
|
|
FromCommit: from,
|
|
ToCommit: to,
|
|
Reverse: reverse,
|
|
IsDirectory: true,
|
|
Staged: false,
|
|
}))
|
|
return err
|
|
}
|
|
|
|
// AdjustLineNumber is used to adjust a line number in the diff that's currently
|
|
// being viewed, so that it corresponds to the line number in the actual working
|
|
// copy state of the file. It is used when clicking on a delta hyperlink in a
|
|
// diff, or when pressing `e` in a focused diff. It works
|
|
// by getting a diff of what's being viewed in the main view against the working
|
|
// copy, and then using that diff to adjust the line number.
|
|
// path is the file path of the file being viewed
|
|
// linenumber is the line number to adjust (one-based)
|
|
// viewname is the name of the view that shows the diff. We need to pass it
|
|
// because the diff adjustment is slightly different depending on which view is
|
|
// showing the diff.
|
|
func (self *DiffHelper) AdjustLineNumber(path string, linenumber int, viewname string) int {
|
|
switch viewname {
|
|
|
|
case "main":
|
|
if diffableContext, ok := self.c.Context().CurrentSide().(types.DiffableContext); ok {
|
|
ref := diffableContext.RefForAdjustingLineNumberInDiff()
|
|
if len(ref) != 0 {
|
|
return self.adjustLineNumber(linenumber, ref, "--", path)
|
|
}
|
|
}
|
|
// if the type cast to DiffableContext returns false, we are in the
|
|
// unstaged changes view of the Files panel; no need to adjust line
|
|
// numbers in this case
|
|
|
|
case "secondary":
|
|
return self.adjustLineNumber(linenumber, "--", path)
|
|
}
|
|
|
|
return linenumber
|
|
}
|
|
|
|
func (self *DiffHelper) adjustLineNumber(linenumber int, diffArgs ...string) int {
|
|
args := append([]string{"--unified=0"}, diffArgs...)
|
|
diff, err := self.c.Git().Diff.GetDiff(false, args...)
|
|
if err != nil {
|
|
return linenumber
|
|
}
|
|
patch := patch.Parse(diff)
|
|
return patch.AdjustLineNumber(linenumber)
|
|
}
|