mirror of
https://github.com/jesseduffield/lazygit.git
synced 2026-09-10 23:56:24 -04:00
Support git's own word diff as a metadata-emitting diff renderer
git now emits the diff line metadata records itself, for the word-diff formats -- the ones whose output can't be read back from its own text, which is the same reason we need records out of delta and difftastic. So a rawGit renderer configured with --color-words is no longer a diff we have to give up on: it names every row it shows, as a patched pager does. Two things stood in the way. The probe only knew how to ask a stdin filter or an external diff driver and reported false for anything else, so git's own records were never looked for; and the focused main view treated a rawGit renderer with arguments as unresolvable by definition, re-rendering it raw whatever the probe said. So probe git the way we will run it, with the renderer's own arguments, and look for an actual record rather than the handshake the other probes settle for. git annotates only some of its formats, so an installed git that doesn't speak the protocol and arguments that select no word diff both leave us without records, and looking for a record answers both at once. Arguments that aren't a word diff (-U10, --stat) therefore keep rendering raw when focused, as they did before. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
450e295ec3
commit
473f83ec49
|
|
@ -3,6 +3,7 @@ package git_commands
|
|||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
|
||||
|
|
@ -25,20 +26,23 @@ func NewDiffCommands(gitCommon *GitCommon) *DiffCommands {
|
|||
// swallowed on a real render, escapeInterpreter.dropMetadataIfHandshake.
|
||||
const metadataHandshake = "\x1b]1717"
|
||||
|
||||
// ProbePagerEmitsDiffMetadata reports whether the configured pager speaks the
|
||||
// diff-line-metadata protocol, by running it on empty input and checking for its
|
||||
// handshake. It's the focused main view's signal for whether it can act on the
|
||||
// pager's rendered diff or must fall back to the raw diff (see
|
||||
// StagingHelper.DiffMainViewShouldRenderRaw). The verdict is content-independent —
|
||||
// the handshake is the pager's first output regardless of the diff — so the caller
|
||||
// caches it per pager.
|
||||
// metadataRecordPattern matches a per-line record rather than the handshake: a record
|
||||
// carries fields after the version, so a second ";" follows it. Used where the
|
||||
// handshake alone doesn't answer whether records will actually follow (see
|
||||
// rawGitEmitsMetadata).
|
||||
var metadataRecordPattern = regexp.MustCompile(`\x1b]1717;\d+;`)
|
||||
|
||||
// ProbePagerEmitsDiffMetadata reports whether the configured diff renderer speaks the
|
||||
// diff-line-metadata protocol, by running it and checking its output. It's the focused
|
||||
// main view's signal for whether it can act on the rendered diff or must fall back to
|
||||
// the raw diff (see StagingHelper.DiffMainViewShouldRenderRaw). The verdict is
|
||||
// content-independent, so the caller caches it per renderer.
|
||||
//
|
||||
// No PTY is needed: git needs a terminal to decide to invoke a pager, but the pager
|
||||
// itself emits the handshake whenever OSC1717 is set, so we can run it
|
||||
// directly with empty input.
|
||||
// No PTY is needed: git needs a terminal to decide to invoke a pager, but a renderer
|
||||
// emits its records whenever OSC1717 is set, so we can run it directly.
|
||||
//
|
||||
// A git-config external diff driver (useExternalDiffGitConfig) is chosen per file via
|
||||
// .gitattributes and a single diff can mix drivers, so there's no one pager to probe;
|
||||
// .gitattributes and a single diff can mix drivers, so there's no one renderer to probe;
|
||||
// we conservatively report false (the focused main view then always renders raw).
|
||||
func (self *DiffCommands) ProbePagerEmitsDiffMetadata() bool {
|
||||
if extDiffCmd := self.diffRendererConfigManager.GetExternalDiffCommand(3); extDiffCmd != "" {
|
||||
|
|
@ -47,9 +51,52 @@ func (self *DiffCommands) ProbePagerEmitsDiffMetadata() bool {
|
|||
if pagerCmd := self.diffRendererConfigManager.GetStdinFilterCommand(0); pagerCmd != "" {
|
||||
return self.probeEmitsMetadata(self.cmd.NewShell(pagerCmd, ""))
|
||||
}
|
||||
if rawGitArgs := self.diffRendererConfigManager.GetRawGitArgs(); len(rawGitArgs) > 0 {
|
||||
return self.rawGitEmitsMetadata(rawGitArgs)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// rawGitEmitsMetadata probes git itself, run with the diff renderer's own arguments, on
|
||||
// a synthetic diff of two differing temp files.
|
||||
//
|
||||
// It looks for an actual record where the other probes settle for the handshake. What we
|
||||
// depend on is that the rows we are about to show carry records, and only running git
|
||||
// the way we will run it answers that: git annotates only the formats whose output can't
|
||||
// be read back from its own text, so an installed git that doesn't speak the protocol
|
||||
// and arguments that select no word diff both leave us without records. Looking for a
|
||||
// record covers both, and keeps this independent of how git decides to announce itself.
|
||||
func (self *DiffCommands) rawGitEmitsMetadata(rawGitArgs []string) bool {
|
||||
oldFile, err := os.CreateTemp("", "lazygit-probe-old-*")
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
defer os.Remove(oldFile.Name())
|
||||
fmt.Fprintln(oldFile, "old")
|
||||
oldFile.Close()
|
||||
|
||||
newFile, err := os.CreateTemp("", "lazygit-probe-new-*")
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
defer os.Remove(newFile.Name())
|
||||
fmt.Fprintln(newFile, "new")
|
||||
newFile.Close()
|
||||
|
||||
cmdObj := self.cmd.New(
|
||||
NewGitCmd("diff").
|
||||
Arg("--no-index").
|
||||
Arg(rawGitArgs...).
|
||||
Arg(oldFile.Name(), newFile.Name()).
|
||||
ToArgv(),
|
||||
)
|
||||
cmdObj.AddEnvVars("OSC1717=V1")
|
||||
// git exits non-zero because the two files differ, which is the point; the output
|
||||
// is captured either way.
|
||||
output, _ := cmdObj.RunWithOutput()
|
||||
return metadataRecordPattern.MatchString(output)
|
||||
}
|
||||
|
||||
// externalDiffEmitsMetadata probes an external diff command, invoking it the way git
|
||||
// invokes a diff.external driver — with 7 positional args
|
||||
// (path old-file old-hex old-mode new-file new-hex new-mode) — but on two empty temp
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import (
|
|||
"fmt"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/models"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/patch"
|
||||
|
|
@ -898,21 +899,26 @@ func (self *StagingHelper) diffLineInfoFromHyperlink(hyperlink string) (types.Di
|
|||
}, true
|
||||
}
|
||||
|
||||
// --- The raw-diff fallback for unsupported pagers ---
|
||||
// --- The raw-diff fallback for unsupported diff renderers ---
|
||||
//
|
||||
// The focused main view is the staging surface, so it must be able to resolve the
|
||||
// diff it shows to patch-space. A pager that restructures the diff without emitting
|
||||
// diff it shows to patch-space. A renderer that restructures the diff without emitting
|
||||
// our metadata (stock delta-default, plain difftastic, `cat -n`) produces output we
|
||||
// can't resolve, so when such a diff is focused we re-render it raw (git's own
|
||||
// colour, no pager) — the same content a no-pager setup shows, which the buffer
|
||||
// parser handles. Browsing keeps the pretty pager output; only focusing to act
|
||||
// switches to raw.
|
||||
// colour, no renderer) — the same content a no-renderer setup shows, which the buffer
|
||||
// parser handles. Browsing keeps the pretty output; only focusing to act switches to
|
||||
// raw.
|
||||
//
|
||||
// Whether a pager is usable is decided by probing it for the metadata handshake (see
|
||||
// DiffCommands.ProbePagerEmitsDiffMetadata) — a pager-level, content-independent fact,
|
||||
// so the verdict is stable (a binary file, which has no change lines under any pager,
|
||||
// can't mislead it) and known before we render, so we never render pretty only to
|
||||
// discover mid-flight that we should have rendered raw.
|
||||
// This is not only about pagers: git's own word-diff formats (a rawGit renderer whose
|
||||
// arguments include --color-words or --word-diff) restructure the diff just as much,
|
||||
// their markup being inline, and a git that speaks the protocol annotates them for
|
||||
// exactly that reason. So the question is asked of whatever renders, git included.
|
||||
//
|
||||
// Whether a renderer is usable is decided by probing it (see
|
||||
// DiffCommands.ProbePagerEmitsDiffMetadata) — a renderer-level, content-independent
|
||||
// fact, so the verdict is stable (a binary file, which has no change lines under any
|
||||
// renderer, can't mislead it) and known before we render, so we never render pretty
|
||||
// only to discover mid-flight that we should have rendered raw.
|
||||
|
||||
// pagerSupportsMetadata returns whether the current pager speaks the metadata
|
||||
// protocol, probing it once and caching the result, re-probing when the pager changes.
|
||||
|
|
@ -932,8 +938,9 @@ func (self *StagingHelper) pagerSupportsMetadata() bool {
|
|||
func (self *StagingHelper) currentPagerSignature() string {
|
||||
pc := self.c.State().GetDiffRendererConfigManager()
|
||||
index, _ := pc.CurrentDiffRendererIndex()
|
||||
return fmt.Sprintf("%d\x00%s\x00%s",
|
||||
index, pc.GetExternalDiffCommand(3), pc.GetStdinFilterCommand(0))
|
||||
return fmt.Sprintf("%d\x00%s\x00%s\x00%s",
|
||||
index, pc.GetExternalDiffCommand(3), pc.GetStdinFilterCommand(0),
|
||||
strings.Join(pc.GetRawGitArgs(), "\x00"))
|
||||
}
|
||||
|
||||
// MainViewPagerConfigured reports whether any custom pager is in effect for the
|
||||
|
|
@ -951,15 +958,25 @@ func (self *StagingHelper) rendererIsRawGitWithArgs() bool {
|
|||
len(pc.GetRawGitArgs()) > 0
|
||||
}
|
||||
|
||||
// mainViewDiffNeedsMetadata reports whether the diff shown in the main view is one we
|
||||
// can't recover the patch-space identity of a row from by reading the diff's own text,
|
||||
// and so can only act on through the metadata records. Any custom renderer restructures
|
||||
// the diff; so does git itself once the renderer's arguments select a word diff, whose
|
||||
// markup is inline. Plain `git diff` output is its own answer and needs no records.
|
||||
func (self *StagingHelper) mainViewDiffNeedsMetadata() bool {
|
||||
return self.MainViewPagerConfigured() || self.rendererIsRawGitWithArgs()
|
||||
}
|
||||
|
||||
// DiffMainViewShouldRenderRaw reports whether a side panel rendering its diff into
|
||||
// the focused main view should bypass the pager and render the raw (git-coloured)
|
||||
// diff. This holds while the main view holds focus and the configured pager doesn't
|
||||
// speak the metadata protocol. Side panels consult it when building their main-view
|
||||
// diff (see e.g. FilesController.GetOnRenderToMain) so that a re-render while focused
|
||||
// — after staging a hunk, say — stays raw.
|
||||
// the focused main view should bypass the diff renderer and render the raw
|
||||
// (git-coloured) diff. This holds while the main view holds focus and the diff we would
|
||||
// otherwise show needs the metadata records to be resolvable but won't carry them.
|
||||
// Side panels consult it when building their main-view diff (see e.g.
|
||||
// FilesController.GetOnRenderToMain) so that a re-render while focused — after staging
|
||||
// a hunk, say — stays raw.
|
||||
func (self *StagingHelper) DiffMainViewShouldRenderRaw() bool {
|
||||
return self.focusedOnMainView() && ((self.MainViewPagerConfigured() && !self.pagerSupportsMetadata()) ||
|
||||
self.rendererIsRawGitWithArgs())
|
||||
return self.focusedOnMainView() && self.mainViewDiffNeedsMetadata() &&
|
||||
!self.pagerSupportsMetadata()
|
||||
}
|
||||
|
||||
func (self *StagingHelper) focusedOnMainView() bool {
|
||||
|
|
|
|||
Loading…
Reference in a new issue