mirror of
https://github.com/jesseduffield/lazygit.git
synced 2026-09-13 00:56:25 -04:00
The secondary pane showed the custom patch with a bespoke in-memory render (PatchBuilder.RenderAggregatedPatch / FormatView). That had two problems: it could never be fed to a pager the way the main view's diff is (a stdin pager might have worked, but an external diff tool like difftastic, which diffs two files rather than a unified diff, could not), and its hand-rolled hunk/context handling differed subtly from git's. Materialize the patch instead as two real file trees under a temp dir — a/ holds each patched file's "from"-side content, b/ that content with the patch applied — and render it with `git diff --no-index`, reusing the exact pager wiring the main view uses (stdin pager, external diff, or git's own colour as the raw fallback). `--no-index` honors both GIT_PAGER and --ext-diff, so every pager type now renders the custom patch like any other diff, and git computes the context, fixing the quirks. Because the secondary is now an async diff task, the post-removal selection reveal (which rides a task's restore) finally takes effect. The trees are named a/b so that with --no-prefix the diff shows the real repo-relative paths; added files are seeded empty in a/ so they pair up and show their real paths rather than git's directory-comparison "added in b" form. The patch builder owns the temp dir's lifetime (created on Start, removed on Reset) and bumps a generation counter on every change, so the trees are rebuilt only when the patch actually changes — covering the focused-main view and the old explorer alike, without rebuilding on mere navigation. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
188 lines
7.3 KiB
Go
188 lines
7.3 KiB
Go
package git_commands
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"strings"
|
|
|
|
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
|
|
"github.com/mgutz/str"
|
|
)
|
|
|
|
type DiffCommands struct {
|
|
*GitCommon
|
|
}
|
|
|
|
func NewDiffCommands(gitCommon *GitCommon) *DiffCommands {
|
|
return &DiffCommands{
|
|
GitCommon: gitCommon,
|
|
}
|
|
}
|
|
|
|
// metadataHandshake is the OSC sequence a metadata-aware pager emits, as its first
|
|
// output, to announce that it speaks the diff-line-metadata protocol: a version-only
|
|
// OSC 1717 record (no fields). See ProbePagerEmitsDiffMetadata and, for how it's
|
|
// 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.
|
|
//
|
|
// No PTY is needed: git needs a terminal to decide to invoke a pager, but the pager
|
|
// itself emits the handshake whenever EMIT_OSC1717_METADATA is set, so we can run it
|
|
// directly with empty input.
|
|
//
|
|
// 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;
|
|
// we conservatively report false (the focused main view then always renders raw).
|
|
func (self *DiffCommands) ProbePagerEmitsDiffMetadata() bool {
|
|
if extDiffCmd := self.diffRendererConfigManager.GetExternalDiffCommand(3); extDiffCmd != "" {
|
|
return self.externalDiffEmitsMetadata(extDiffCmd)
|
|
}
|
|
if pagerCmd := self.diffRendererConfigManager.GetStdinFilterCommand(0); pagerCmd != "" {
|
|
return self.probeEmitsMetadata(self.cmd.NewShell(pagerCmd, ""))
|
|
}
|
|
return false
|
|
}
|
|
|
|
// 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
|
|
// files, so it emits its handshake without there being a real diff to render.
|
|
func (self *DiffCommands) externalDiffEmitsMetadata(extDiffCmd string) bool {
|
|
oldFile, err := os.CreateTemp("", "lazygit-probe-old-*")
|
|
if err != nil {
|
|
return false
|
|
}
|
|
defer os.Remove(oldFile.Name())
|
|
oldFile.Close()
|
|
|
|
newFile, err := os.CreateTemp("", "lazygit-probe-new-*")
|
|
if err != nil {
|
|
return false
|
|
}
|
|
defer os.Remove(newFile.Name())
|
|
newFile.Close()
|
|
|
|
args := append(str.ToArgv(extDiffCmd),
|
|
"probe", oldFile.Name(), "0000000", "100644", newFile.Name(), "0000000", "100644")
|
|
return self.probeEmitsMetadata(self.cmd.New(args))
|
|
}
|
|
|
|
func (self *DiffCommands) probeEmitsMetadata(cmdObj *oscommands.CmdObj) bool {
|
|
cmdObj.AddEnvVars("EMIT_OSC1717_METADATA=V1")
|
|
// The pager may exit non-zero on the synthetic input; we only care about whether
|
|
// it emitted the handshake first, and the output is captured either way.
|
|
output, _ := cmdObj.RunWithOutput()
|
|
return strings.Contains(output, metadataHandshake)
|
|
}
|
|
|
|
// This is for generating diffs to be shown in the UI (e.g. rendering a range
|
|
// diff to the main view). It uses a custom diff renderer if one is configured, unless
|
|
// ignoreExternalDiff is set (the focused main view's raw-diff fallback; keeps the
|
|
// colour, unlike a plain diff).
|
|
func (self *DiffCommands) DiffCmdObj(diffArgs []string, ignoreExternalDiff bool) *oscommands.CmdObj {
|
|
return self.cmd.New(
|
|
NewGitCmd("diff").
|
|
Config("diff.noprefix=false").
|
|
AddCommonDiffArgs(self.diffRendererConfigManager, self.UserConfig(), !ignoreExternalDiff).
|
|
Arg("--submodule").
|
|
Arg(fmt.Sprintf("--color=%s", self.diffRendererConfigManager.GetColorArg())).
|
|
Arg(diffArgs...).
|
|
Dir(self.repoPaths.worktreePath).
|
|
ToArgv(),
|
|
)
|
|
}
|
|
|
|
// CustomPatchDiffCmdObj builds the command that renders the custom patch shown in the
|
|
// secondary pane: a `git diff --no-index` of the two file trees PatchCommands materialized
|
|
// under dir (a/ = before, b/ = after; see WriteCustomPatchDiffTrees). It uses the same pager
|
|
// wiring as DiffCmdObj so the patch renders exactly like any other diff — through a stdin
|
|
// pager, an external diff tool, or (when ignoreExternalDiff is set, the focused main view's
|
|
// raw-diff fallback) git's own colour. --no-prefix is used because the a/ and b/ tree names
|
|
// already stand in for git's conventional a//b/ path prefixes, so the diff's paths come out
|
|
// as the real repo-relative paths.
|
|
func (self *DiffCommands) CustomPatchDiffCmdObj(dir string, ignoreExternalDiff bool) *oscommands.CmdObj {
|
|
return self.cmd.New(
|
|
NewGitCmd("diff").
|
|
AddCommonDiffArgs(self.diffRendererConfigManager, self.UserConfig(), !ignoreExternalDiff).
|
|
Arg("--no-index").
|
|
Arg("--no-prefix").
|
|
Arg(fmt.Sprintf("--color=%s", self.diffRendererConfigManager.GetColorArg())).
|
|
Arg("a", "b").
|
|
Dir(dir).
|
|
ToArgv(),
|
|
)
|
|
}
|
|
|
|
// This is a basic generic diff command that can be used for any diff operation
|
|
// (e.g. copying a diff to the clipboard). It will not use a custom diff renderer,
|
|
// and does not use user configs such as ignore whitespace.
|
|
// If you want to diff specific refs (one or two), you need to add them yourself
|
|
// in additionalArgs; it is recommended to also pass `--` after that. If you
|
|
// want to restrict the diff to specific paths, pass them in additionalArgs
|
|
// after the `--`.
|
|
func (self *DiffCommands) GetDiff(staged bool, additionalArgs ...string) (string, error) {
|
|
return self.cmd.New(
|
|
NewGitCmd("diff").
|
|
Config("diff.noprefix=false").
|
|
Arg("--no-ext-diff", "--no-color").
|
|
ArgIf(staged, "--staged").
|
|
Dir(self.repoPaths.worktreePath).
|
|
Arg(additionalArgs...).
|
|
ToArgv(),
|
|
).DontLog().RunWithOutput()
|
|
}
|
|
|
|
type DiffToolCmdOptions struct {
|
|
// The path to show a diff for. Pass "." for the entire repo.
|
|
Filepath string
|
|
|
|
// The commit against which to show the diff. Leave empty to show a diff of
|
|
// the working copy.
|
|
FromCommit string
|
|
|
|
// The commit to diff against FromCommit. Leave empty to diff the working
|
|
// copy against FromCommit. Leave both FromCommit and ToCommit empty to show
|
|
// the diff of the unstaged working copy changes against the index if Staged
|
|
// is false, or the staged changes against HEAD if Staged is true.
|
|
ToCommit string
|
|
|
|
// Whether to reverse the left and right sides of the diff.
|
|
Reverse bool
|
|
|
|
// Whether the given Filepath is a directory. We'll pass --dir-diff to
|
|
// git-difftool in that case.
|
|
IsDirectory bool
|
|
|
|
// Whether to show the staged or the unstaged changes. Must be false if both
|
|
// FromCommit and ToCommit are non-empty.
|
|
Staged bool
|
|
}
|
|
|
|
func (self *DiffCommands) OpenDiffToolCmdObj(opts DiffToolCmdOptions) *oscommands.CmdObj {
|
|
return self.cmd.New(NewGitCmd("difftool").
|
|
Arg("--no-prompt").
|
|
ArgIf(opts.IsDirectory, "--dir-diff").
|
|
ArgIf(opts.Staged, "--cached").
|
|
ArgIf(opts.FromCommit != "", opts.FromCommit).
|
|
ArgIf(opts.ToCommit != "", opts.ToCommit).
|
|
ArgIf(opts.Reverse, "-R").
|
|
Arg("--", opts.Filepath).
|
|
ToArgv())
|
|
}
|
|
|
|
func (self *DiffCommands) DiffIndexCmdObj(diffArgs ...string) *oscommands.CmdObj {
|
|
return self.cmd.New(
|
|
NewGitCmd("diff-index").
|
|
Config("diff.noprefix=false").
|
|
Arg("--submodule", "--no-ext-diff", "--no-color", "--patch").
|
|
Arg(diffArgs...).ToArgv(),
|
|
)
|
|
}
|