mirror of
https://github.com/jesseduffield/lazygit.git
synced 2026-10-05 21:46:49 -04:00
Materialize the custom patch so the diff renderer can show it
The pane beside a commit's diff showed the patch being built from it by assembling the text itself. That text could not be handed to a diff renderer the way a diff can: a stdin filter might have coped, but a tool that diffs two files could not. Its idea of how much context to show around a hunk was also its own rather than git's. Materialize the patch instead: write each of its files as it is before the patch into one tree and as it is after into another, and let git diff the two trees. The patch becomes a diff of real files, rendered by whatever renders the rest of them, with git's own context around it. Its lines can then be pointed at; taking them back out of the patch will need that. The trees are named a and b, so that with git's own prefixes suppressed the paths read like an ordinary diff's over the repo's own paths. They are written when the patch changes rather than when it is shown, the patch builder counting its own versions for that, and they go away with the patch. A renamed file is materialized under the name the patch expects to find it under. Where the patch carries the rename, that is the name the file had before, so the rename comes out as a rename. git names the trees themselves in the two rename lines, having only the two paths to go by. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
d920db5129
commit
02831c7157
@@ -139,6 +139,11 @@ func NewGitCommandAux(
|
||||
// A patch is built from git's own diff: what a diff renderer would make of it
|
||||
// is a picture of it, not something that can be applied.
|
||||
return workingTreeCommands.ShowFileDiff(from, to, reverse, filename, previousPath, git_commands.DiffModePlain)
|
||||
},
|
||||
func() (string, error) {
|
||||
// Under lazygit's own temp dir, so that it honours the configured location
|
||||
// and is cleaned up with everything else when we exit.
|
||||
return os.MkdirTemp(osCommand.GetTempDir(), "custom-patch-")
|
||||
})
|
||||
patchCommands := git_commands.NewPatchCommands(gitCommon, rebaseCommands, commitCommands, statusCommands, stashCommands, patchBuilder)
|
||||
bisectCommands := git_commands.NewBisectCommands(gitCommon)
|
||||
|
||||
@@ -156,6 +156,28 @@ func (self *DiffCommands) DiffCmdObj(diffArgs []string, mode DiffMode) *oscomman
|
||||
)
|
||||
}
|
||||
|
||||
// CustomPatchDiffCmdObj is the command that renders the custom patch being built: a diff
|
||||
// of the two trees the patch was materialized into (PatchCommands.WriteCustomPatchDiffTrees),
|
||||
// under the directory holding them. It goes through the same wiring as any other diff we
|
||||
// show, so the patch is rendered by whatever renders the rest of them, and git works out
|
||||
// how much context to give it.
|
||||
//
|
||||
// git's own path prefixes are suppressed because the trees are named a and b themselves,
|
||||
// which leaves the paths reading like an ordinary diff's over the repo's own paths.
|
||||
func (self *DiffCommands) CustomPatchDiffCmdObj(dir string, mode DiffMode) *oscommands.CmdObj {
|
||||
return self.cmd.New(
|
||||
NewGitCmd("diff").
|
||||
AddCommonDiffArgs(self.diffRendererConfigManager, self.UserConfig(), mode).
|
||||
NoLineEndingConversion().
|
||||
Arg("--no-index").
|
||||
Arg("--no-prefix").
|
||||
Arg(fmt.Sprintf("--color=%s", mode.colorArg(self.diffRendererConfigManager))).
|
||||
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.
|
||||
|
||||
@@ -123,6 +123,27 @@ func (self *GitCommandBuilder) GitDirIf(condition bool, path string) *GitCommand
|
||||
return self
|
||||
}
|
||||
|
||||
// NoLineEndingConversion keeps git's line-ending machinery away from files that are not
|
||||
// a working tree's. The trees the custom patch is materialized into hold the bytes git
|
||||
// states the patch in, so a command over them has to read and write those bytes as they
|
||||
// are. On a machine that checks files out with CRLF, `git apply` writes the after tree
|
||||
// in that form while the before tree keeps the LF it was written with. git's own diff
|
||||
// converts both back, but it warns about a round trip through a working tree these
|
||||
// files never belong to, and an external diff renderer is handed the two files as they
|
||||
// stand, one line ending apart in every line.
|
||||
func (self *GitCommandBuilder) NoLineEndingConversion() *GitCommandBuilder {
|
||||
return self.
|
||||
// The setting that converts on most machines, and the one Git for Windows
|
||||
// installs itself with.
|
||||
Config("core.autocrlf=false").
|
||||
// An attributes file outside the repo can still mark the files as text. The
|
||||
// form to keep them in is then the form they are written in.
|
||||
Config("core.eol=lf").
|
||||
// An attribute naming CRLF outright overrides that, and git converts after all.
|
||||
// The warning it gives is about a checkout these files never have.
|
||||
Config("core.safecrlf=false")
|
||||
}
|
||||
|
||||
func (self *GitCommandBuilder) AddCommonDiffArgs(diffRendererConfigManager *config.DiffRendererConfigManager, userConfig *config.UserConfig, mode DiffMode) *GitCommandBuilder {
|
||||
contextSize := userConfig.Git.DiffContextSize
|
||||
useExtDiff := mode == DiffModeRendered && diffRendererConfigManager.GetDiffRendererType() == config.DiffRendererType_ExtDiff
|
||||
|
||||
@@ -2,13 +2,16 @@ package git_commands
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/go-errors/errors"
|
||||
"github.com/jesseduffield/lazygit/pkg/app/daemon"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/models"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/patch"
|
||||
"github.com/samber/lo"
|
||||
"github.com/stefanhaller/git-todo-parser/todo"
|
||||
)
|
||||
|
||||
@@ -20,6 +23,10 @@ type PatchCommands struct {
|
||||
stash *StashCommands
|
||||
|
||||
PatchBuilder *patch.PatchBuilder
|
||||
|
||||
// The version of the patch the diff trees were last written for, so that they are
|
||||
// written again when, and only when, the patch has changed since.
|
||||
treesWrittenForGeneration int
|
||||
}
|
||||
|
||||
func NewPatchCommands(
|
||||
@@ -40,6 +47,88 @@ func NewPatchCommands(
|
||||
}
|
||||
}
|
||||
|
||||
// EnsureCustomPatchDiffTrees writes the custom patch's diff trees if what is there no
|
||||
// longer describes the patch. Call it before rendering the patch, which is often — every
|
||||
// time the panel showing it re-renders — while the patch itself changes rarely.
|
||||
func (self *PatchCommands) EnsureCustomPatchDiffTrees() error {
|
||||
if self.PatchBuilder.Generation() == self.treesWrittenForGeneration {
|
||||
return nil
|
||||
}
|
||||
if err := self.WriteCustomPatchDiffTrees(); err != nil {
|
||||
return err
|
||||
}
|
||||
self.treesWrittenForGeneration = self.PatchBuilder.Generation()
|
||||
return nil
|
||||
}
|
||||
|
||||
// WriteCustomPatchDiffTrees materializes the custom patch as two file trees under the
|
||||
// directory the patch builder keeps for it: `a` holds each of the patch's files as it is
|
||||
// before the patch, `b` as it is after. Diffing those two trees against each other
|
||||
// (DiffCommands.CustomPatchDiffCmdObj) turns the patch into a diff of real files, which
|
||||
// can then be rendered exactly as any other diff is — through a diff renderer of any
|
||||
// kind, and with git's own idea of how much context to show.
|
||||
//
|
||||
// The trees are named a and b so that the diff's paths, with git's own prefixes
|
||||
// suppressed, come out reading like the a/ and b/ of an ordinary diff, over the real
|
||||
// repo-relative paths.
|
||||
func (self *PatchCommands) WriteCustomPatchDiffTrees() error {
|
||||
dir := self.PatchBuilder.TempDir()
|
||||
if dir == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
before := filepath.Join(dir, "a")
|
||||
after := filepath.Join(dir, "b")
|
||||
for _, tree := range []string{before, after} {
|
||||
if err := os.RemoveAll(tree); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := os.MkdirAll(tree, 0o700); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
for _, file := range self.PatchBuilder.FilesInPatch() {
|
||||
content, err := self.commit.ShowFileContentCmdObj(self.PatchBuilder.From, file.ContentPath).RunWithOutput()
|
||||
// A file the patch adds has no content on the before side, so git has nothing to
|
||||
// show for it.
|
||||
added := err != nil
|
||||
|
||||
// The before side holds an added file as an empty file rather than not at all, so
|
||||
// that the diff pairs the two sides up and states the file's real path, instead of
|
||||
// reporting a file that only one of the trees has.
|
||||
if err := self.os.CreateFileWithContent(filepath.Join(before, file.Path),
|
||||
lo.Ternary(added, "", content)); err != nil {
|
||||
return err
|
||||
}
|
||||
// The after side is seeded with the same content, for the patch to change; a file
|
||||
// the patch adds is left absent, for the patch to create.
|
||||
if !added {
|
||||
if err := self.os.CreateFileWithContent(filepath.Join(after, file.Path), content); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Write added files as creations rather than as diffs against an empty file: the
|
||||
// patch is applied in one go, so a file it expects to be there already would make
|
||||
// the whole of it fail.
|
||||
patchText := self.PatchBuilder.PatchToApply(false, false)
|
||||
if strings.TrimSpace(patchText) == "" {
|
||||
// Nothing in the patch, so the two trees are alike and the diff is empty.
|
||||
return nil
|
||||
}
|
||||
patchFilePath, err := self.SaveTemporaryPatch(patchText)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return self.cmd.New(NewGitCmd("apply").
|
||||
NoLineEndingConversion().
|
||||
Arg(patchFilePath).
|
||||
Dir(after).
|
||||
ToArgv()).Run()
|
||||
}
|
||||
|
||||
type ApplyPatchOpts struct {
|
||||
ThreeWay bool
|
||||
Cached bool
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package patch
|
||||
|
||||
import (
|
||||
"os"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
@@ -61,12 +62,27 @@ type PatchBuilder struct {
|
||||
|
||||
// loadFileDiff loads the diff of a file, for a given to (typically a commit hash)
|
||||
loadFileDiff loadFileDiffFunc
|
||||
|
||||
// newTempDir makes a directory for the current patch to be materialized into, as
|
||||
// two file trees that can be diffed against each other and so rendered like any
|
||||
// other diff (see PatchCommands.WriteCustomPatchDiffTrees). Its lifetime is the
|
||||
// patch's: made when one is started, removed when it is given up.
|
||||
newTempDir func() (string, error)
|
||||
tempDir string
|
||||
|
||||
// generation counts the changes made to the patch, so that whoever materializes it
|
||||
// can tell whether what they last built still describes it — and rebuild only then,
|
||||
// rather than on every render of it.
|
||||
generation int
|
||||
}
|
||||
|
||||
func NewPatchBuilder(log *logrus.Entry, loadFileDiff loadFileDiffFunc) *PatchBuilder {
|
||||
func NewPatchBuilder(
|
||||
log *logrus.Entry, loadFileDiff loadFileDiffFunc, newTempDir func() (string, error),
|
||||
) *PatchBuilder {
|
||||
return &PatchBuilder{
|
||||
Log: log,
|
||||
loadFileDiff: loadFileDiff,
|
||||
newTempDir: newTempDir,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -74,6 +90,9 @@ func (p *PatchBuilder) Start(from, to string, reverse bool, canRebase bool) {
|
||||
p.mutex.Lock()
|
||||
defer p.mutex.Unlock()
|
||||
|
||||
p.generation++
|
||||
p.makeTempDir()
|
||||
|
||||
p.To = to
|
||||
p.From = from
|
||||
p.reverse = reverse
|
||||
@@ -92,6 +111,89 @@ func (p *PatchBuilder) snapshotFileInfoMap() map[string]*fileInfo {
|
||||
return p.fileInfoMap
|
||||
}
|
||||
|
||||
// TempDir is the directory the patch is materialized into for rendering, and "" when
|
||||
// there is none — no patch, or a directory we failed to make.
|
||||
func (p *PatchBuilder) TempDir() string {
|
||||
p.mutex.Lock()
|
||||
defer p.mutex.Unlock()
|
||||
|
||||
return p.tempDir
|
||||
}
|
||||
|
||||
// Generation says which version of the patch this is; see the field.
|
||||
func (p *PatchBuilder) Generation() int {
|
||||
p.mutex.Lock()
|
||||
defer p.mutex.Unlock()
|
||||
|
||||
return p.generation
|
||||
}
|
||||
|
||||
// makeTempDir replaces the directory the patch is materialized into with a fresh one.
|
||||
// Only call this with the lock held.
|
||||
func (p *PatchBuilder) makeTempDir() {
|
||||
p.removeTempDir()
|
||||
if p.newTempDir == nil {
|
||||
return
|
||||
}
|
||||
dir, err := p.newTempDir()
|
||||
if err != nil {
|
||||
p.Log.Error(err)
|
||||
return
|
||||
}
|
||||
p.tempDir = dir
|
||||
}
|
||||
|
||||
// removeTempDir takes the patch's materialized form away with the patch. Only call this
|
||||
// with the lock held.
|
||||
func (p *PatchBuilder) removeTempDir() {
|
||||
if p.tempDir == "" {
|
||||
return
|
||||
}
|
||||
if err := os.RemoveAll(p.tempDir); err != nil {
|
||||
p.Log.Error(err)
|
||||
}
|
||||
p.tempDir = ""
|
||||
}
|
||||
|
||||
// PatchFile records what materializing the patch needs to know about one of its files:
|
||||
// where the patch expects to find it, and where its content before the patch comes from.
|
||||
type PatchFile struct {
|
||||
// Path is the name the patch knows the file by: for a renamed file, the name it had
|
||||
// before where the patch carries the rename, and the name it was renamed to where the
|
||||
// patch keeps only a content change and leaves the rename behind.
|
||||
Path string
|
||||
// ContentPath is where the file's content before the patch is to be found in the
|
||||
// commit the patch is built from — for a renamed file always the name it had there,
|
||||
// whatever the patch calls it.
|
||||
ContentPath string
|
||||
}
|
||||
|
||||
// FilesInPatch says which files the patch touches, in a stable order, and where each of
|
||||
// them comes from.
|
||||
func (p *PatchBuilder) FilesInPatch() []PatchFile {
|
||||
fileInfoMap := p.snapshotFileInfoMap()
|
||||
|
||||
filenames := maps.Keys(fileInfoMap)
|
||||
sort.Strings(filenames)
|
||||
|
||||
files := make([]PatchFile, 0, len(filenames))
|
||||
for _, filename := range filenames {
|
||||
info := fileInfoMap[filename]
|
||||
if info.mode == UNSELECTED {
|
||||
continue
|
||||
}
|
||||
file := PatchFile{Path: filename, ContentPath: filename}
|
||||
if info.previousPath != "" {
|
||||
file.ContentPath = info.previousPath
|
||||
if info.mode == WHOLE {
|
||||
file.Path = info.previousPath
|
||||
}
|
||||
}
|
||||
files = append(files, file)
|
||||
}
|
||||
return files
|
||||
}
|
||||
|
||||
func (p *PatchBuilder) PatchToApply(reverse bool, turnAddedFilesIntoDiffAgainstEmptyFile bool) string {
|
||||
var patch strings.Builder
|
||||
|
||||
@@ -136,6 +238,7 @@ func (p *PatchBuilder) AddFileWhole(filename string, previousPath string) error
|
||||
return err
|
||||
}
|
||||
|
||||
p.generation++
|
||||
p.addFileWhole(info)
|
||||
|
||||
return nil
|
||||
@@ -147,6 +250,7 @@ func (p *PatchBuilder) RemoveFile(filename string, previousPath string) error {
|
||||
return err
|
||||
}
|
||||
|
||||
p.generation++
|
||||
p.removeFile(info)
|
||||
|
||||
return nil
|
||||
@@ -183,6 +287,7 @@ func (p *PatchBuilder) AddFileLineRange(filename string, previousPath string, li
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
p.generation++
|
||||
info.mode = PART
|
||||
info.includedLineIndices = lo.Union(info.includedLineIndices, lineIndices)
|
||||
|
||||
@@ -194,6 +299,7 @@ func (p *PatchBuilder) RemoveFileLineRange(filename string, previousPath string,
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
p.generation++
|
||||
info.mode = PART
|
||||
info.includedLineIndices, _ = lo.Difference(info.includedLineIndices, lineIndices)
|
||||
if len(info.includedLineIndices) == 0 {
|
||||
@@ -380,6 +486,9 @@ func (p *PatchBuilder) Reset() {
|
||||
p.mutex.Lock()
|
||||
defer p.mutex.Unlock()
|
||||
|
||||
p.generation++
|
||||
p.removeTempDir()
|
||||
|
||||
p.To = ""
|
||||
p.fileInfoMap = map[string]*fileInfo{}
|
||||
}
|
||||
|
||||
@@ -13,7 +13,10 @@ func newTestPatchBuilder(diff string) *PatchBuilder {
|
||||
patchBuilder := NewPatchBuilder(logrus.New().WithField("test", "test"),
|
||||
func(from string, to string, reverse bool, filename string, previousPath string) (string, error) {
|
||||
return diff, nil
|
||||
})
|
||||
},
|
||||
// Nothing here renders the patch, so it needs no directory to be
|
||||
// materialized into.
|
||||
nil)
|
||||
patchBuilder.Start("from", "to", false, true)
|
||||
return patchBuilder
|
||||
}
|
||||
@@ -64,6 +67,35 @@ func TestIncludedLineIdentities(t *testing.T) {
|
||||
patchBuilder.IncludedLineIdentities("filename"))
|
||||
}
|
||||
|
||||
func TestFilesInPatch(t *testing.T) {
|
||||
patchBuilder := newTestPatchBuilder(simpleDiff)
|
||||
|
||||
// A file no part of the patch is no part of what the patch is materialized from.
|
||||
assert.Empty(t, patchBuilder.FilesInPatch())
|
||||
|
||||
assert.NoError(t, patchBuilder.AddFileLineRange("filename", "", []int{6}))
|
||||
assert.Equal(t,
|
||||
[]PatchFile{{Path: "filename", ContentPath: "filename"}},
|
||||
patchBuilder.FilesInPatch())
|
||||
}
|
||||
|
||||
// A renamed file's content is under the name it had before whatever the patch calls the
|
||||
// file, and the patch calls it by the name it had before only where it carries the
|
||||
// rename — a partial selection has the rename stripped and names the file by the new one.
|
||||
func TestFilesInPatchOfARenamedFile(t *testing.T) {
|
||||
patchBuilder := newTestPatchBuilder(renameWithModificationDiff)
|
||||
|
||||
assert.NoError(t, patchBuilder.AddFileLineRange("newname", "oldname", []int{9}))
|
||||
assert.Equal(t,
|
||||
[]PatchFile{{Path: "newname", ContentPath: "oldname"}},
|
||||
patchBuilder.FilesInPatch())
|
||||
|
||||
assert.NoError(t, patchBuilder.AddFileWhole("newname", "oldname"))
|
||||
assert.Equal(t,
|
||||
[]PatchFile{{Path: "oldname", ContentPath: "oldname"}},
|
||||
patchBuilder.FilesInPatch())
|
||||
}
|
||||
|
||||
// A file taken into the patch whole has every one of its change lines in it.
|
||||
func TestIncludedLineIdentitiesOfAWholeFile(t *testing.T) {
|
||||
patchBuilder := newTestPatchBuilder(simpleDiff)
|
||||
|
||||
@@ -722,17 +722,30 @@ func (self *LocalCommitsController) GetOnRenderToMain() func() {
|
||||
}
|
||||
}
|
||||
|
||||
// secondaryPatchPanelUpdateOpts renders the custom patch being built into the pane
|
||||
// beside the diff it is being built from, as a diff of the two trees the patch is
|
||||
// materialized into. This way it is shown by whatever renders the rest of the diffs,
|
||||
// and its lines can be pointed at and taken back out of the patch.
|
||||
func secondaryPatchPanelUpdateOpts(c *ControllerCommon) *types.ViewUpdateOpts {
|
||||
if c.Git().Patch.PatchBuilder.Active() {
|
||||
patch := c.Git().Patch.PatchBuilder.RenderAggregatedPatch(false)
|
||||
|
||||
return &types.ViewUpdateOpts{
|
||||
Task: types.NewMainViewDiffStringTask(patch),
|
||||
Title: c.Tr.CustomPatch,
|
||||
}
|
||||
if !c.Git().Patch.PatchBuilder.Active() {
|
||||
return nil
|
||||
}
|
||||
|
||||
return nil
|
||||
// A render of the same patch reuses the trees; only a change to the patch writes them
|
||||
// again.
|
||||
if err := c.Git().Patch.EnsureCustomPatchDiffTrees(); err != nil {
|
||||
c.Log.Error(err)
|
||||
}
|
||||
|
||||
// The same mode as the diff beside it: both panes of the pair have to agree about
|
||||
// whether what they show can be acted on.
|
||||
mode := c.Helpers().DiffLine.MainViewDiffMode()
|
||||
cmdObj := c.Git().Diff.CustomPatchDiffCmdObj(c.Git().Patch.PatchBuilder.TempDir(), mode)
|
||||
|
||||
return &types.ViewUpdateOpts{
|
||||
Task: types.NewMainViewDiffTask(cmdObj.GetCmd(), mode),
|
||||
Title: c.Tr.CustomPatch,
|
||||
}
|
||||
}
|
||||
|
||||
func (self *LocalCommitsController) squashDown(selectedCommits []*models.Commit, startIdx int, endIdx int) error {
|
||||
|
||||
@@ -230,6 +230,7 @@ M file1
|
||||
func(from string, to string, reverse bool, filename string, previousPath string) (string, error) {
|
||||
return "", nil
|
||||
},
|
||||
nil,
|
||||
)
|
||||
patchBuilder.Start("from", "to", false, false)
|
||||
result := RenderCommitFileTree(viewModel, patchBuilder, false, &config.CustomIconsConfig{})
|
||||
|
||||
@@ -69,9 +69,6 @@ func NewRenderStringTask(str string) *RenderStringTask {
|
||||
|
||||
type RenderStringWithoutScrollTask struct {
|
||||
Str string
|
||||
|
||||
// contentIsDiff marks a string that is a panel's own diff; see ContentIsDiff.
|
||||
contentIsDiff bool
|
||||
}
|
||||
|
||||
func (t *RenderStringWithoutScrollTask) IsUpdateTask() {}
|
||||
@@ -80,16 +77,6 @@ func NewRenderStringWithoutScrollTask(str string) *RenderStringWithoutScrollTask
|
||||
return &RenderStringWithoutScrollTask{Str: str}
|
||||
}
|
||||
|
||||
// NewMainViewDiffStringTask returns the task for rendering a diff we hold as text
|
||||
// rather than as a command to run — the custom patch being built, which we assemble
|
||||
// ourselves. The view stays where it is, the patch being rendered again on every
|
||||
// change to it.
|
||||
func NewMainViewDiffStringTask(str string) UpdateTask {
|
||||
task := NewRenderStringWithoutScrollTask(str)
|
||||
task.contentIsDiff = true
|
||||
return task
|
||||
}
|
||||
|
||||
type RenderStringWithScrollTask struct {
|
||||
Str string
|
||||
OriginX int
|
||||
@@ -166,8 +153,6 @@ func NewMainViewDiffTaskWithPrefix(cmd *exec.Cmd, prefix string, mode git_comman
|
||||
// such a render that there is anything to point at.
|
||||
func ContentIsDiff(task UpdateTask) bool {
|
||||
switch task := task.(type) {
|
||||
case *RenderStringWithoutScrollTask:
|
||||
return task.contentIsDiff
|
||||
case *RunCommandTask:
|
||||
return task.contentIsDiff
|
||||
case *RunDiffRendererTask:
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
package main_view
|
||||
|
||||
import (
|
||||
"github.com/jesseduffield/lazygit/pkg/config"
|
||||
. "github.com/jesseduffield/lazygit/pkg/integration/components"
|
||||
)
|
||||
|
||||
var CustomPatchGoesThroughTheDiffRenderer = NewIntegrationTest(NewIntegrationTestArgs{
|
||||
Description: "The custom patch is shown by the configured diff renderer, being rendered as a diff of real files rather than assembled by us",
|
||||
ExtraCmdArgs: []string{},
|
||||
Skip: false,
|
||||
SetupConfig: func(cfg *config.AppConfig) {
|
||||
cfg.GetUserConfig().Gui.UseHunkModeInStagingView = false
|
||||
// A renderer that announces the metadata protocol — so that focusing the main
|
||||
// view keeps its output — and says who it is above the diff it passes through.
|
||||
cfg.GetUserConfig().Git.DiffRenderers = []config.DiffRendererConfig{
|
||||
{Command: `printf '\033]1717;1\007RENDERED BY ME\n'; cat`},
|
||||
}
|
||||
},
|
||||
SetupRepo: func(shell *Shell) {
|
||||
shell.CreateFileAndAdd("file1", "one\ntwo\nthree\n")
|
||||
shell.Commit("first commit")
|
||||
|
||||
shell.UpdateFileAndAdd("file1", "one\nTWO\nthree\n")
|
||||
shell.Commit("second commit")
|
||||
},
|
||||
Run: func(t *TestDriver, keys config.KeybindingConfig) {
|
||||
t.Views().Commits().
|
||||
Focus().
|
||||
Press(keys.Universal.FocusMainView)
|
||||
|
||||
t.Views().Main().
|
||||
IsFocused().
|
||||
Content(Contains("RENDERED BY ME")).
|
||||
SelectedLines(
|
||||
Contains("-two"),
|
||||
).
|
||||
PressPrimaryAction()
|
||||
|
||||
// The patch is a diff like any other, so the renderer has had it too. git worked
|
||||
// out its context, and that is where the unchanged line either side comes from.
|
||||
t.Views().Secondary().
|
||||
Content(Contains("RENDERED BY ME")).
|
||||
ContainsLines(
|
||||
Contains(" one"),
|
||||
Contains("-two"),
|
||||
Contains(" three"),
|
||||
)
|
||||
},
|
||||
})
|
||||
@@ -0,0 +1,52 @@
|
||||
package main_view
|
||||
|
||||
import (
|
||||
"github.com/jesseduffield/lazygit/pkg/config"
|
||||
. "github.com/jesseduffield/lazygit/pkg/integration/components"
|
||||
)
|
||||
|
||||
var CustomPatchIgnoresLineEndingConversion = NewIntegrationTest(NewIntegrationTestArgs{
|
||||
Description: "The custom patch is shown as git states it on a machine that checks files out with CRLF",
|
||||
ExtraCmdArgs: []string{},
|
||||
Skip: false,
|
||||
// The patch is materialized into trees of its own outside the repo, so what the repo
|
||||
// says about line endings never reaches the commands over them. These variables are
|
||||
// how a setting reaches a git command wherever it runs.
|
||||
ExtraEnvVars: map[string]string{
|
||||
"GIT_CONFIG_COUNT": "1",
|
||||
"GIT_CONFIG_KEY_0": "core.autocrlf",
|
||||
"GIT_CONFIG_VALUE_0": "true",
|
||||
},
|
||||
SetupConfig: func(config *config.AppConfig) {},
|
||||
SetupRepo: func(shell *Shell) {
|
||||
shell.CreateFileAndAdd("file1", "one\ntwo\nthree\n")
|
||||
shell.Commit("first commit")
|
||||
|
||||
shell.UpdateFileAndAdd("file1", "one\nthree\n")
|
||||
shell.Commit("second commit")
|
||||
},
|
||||
Run: func(t *TestDriver, keys config.KeybindingConfig) {
|
||||
t.Views().Commits().
|
||||
Focus().
|
||||
Press(keys.Universal.FocusMainView)
|
||||
|
||||
t.Views().Main().
|
||||
IsFocused().
|
||||
SelectedLines(
|
||||
Contains("-two"),
|
||||
).
|
||||
PressPrimaryAction()
|
||||
|
||||
// The patch is one line and its context, so that is all the two trees differ in.
|
||||
// A tree written in the form the platform checks files out in would differ from
|
||||
// the other in every line instead.
|
||||
t.Views().Secondary().
|
||||
ContainsLines(
|
||||
Contains(" one"),
|
||||
Contains("-two"),
|
||||
Contains(" three"),
|
||||
).
|
||||
// git says nothing about a round trip these files never take.
|
||||
Content(DoesNotContain("warning"))
|
||||
},
|
||||
})
|
||||
@@ -36,11 +36,16 @@ var RenamedFileWhole = NewIntegrationTest(NewIntegrationTestArgs{
|
||||
|
||||
t.Views().Information().Content(Contains("Building patch"))
|
||||
|
||||
// The whole file is added, so the patch carries the rename itself.
|
||||
// The whole file is added, so the patch carries the rename itself, and the diff
|
||||
// the patch is shown as carries it too. The trees the patch is materialized into
|
||||
// are what git names in the rename lines, the paths of the two sides being all it
|
||||
// has to go by.
|
||||
t.Views().Secondary().
|
||||
ContainsLines(
|
||||
Contains("rename from original"),
|
||||
Contains("rename to renamed"),
|
||||
Contains("diff --git a/original b/renamed"),
|
||||
Contains("similarity index"),
|
||||
Contains("rename from a/original"),
|
||||
Contains("rename to b/renamed"),
|
||||
)
|
||||
|
||||
t.Common().SelectPatchOption(Contains("Remove patch from original commit"))
|
||||
|
||||
@@ -134,14 +134,14 @@ var SpecificSelection = NewIntegrationTest(NewIntegrationTestArgs{
|
||||
Contains(`index`),
|
||||
Contains(`--- a/hunk-file`),
|
||||
Contains(`+++ b/hunk-file`),
|
||||
Contains(`@@ -1,6 +1,6 @@`),
|
||||
// git works out the context of the patch's own hunks, which is why
|
||||
// this is not the six lines the file's diff had
|
||||
Contains(`@@ -1,4 +1,4 @@`),
|
||||
Contains(`-1a`),
|
||||
Contains(`+aa`),
|
||||
Contains(` 1b`),
|
||||
Contains(` 1c`),
|
||||
Contains(` 1d`),
|
||||
Contains(` 1e`),
|
||||
Contains(` 1f`),
|
||||
// line-file patch
|
||||
Contains(`diff --git a/line-file b/line-file`),
|
||||
Contains(`index`),
|
||||
|
||||
@@ -375,6 +375,8 @@ var tests = []*components.IntegrationTest{
|
||||
main_view.CommitFromMainView,
|
||||
main_view.CopyRowsThatAreNoDiffLine,
|
||||
main_view.CopySelectedDiffLines,
|
||||
main_view.CustomPatchGoesThroughTheDiffRenderer,
|
||||
main_view.CustomPatchIgnoresLineEndingConversion,
|
||||
main_view.DiscardDiffLines,
|
||||
main_view.DiscardFromACommitOnlyWhereItCanBeRewritten,
|
||||
main_view.DiscardLinesFromACommit,
|
||||
|
||||
Reference in New Issue
Block a user