mirror of
https://github.com/jesseduffield/lazygit.git
synced 2026-10-05 21:46:49 -04:00
Some merge conflicts can only be resolved by picking a side. The files panel explains those rather than diffing them, and where one side deleted the file and the other modified it, git's diff of that modification is shown below the explanation. The focused main view took those change lines for a diff of its own. It drew a selection over them and offered to stage hunks of a file whose conflict staging can't resolve. So have a render say whether it holds the diff the panel offers in the main view, and put a selection only on one that does. Every diff render already goes through NewMainViewDiffTask, so it says so for itself; the custom patch preview, assembled as text rather than run as a command, says so through NewMainViewDiffStringTask. Establishing a selection asks the pane the same question rather than looking for change lines itself. The selection has been wrong over this content since "Show a selection in the focused main view" introduced it, and the fix belongs there. It lands here instead because a render had no way to say what it holds until "Show git's own diff when the renderer's can't be acted on" gave every diff render one constructor to go through. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
268 lines
9.2 KiB
Go
268 lines
9.2 KiB
Go
package helpers
|
|
|
|
import (
|
|
"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/samber/lo"
|
|
)
|
|
|
|
type DiffHelper struct {
|
|
c *HelperCommon
|
|
diffLineHelper *DiffLineHelper
|
|
}
|
|
|
|
func NewDiffHelper(c *HelperCommon, diffLineHelper *DiffLineHelper) *DiffHelper {
|
|
return &DiffHelper{
|
|
c: c,
|
|
diffLineHelper: diffLineHelper,
|
|
}
|
|
}
|
|
|
|
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.
|
|
func (self *DiffHelper) GetUpdateTaskForRenderingCommitsDiff(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.NewMainViewDiffTask(cmdObj.GetCmd(), mode)
|
|
}
|
|
|
|
// 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
|
|
}
|
|
|
|
// RenderToMainAgain renders the current side panel into the main view again, if
|
|
// that is what the main view shows. This is for when something that the
|
|
// rendering depends on has changed, such as the diff renderer.
|
|
func (self *DiffHelper) RenderToMainAgain() {
|
|
currentSide := self.c.Context().CurrentSide()
|
|
currentKey := self.c.Context().Current().GetKey()
|
|
if currentSide.GetKey() == currentKey ||
|
|
currentKey == context.NORMAL_MAIN_CONTEXT_KEY ||
|
|
currentKey == context.NORMAL_SECONDARY_CONTEXT_KEY {
|
|
// Whatever changed can make the diff come out differently, such as a new
|
|
// renderer laying it out its own way, so the line you were looking at could
|
|
// end up anywhere in the view; keep it in front of you.
|
|
self.diffLineHelper.PreserveDiffPositionOnRerender(self.c.Contexts().Normal.GetView())
|
|
self.diffLineHelper.PreserveDiffPositionOnRerender(self.c.Contexts().NormalSecondary.GetView())
|
|
currentSide.HandleRenderToMain()
|
|
}
|
|
}
|
|
|
|
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.NewMainViewDiffTaskWithPrefix(cmdObj.GetCmd(), prefix, git_commands.DiffModeRendered)
|
|
|
|
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 the staging or patch building panels. 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", "patchBuilding":
|
|
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", "stagingSecondary":
|
|
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)
|
|
}
|