diff --git a/pkg/commands/git.go b/pkg/commands/git.go index bfe19b85f..efb4bb22d 100644 --- a/pkg/commands/git.go +++ b/pkg/commands/git.go @@ -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) diff --git a/pkg/commands/git_commands/diff.go b/pkg/commands/git_commands/diff.go index 3fece6235..01bb670c2 100644 --- a/pkg/commands/git_commands/diff.go +++ b/pkg/commands/git_commands/diff.go @@ -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. diff --git a/pkg/commands/git_commands/git_command_builder.go b/pkg/commands/git_commands/git_command_builder.go index fe7a6d075..a4bd7340e 100644 --- a/pkg/commands/git_commands/git_command_builder.go +++ b/pkg/commands/git_commands/git_command_builder.go @@ -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 diff --git a/pkg/commands/git_commands/patch.go b/pkg/commands/git_commands/patch.go index f40f7fa6f..823206c90 100644 --- a/pkg/commands/git_commands/patch.go +++ b/pkg/commands/git_commands/patch.go @@ -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 diff --git a/pkg/commands/patch/patch_builder.go b/pkg/commands/patch/patch_builder.go index 55256a99c..8d94bdc9b 100644 --- a/pkg/commands/patch/patch_builder.go +++ b/pkg/commands/patch/patch_builder.go @@ -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{} } diff --git a/pkg/commands/patch/patch_builder_test.go b/pkg/commands/patch/patch_builder_test.go index 9fd8caf80..cb6bb3684 100644 --- a/pkg/commands/patch/patch_builder_test.go +++ b/pkg/commands/patch/patch_builder_test.go @@ -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) diff --git a/pkg/gui/controllers/local_commits_controller.go b/pkg/gui/controllers/local_commits_controller.go index 72c99d2ad..983ca789f 100644 --- a/pkg/gui/controllers/local_commits_controller.go +++ b/pkg/gui/controllers/local_commits_controller.go @@ -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 { diff --git a/pkg/gui/presentation/files_test.go b/pkg/gui/presentation/files_test.go index 74d8f1369..9a92731a0 100644 --- a/pkg/gui/presentation/files_test.go +++ b/pkg/gui/presentation/files_test.go @@ -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{}) diff --git a/pkg/gui/types/rendering.go b/pkg/gui/types/rendering.go index ac8932a9b..9710ca948 100644 --- a/pkg/gui/types/rendering.go +++ b/pkg/gui/types/rendering.go @@ -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: diff --git a/pkg/integration/tests/main_view/custom_patch_goes_through_the_diff_renderer.go b/pkg/integration/tests/main_view/custom_patch_goes_through_the_diff_renderer.go new file mode 100644 index 000000000..96bbdda8c --- /dev/null +++ b/pkg/integration/tests/main_view/custom_patch_goes_through_the_diff_renderer.go @@ -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"), + ) + }, +}) diff --git a/pkg/integration/tests/main_view/custom_patch_ignores_line_ending_conversion.go b/pkg/integration/tests/main_view/custom_patch_ignores_line_ending_conversion.go new file mode 100644 index 000000000..5d5e131a2 --- /dev/null +++ b/pkg/integration/tests/main_view/custom_patch_ignores_line_ending_conversion.go @@ -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")) + }, +}) diff --git a/pkg/integration/tests/patch_building/renamed_file_whole.go b/pkg/integration/tests/patch_building/renamed_file_whole.go index f4151a766..71b0cbedc 100644 --- a/pkg/integration/tests/patch_building/renamed_file_whole.go +++ b/pkg/integration/tests/patch_building/renamed_file_whole.go @@ -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")) diff --git a/pkg/integration/tests/patch_building/specific_selection.go b/pkg/integration/tests/patch_building/specific_selection.go index 2e140e41c..723bde864 100644 --- a/pkg/integration/tests/patch_building/specific_selection.go +++ b/pkg/integration/tests/patch_building/specific_selection.go @@ -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`), diff --git a/pkg/integration/tests/test_list.go b/pkg/integration/tests/test_list.go index 009036d45..ac983a530 100644 --- a/pkg/integration/tests/test_list.go +++ b/pkg/integration/tests/test_list.go @@ -374,6 +374,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,