mirror of
https://github.com/jesseduffield/lazygit.git
synced 2026-08-24 10:13:41 -05:00
Show renamed files in the custom patch builder
When loading the files of a commit we passed --no-renames, so a rename showed up as a separate delete and add rather than a single R entry. That made it impossible to work with a rename that also modifies the file: the modifications were spread across a full deletion and a full addition instead of appearing as the handful of lines that actually changed. The staging view already shows renames and lets you stage their hunks, so there was no good reason for the patch builder to differ; the flag was only there because the commit-file parser couldn't cope with the rename record format. Switch the commit-file loader and the per-file diff to --find-renames, teach the parser about the rename record (a status followed by two paths), and carry the previous path through the patch builder so the diff for a rename is loaded with both paths, which is what makes git emit the rename in the first place. A whole-file selection keeps the rename in the header, so the rename moves or is discarded together with the file's contents. A partial selection instead strips the rename metadata and points the header at the new path, so applying the patch only changes the contents and leaves the rename in place; the blob index line is kept so that a 3-way apply can still fall back to a blob merge. Discarding a renamed file from a commit now discards both the new and the old path, so the new file is removed and the old one is restored. Changing the rename similarity threshold refreshes the commit files panel too, not just the files panel, so that a rename can turn into a delete and add or back. It is disabled while building a patch, however, because the patch builder caches each file's diff by path and would desync if a rename changed into a delete and add underneath it. Finally, copying a file's diff from the commit files panel now passes both paths for a rename, so the copied diff shows the rename instead of a new-file add. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
c085598100
commit
f84ada4941
+2
-2
@@ -117,8 +117,8 @@ func NewGitCommandAux(
|
||||
rebaseCommands := git_commands.NewRebaseCommands(gitCommon, commitCommands, workingTreeCommands)
|
||||
stashCommands := git_commands.NewStashCommands(gitCommon, fileLoader, workingTreeCommands)
|
||||
patchBuilder := patch.NewPatchBuilder(cmn.Log,
|
||||
func(from string, to string, reverse bool, filename string, plain bool) (string, error) {
|
||||
return workingTreeCommands.ShowFileDiff(from, to, reverse, filename, plain)
|
||||
func(from string, to string, reverse bool, filename string, previousPath string, plain bool) (string, error) {
|
||||
return workingTreeCommands.ShowFileDiff(from, to, reverse, filename, previousPath, plain)
|
||||
})
|
||||
patchCommands := git_commands.NewPatchCommands(gitCommon, rebaseCommands, commitCommands, statusCommands, stashCommands, patchBuilder)
|
||||
bisectCommands := git_commands.NewBisectCommands(gitCommon)
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
package git_commands
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/models"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
|
||||
"github.com/jesseduffield/lazygit/pkg/common"
|
||||
"github.com/samber/lo"
|
||||
)
|
||||
|
||||
type CommitFileLoader struct {
|
||||
@@ -29,7 +29,7 @@ func (self *CommitFileLoader) GetFilesInDiff(from string, to string, reverse boo
|
||||
Arg("--no-ext-diff").
|
||||
Arg("--name-status").
|
||||
Arg("-z").
|
||||
Arg("--no-renames").
|
||||
Arg(fmt.Sprintf("--find-renames=%d%%", self.UserConfig().Git.RenameSimilarityThreshold)).
|
||||
ArgIf(reverse, "-R").
|
||||
Arg(from).
|
||||
Arg(to).
|
||||
@@ -44,18 +44,37 @@ func (self *CommitFileLoader) GetFilesInDiff(from string, to string, reverse boo
|
||||
}
|
||||
|
||||
// filenames string is something like "MM\x00file1\x00MU\x00file2\x00AA\x00file3\x00"
|
||||
// so we need to split it by the null character and then map each status-name pair to a commit file
|
||||
// so we need to split it by the null character and then map each status-name pair
|
||||
// to a commit file. Renames (and copies) are special: their status is followed by
|
||||
// two paths (the old one and the new one) rather than one, e.g.
|
||||
// "R100\x00old\x00new\x00".
|
||||
func getCommitFilesFromFilenames(filenames string) []*models.CommitFile {
|
||||
lines := strings.Split(strings.TrimRight(filenames, "\x00"), "\x00")
|
||||
if len(lines) == 1 {
|
||||
fields := strings.Split(strings.TrimRight(filenames, "\x00"), "\x00")
|
||||
if len(fields) == 1 {
|
||||
return []*models.CommitFile{}
|
||||
}
|
||||
|
||||
// typical result looks like 'A my_file' meaning my_file was added
|
||||
return lo.Map(lo.Chunk(lines, 2), func(chunk []string, _ int) *models.CommitFile {
|
||||
return &models.CommitFile{
|
||||
ChangeStatus: chunk[0],
|
||||
Path: chunk[1],
|
||||
commitFiles := make([]*models.CommitFile, 0, len(fields)/2)
|
||||
for i := 0; i < len(fields)-1; {
|
||||
changeStatus := fields[i]
|
||||
if changeStatus[0] == 'R' || changeStatus[0] == 'C' {
|
||||
// The status has a similarity score appended (e.g. "R100"); drop it
|
||||
// so the rest of the code only has to deal with a plain "R" or "C".
|
||||
commitFiles = append(commitFiles, &models.CommitFile{
|
||||
ChangeStatus: changeStatus[:1],
|
||||
PreviousPath: fields[i+1],
|
||||
Path: fields[i+2],
|
||||
})
|
||||
i += 3
|
||||
} else {
|
||||
// typical result looks like 'A my_file' meaning my_file was added
|
||||
commitFiles = append(commitFiles, &models.CommitFile{
|
||||
ChangeStatus: changeStatus,
|
||||
Path: fields[i+1],
|
||||
})
|
||||
i += 2
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
return commitFiles
|
||||
}
|
||||
|
||||
@@ -60,6 +60,25 @@ func TestGetCommitFilesFromFilenames(t *testing.T) {
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
testName: "a rename among regular files",
|
||||
input: "M\x00Myfile\x00R100\x00before\x00after\x00A\x00Added\x00",
|
||||
output: []*models.CommitFile{
|
||||
{
|
||||
Path: "Myfile",
|
||||
ChangeStatus: "M",
|
||||
},
|
||||
{
|
||||
Path: "after",
|
||||
PreviousPath: "before",
|
||||
ChangeStatus: "R",
|
||||
},
|
||||
{
|
||||
Path: "Added",
|
||||
ChangeStatus: "A",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
|
||||
@@ -432,8 +432,14 @@ func (self *WorkingTreeCommands) WorktreeFileDiffCmdObj(node models.IFile, plain
|
||||
|
||||
// ShowFileDiff get the diff of specified from and to. Typically this will be used for a single commit so it'll be 123abc^..123abc
|
||||
// but when we're in diff mode it could be any 'from' to any 'to'. The reverse flag is also here thanks to diff mode.
|
||||
func (self *WorkingTreeCommands) ShowFileDiff(from string, to string, reverse bool, fileName string, plain bool) (string, error) {
|
||||
return self.ShowFileDiffCmdObj(from, to, reverse, []string{fileName}, plain).RunWithOutput()
|
||||
// For a renamed file, previousPath is the path it was renamed from (empty otherwise);
|
||||
// both paths must be passed to git for the rename to be detected.
|
||||
func (self *WorkingTreeCommands) ShowFileDiff(from string, to string, reverse bool, fileName string, previousPath string, plain bool) (string, error) {
|
||||
fileNames := []string{fileName}
|
||||
if previousPath != "" {
|
||||
fileNames = append(fileNames, previousPath)
|
||||
}
|
||||
return self.ShowFileDiffCmdObj(from, to, reverse, fileNames, plain).RunWithOutput()
|
||||
}
|
||||
|
||||
func (self *WorkingTreeCommands) ShowFileDiffCmdObj(from string, to string, reverse bool, fileNames []string, plain bool) *oscommands.CmdObj {
|
||||
@@ -454,7 +460,7 @@ func (self *WorkingTreeCommands) ShowFileDiffCmdObj(from string, to string, reve
|
||||
ArgIfElse(useExtDiff || useExtDiffGitConfig, "--ext-diff", "--no-ext-diff").
|
||||
Arg("--submodule").
|
||||
Arg(fmt.Sprintf("--unified=%d", contextSize)).
|
||||
Arg("--no-renames").
|
||||
Arg(fmt.Sprintf("--find-renames=%d%%", self.UserConfig().Git.RenameSimilarityThreshold)).
|
||||
Arg(fmt.Sprintf("--color=%s", colorArg)).
|
||||
Arg(from).
|
||||
Arg(to).
|
||||
|
||||
@@ -339,6 +339,8 @@ func TestWorkingTreeShowFileDiff(t *testing.T) {
|
||||
from string
|
||||
to string
|
||||
reverse bool
|
||||
fileName string
|
||||
previousPath string
|
||||
plain bool
|
||||
ignoreWhitespace bool
|
||||
contextSize uint64
|
||||
@@ -353,33 +355,49 @@ func TestWorkingTreeShowFileDiff(t *testing.T) {
|
||||
from: "1234567890",
|
||||
to: "0987654321",
|
||||
reverse: false,
|
||||
fileName: "test.txt",
|
||||
plain: false,
|
||||
ignoreWhitespace: false,
|
||||
contextSize: 3,
|
||||
runner: oscommands.NewFakeRunner(t).
|
||||
ExpectGitArgs([]string{"-C", "/path/to/worktree", "-c", "diff.noprefix=false", "diff", "--no-ext-diff", "--submodule", "--unified=3", "--no-renames", "--color=always", "1234567890", "0987654321", "--", "test.txt"}, expectedResult, nil),
|
||||
ExpectGitArgs([]string{"-C", "/path/to/worktree", "-c", "diff.noprefix=false", "diff", "--no-ext-diff", "--submodule", "--unified=3", "--find-renames=50%", "--color=always", "1234567890", "0987654321", "--", "test.txt"}, expectedResult, nil),
|
||||
},
|
||||
{
|
||||
testName: "Show diff with custom context size",
|
||||
from: "1234567890",
|
||||
to: "0987654321",
|
||||
reverse: false,
|
||||
fileName: "test.txt",
|
||||
plain: false,
|
||||
ignoreWhitespace: false,
|
||||
contextSize: 123,
|
||||
runner: oscommands.NewFakeRunner(t).
|
||||
ExpectGitArgs([]string{"-C", "/path/to/worktree", "-c", "diff.noprefix=false", "diff", "--no-ext-diff", "--submodule", "--unified=123", "--no-renames", "--color=always", "1234567890", "0987654321", "--", "test.txt"}, expectedResult, nil),
|
||||
ExpectGitArgs([]string{"-C", "/path/to/worktree", "-c", "diff.noprefix=false", "diff", "--no-ext-diff", "--submodule", "--unified=123", "--find-renames=50%", "--color=always", "1234567890", "0987654321", "--", "test.txt"}, expectedResult, nil),
|
||||
},
|
||||
{
|
||||
testName: "Default case (ignore whitespace)",
|
||||
from: "1234567890",
|
||||
to: "0987654321",
|
||||
reverse: false,
|
||||
fileName: "test.txt",
|
||||
plain: false,
|
||||
ignoreWhitespace: true,
|
||||
contextSize: 3,
|
||||
runner: oscommands.NewFakeRunner(t).
|
||||
ExpectGitArgs([]string{"-C", "/path/to/worktree", "-c", "diff.noprefix=false", "diff", "--no-ext-diff", "--submodule", "--unified=3", "--no-renames", "--color=always", "1234567890", "0987654321", "--ignore-all-space", "--", "test.txt"}, expectedResult, nil),
|
||||
ExpectGitArgs([]string{"-C", "/path/to/worktree", "-c", "diff.noprefix=false", "diff", "--no-ext-diff", "--submodule", "--unified=3", "--find-renames=50%", "--color=always", "1234567890", "0987654321", "--ignore-all-space", "--", "test.txt"}, expectedResult, nil),
|
||||
},
|
||||
{
|
||||
testName: "Renamed file passes both paths so the rename is detected",
|
||||
from: "1234567890",
|
||||
to: "0987654321",
|
||||
reverse: false,
|
||||
fileName: "new.txt",
|
||||
previousPath: "old.txt",
|
||||
plain: false,
|
||||
ignoreWhitespace: false,
|
||||
contextSize: 3,
|
||||
runner: oscommands.NewFakeRunner(t).
|
||||
ExpectGitArgs([]string{"-C", "/path/to/worktree", "-c", "diff.noprefix=false", "diff", "--no-ext-diff", "--submodule", "--unified=3", "--find-renames=50%", "--color=always", "1234567890", "0987654321", "--", "new.txt", "old.txt"}, expectedResult, nil),
|
||||
},
|
||||
}
|
||||
|
||||
@@ -394,7 +412,7 @@ func TestWorkingTreeShowFileDiff(t *testing.T) {
|
||||
|
||||
instance := buildWorkingTreeCommands(commonDeps{runner: s.runner, userConfig: userConfig, appState: &config.AppState{}, repoPaths: &repoPaths})
|
||||
|
||||
result, err := instance.ShowFileDiff(s.from, s.to, s.reverse, "test.txt", s.plain)
|
||||
result, err := instance.ShowFileDiff(s.from, s.to, s.reverse, s.fileName, s.previousPath, s.plain)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, expectedResult, result)
|
||||
s.runner.CheckForMissingCalls()
|
||||
|
||||
@@ -4,6 +4,9 @@ package models
|
||||
type CommitFile struct {
|
||||
Path string
|
||||
|
||||
// For a renamed file, the path it was renamed from; empty otherwise.
|
||||
PreviousPath string
|
||||
|
||||
ChangeStatus string // e.g. 'A' for added or 'M' for modified. This is based on the result from git diff --name-status
|
||||
}
|
||||
|
||||
@@ -23,6 +26,24 @@ func (f *CommitFile) Deleted() bool {
|
||||
return f.ChangeStatus == "D"
|
||||
}
|
||||
|
||||
func (f *CommitFile) IsRename() bool {
|
||||
return f.PreviousPath != ""
|
||||
}
|
||||
|
||||
// Names returns an array containing just the path, or in the case of a rename,
|
||||
// the after path and the before path.
|
||||
func (f *CommitFile) Names() []string {
|
||||
result := []string{f.Path}
|
||||
if f.PreviousPath != "" {
|
||||
result = append(result, f.PreviousPath)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func (f *CommitFile) GetPath() string {
|
||||
return f.Path
|
||||
}
|
||||
|
||||
func (f *CommitFile) GetPreviousPath() string {
|
||||
return f.PreviousPath
|
||||
}
|
||||
|
||||
@@ -25,10 +25,14 @@ type fileInfo struct {
|
||||
mode PatchStatus
|
||||
includedLineIndices []int
|
||||
diff string
|
||||
// For a renamed file, the path it was renamed from; empty otherwise. We
|
||||
// need to keep hold of it so we can re-render the file's patch (which is
|
||||
// keyed by the new path) without the caller having to supply it again.
|
||||
previousPath string
|
||||
}
|
||||
|
||||
type (
|
||||
loadFileDiffFunc func(from string, to string, reverse bool, filename string, plain bool) (string, error)
|
||||
loadFileDiffFunc func(from string, to string, reverse bool, filename string, previousPath string, plain bool) (string, error)
|
||||
)
|
||||
|
||||
// PatchBuilder manages the building of a patch for a commit to be applied to another commit (or the working tree, or removed from the current commit). We also support building patches from things like stashes, for which there is less flexibility
|
||||
@@ -75,6 +79,7 @@ func (p *PatchBuilder) PatchToApply(reverse bool, turnAddedFilesIntoDiffAgainstE
|
||||
|
||||
patch.WriteString(p.RenderPatchForFile(RenderPatchForFileOpts{
|
||||
Filename: filename,
|
||||
PreviousPath: info.previousPath,
|
||||
Plain: true,
|
||||
Reverse: reverse,
|
||||
TurnAddedFilesIntoDiffAgainstEmptyFile: turnAddedFilesIntoDiffAgainstEmptyFile,
|
||||
@@ -102,8 +107,8 @@ func (p *PatchBuilder) removeFile(info *fileInfo) {
|
||||
info.includedLineIndices = nil
|
||||
}
|
||||
|
||||
func (p *PatchBuilder) AddFileWhole(filename string) error {
|
||||
info, err := p.getFileInfo(filename)
|
||||
func (p *PatchBuilder) AddFileWhole(filename string, previousPath string) error {
|
||||
info, err := p.getFileInfo(filename, previousPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -113,8 +118,8 @@ func (p *PatchBuilder) AddFileWhole(filename string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *PatchBuilder) RemoveFile(filename string) error {
|
||||
info, err := p.getFileInfo(filename)
|
||||
func (p *PatchBuilder) RemoveFile(filename string, previousPath string) error {
|
||||
info, err := p.getFileInfo(filename, previousPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -124,19 +129,20 @@ func (p *PatchBuilder) RemoveFile(filename string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *PatchBuilder) getFileInfo(filename string) (*fileInfo, error) {
|
||||
func (p *PatchBuilder) getFileInfo(filename string, previousPath string) (*fileInfo, error) {
|
||||
info, ok := p.fileInfoMap[filename]
|
||||
if ok {
|
||||
return info, nil
|
||||
}
|
||||
|
||||
diff, err := p.loadFileDiff(p.From, p.To, p.reverse, filename, true)
|
||||
diff, err := p.loadFileDiff(p.From, p.To, p.reverse, filename, previousPath, true)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
info = &fileInfo{
|
||||
mode: UNSELECTED,
|
||||
diff: diff,
|
||||
mode: UNSELECTED,
|
||||
diff: diff,
|
||||
previousPath: previousPath,
|
||||
}
|
||||
|
||||
p.fileInfoMap[filename] = info
|
||||
@@ -144,8 +150,8 @@ func (p *PatchBuilder) getFileInfo(filename string) (*fileInfo, error) {
|
||||
return info, nil
|
||||
}
|
||||
|
||||
func (p *PatchBuilder) AddFileLineRange(filename string, lineIndices []int) error {
|
||||
info, err := p.getFileInfo(filename)
|
||||
func (p *PatchBuilder) AddFileLineRange(filename string, previousPath string, lineIndices []int) error {
|
||||
info, err := p.getFileInfo(filename, previousPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -155,8 +161,8 @@ func (p *PatchBuilder) AddFileLineRange(filename string, lineIndices []int) erro
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *PatchBuilder) RemoveFileLineRange(filename string, lineIndices []int) error {
|
||||
info, err := p.getFileInfo(filename)
|
||||
func (p *PatchBuilder) RemoveFileLineRange(filename string, previousPath string, lineIndices []int) error {
|
||||
info, err := p.getFileInfo(filename, previousPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -171,13 +177,14 @@ func (p *PatchBuilder) RemoveFileLineRange(filename string, lineIndices []int) e
|
||||
|
||||
type RenderPatchForFileOpts struct {
|
||||
Filename string
|
||||
PreviousPath string
|
||||
Plain bool
|
||||
Reverse bool
|
||||
TurnAddedFilesIntoDiffAgainstEmptyFile bool
|
||||
}
|
||||
|
||||
func (p *PatchBuilder) RenderPatchForFile(opts RenderPatchForFileOpts) string {
|
||||
info, err := p.getFileInfo(opts.Filename)
|
||||
info, err := p.getFileInfo(opts.Filename, opts.PreviousPath)
|
||||
if err != nil {
|
||||
p.Log.Error(err)
|
||||
return ""
|
||||
@@ -198,7 +205,12 @@ func (p *PatchBuilder) RenderPatchForFile(opts RenderPatchForFileOpts) string {
|
||||
Transform(TransformOpts{
|
||||
Reverse: opts.Reverse,
|
||||
TurnAddedFilesIntoDiffAgainstEmptyFile: opts.TurnAddedFilesIntoDiffAgainstEmptyFile,
|
||||
IncludedLineIndices: info.includedLineIndices,
|
||||
// For a partial selection of a renamed file we keep only the
|
||||
// content change and drop the rename, so that the rename stays in
|
||||
// the commit. A whole-file selection keeps the rename (and short-
|
||||
// circuits before this for plain output).
|
||||
StripRename: info.mode == PART && info.previousPath != "",
|
||||
IncludedLineIndices: info.includedLineIndices,
|
||||
})
|
||||
|
||||
if opts.Plain {
|
||||
@@ -215,6 +227,7 @@ func (p *PatchBuilder) renderEachFilePatch(plain bool) []string {
|
||||
patches := lo.Map(filenames, func(filename string, _ int) string {
|
||||
return p.RenderPatchForFile(RenderPatchForFileOpts{
|
||||
Filename: filename,
|
||||
PreviousPath: p.fileInfoMap[filename].previousPath,
|
||||
Plain: plain,
|
||||
Reverse: false,
|
||||
TurnAddedFilesIntoDiffAgainstEmptyFile: true,
|
||||
@@ -244,8 +257,8 @@ func (p *PatchBuilder) GetFileStatus(filename string, parent string) PatchStatus
|
||||
return info.mode
|
||||
}
|
||||
|
||||
func (p *PatchBuilder) GetFileIncLineIndices(filename string) ([]int, error) {
|
||||
info, err := p.getFileInfo(filename)
|
||||
func (p *PatchBuilder) GetFileIncLineIndices(filename string, previousPath string) ([]int, error) {
|
||||
info, err := p.getFileInfo(filename, previousPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -19,6 +19,22 @@ index dcd3485..1ba5540 100644
|
||||
...
|
||||
`
|
||||
|
||||
const renameWithModificationDiff = `diff --git a/oldname b/newname
|
||||
similarity index 62%
|
||||
rename from oldname
|
||||
rename to newname
|
||||
index dcd3485..1ba5540 100644
|
||||
--- a/oldname
|
||||
+++ b/newname
|
||||
@@ -1,5 +1,5 @@
|
||||
apple
|
||||
-orange
|
||||
+grape
|
||||
...
|
||||
...
|
||||
...
|
||||
`
|
||||
|
||||
const addNewlineToEndOfFile = `diff --git a/filename b/filename
|
||||
index 80a73f1..e48a11c 100644
|
||||
--- a/filename
|
||||
@@ -152,6 +168,7 @@ func TestTransform(t *testing.T) {
|
||||
firstLineIndex int
|
||||
lastLineIndex int
|
||||
reverse bool
|
||||
stripRename bool
|
||||
expected string
|
||||
}
|
||||
|
||||
@@ -515,6 +532,43 @@ func TestTransform(t *testing.T) {
|
||||
orange
|
||||
banana
|
||||
lemon
|
||||
`,
|
||||
},
|
||||
{
|
||||
testName: "renamed file, whole change selected, strips the rename so only the content change is applied",
|
||||
firstLineIndex: 9,
|
||||
lastLineIndex: 10,
|
||||
stripRename: true,
|
||||
diffText: renameWithModificationDiff,
|
||||
expected: `diff --git a/newname b/newname
|
||||
index dcd3485..1ba5540 100644
|
||||
--- a/newname
|
||||
+++ b/newname
|
||||
@@ -1,5 +1,5 @@
|
||||
apple
|
||||
-orange
|
||||
+grape
|
||||
...
|
||||
...
|
||||
...
|
||||
`,
|
||||
},
|
||||
{
|
||||
testName: "renamed file, only removal selected, strips the rename",
|
||||
firstLineIndex: 9,
|
||||
lastLineIndex: 9,
|
||||
stripRename: true,
|
||||
diffText: renameWithModificationDiff,
|
||||
expected: `diff --git a/newname b/newname
|
||||
index dcd3485..1ba5540 100644
|
||||
--- a/newname
|
||||
+++ b/newname
|
||||
@@ -1,5 +1,4 @@
|
||||
apple
|
||||
-orange
|
||||
...
|
||||
...
|
||||
...
|
||||
`,
|
||||
},
|
||||
}
|
||||
@@ -527,6 +581,7 @@ func TestTransform(t *testing.T) {
|
||||
Transform(TransformOpts{
|
||||
Reverse: s.reverse,
|
||||
FileNameOverride: s.filename,
|
||||
StripRename: s.stripRename,
|
||||
IncludedLineIndices: lineIndices,
|
||||
}).
|
||||
FormatPlain()
|
||||
|
||||
@@ -33,6 +33,13 @@ type TransformOpts struct {
|
||||
// treat it as a diff against an empty file.
|
||||
TurnAddedFilesIntoDiffAgainstEmptyFile bool
|
||||
|
||||
// When building a partial patch for a renamed file, strip the rename
|
||||
// metadata from the header and point it at the new path. Applying the
|
||||
// resulting patch then only changes the file's contents and leaves the
|
||||
// rename itself in place. (For a whole-file selection we keep the rename
|
||||
// so that it moves or is discarded together with the contents.)
|
||||
StripRename bool
|
||||
|
||||
// The indices of lines that should be included in the patch.
|
||||
IncludedLineIndices []int
|
||||
}
|
||||
@@ -72,21 +79,61 @@ func (self *patchTransformer) transformHeader() []string {
|
||||
"--- a/" + self.opts.FileNameOverride,
|
||||
"+++ b/" + self.opts.FileNameOverride,
|
||||
}
|
||||
} else if self.opts.TurnAddedFilesIntoDiffAgainstEmptyFile {
|
||||
result := make([]string, 0, len(self.patch.header))
|
||||
for idx, line := range self.patch.header {
|
||||
}
|
||||
|
||||
header := self.patch.header
|
||||
if self.opts.StripRename {
|
||||
header = stripRenameFromHeader(header)
|
||||
}
|
||||
|
||||
if self.opts.TurnAddedFilesIntoDiffAgainstEmptyFile {
|
||||
result := make([]string, 0, len(header))
|
||||
for idx, line := range header {
|
||||
if strings.HasPrefix(line, "new file mode") {
|
||||
continue
|
||||
}
|
||||
if line == "--- /dev/null" && strings.HasPrefix(self.patch.header[idx+1], "+++ b/") {
|
||||
line = "--- a/" + self.patch.header[idx+1][6:]
|
||||
if line == "--- /dev/null" && strings.HasPrefix(header[idx+1], "+++ b/") {
|
||||
line = "--- a/" + header[idx+1][6:]
|
||||
}
|
||||
result = append(result, line)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
return self.patch.header
|
||||
return header
|
||||
}
|
||||
|
||||
// stripRenameFromHeader rewrites a rename diff header so that it looks like a
|
||||
// plain modification of the new path: it drops the rename metadata and points
|
||||
// the diff at the new path on both sides, while keeping the blob index line so
|
||||
// that `git apply --3way` can still fall back to a blob merge. See the
|
||||
// StripRename option for why we do this.
|
||||
func stripRenameFromHeader(header []string) []string {
|
||||
newPath := ""
|
||||
for _, line := range header {
|
||||
if path, ok := strings.CutPrefix(line, "+++ b/"); ok {
|
||||
newPath = path
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
result := make([]string, 0, len(header))
|
||||
for _, line := range header {
|
||||
switch {
|
||||
case strings.HasPrefix(line, "similarity index "),
|
||||
strings.HasPrefix(line, "dissimilarity index "),
|
||||
strings.HasPrefix(line, "rename from "),
|
||||
strings.HasPrefix(line, "rename to "):
|
||||
// drop the rename metadata
|
||||
case strings.HasPrefix(line, "diff --git "):
|
||||
result = append(result, "diff --git a/"+newPath+" b/"+newPath)
|
||||
case strings.HasPrefix(line, "--- "):
|
||||
result = append(result, "--- a/"+newPath)
|
||||
default:
|
||||
result = append(result, line)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func (self *patchTransformer) transformHunks() []*Hunk {
|
||||
|
||||
@@ -60,8 +60,11 @@ func NewContextTree(c *ContextCommon) *ContextTree {
|
||||
"main",
|
||||
PATCH_BUILDING_MAIN_CONTEXT_KEY,
|
||||
func() []int {
|
||||
filename := commitFilesContext.GetSelectedPath()
|
||||
includedLineIndices, err := c.Git().Patch.PatchBuilder.GetFileIncLineIndices(filename)
|
||||
file := commitFilesContext.GetSelectedFile()
|
||||
if file == nil {
|
||||
return nil
|
||||
}
|
||||
includedLineIndices, err := c.Git().Patch.PatchBuilder.GetFileIncLineIndices(file.Path, file.PreviousPath)
|
||||
if err != nil {
|
||||
c.Log.Error(err)
|
||||
return nil
|
||||
|
||||
@@ -191,11 +191,11 @@ func (self *CommitFilesController) GetOnRenderToMain() func() {
|
||||
}
|
||||
}
|
||||
|
||||
func (self *CommitFilesController) copyDiffToClipboard(path string, toastMessage string) error {
|
||||
func (self *CommitFilesController) copyDiffToClipboard(paths []string, toastMessage string) error {
|
||||
from, to := self.context().GetFromAndToForDiff()
|
||||
from, reverse := self.c.Modes().Diffing.GetFromAndReverseArgsForDiff(from)
|
||||
|
||||
cmdObj := self.c.Git().WorkingTree.ShowFileDiffCmdObj(from, to, reverse, []string{path}, true)
|
||||
cmdObj := self.c.Git().WorkingTree.ShowFileDiffCmdObj(from, to, reverse, paths, true)
|
||||
diff, err := cmdObj.RunWithOutput()
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -263,7 +263,7 @@ func (self *CommitFilesController) openCopyMenu() error {
|
||||
copyFileDiffItem := &types.MenuItem{
|
||||
Label: self.c.Tr.CopySelectedDiff,
|
||||
OnPress: func() error {
|
||||
return self.copyDiffToClipboard(node.GetPath(), self.c.Tr.FileDiffCopiedToast)
|
||||
return self.copyDiffToClipboard(self.pathsForDiff(node), self.c.Tr.FileDiffCopiedToast)
|
||||
},
|
||||
DisabledReason: self.require(self.singleItemSelected())(),
|
||||
Keys: menuKey('s'),
|
||||
@@ -271,7 +271,7 @@ func (self *CommitFilesController) openCopyMenu() error {
|
||||
copyAllDiff := &types.MenuItem{
|
||||
Label: self.c.Tr.CopyAllFilesDiff,
|
||||
OnPress: func() error {
|
||||
return self.copyDiffToClipboard(".", self.c.Tr.AllFilesDiffCopiedToast)
|
||||
return self.copyDiffToClipboard([]string{"."}, self.c.Tr.AllFilesDiffCopiedToast)
|
||||
},
|
||||
DisabledReason: self.require(self.itemsSelected())(),
|
||||
Keys: menuKey('a'),
|
||||
@@ -348,7 +348,10 @@ func (self *CommitFilesController) discard(selectedNodes []*filetree.CommitFileN
|
||||
|
||||
for _, node := range selectedNodes {
|
||||
_ = node.ForEachFile(func(file *models.CommitFile) error {
|
||||
filePaths = append(filePaths, file.GetPath())
|
||||
// For a rename we discard both the new and the old path,
|
||||
// so that the new file is removed and the old one is
|
||||
// restored.
|
||||
filePaths = append(filePaths, file.Names()...)
|
||||
return nil
|
||||
})
|
||||
}
|
||||
@@ -465,7 +468,7 @@ func (self *CommitFilesController) toggleForPatch(selectedNodes []*filetree.Comm
|
||||
|
||||
for _, node := range selectedNodes {
|
||||
err := node.ForEachFile(func(file *models.CommitFile) error {
|
||||
return patchOperationFunction(file.Path)
|
||||
return patchOperationFunction(file.Path, file.PreviousPath)
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -610,11 +613,16 @@ func (self *CommitFilesController) pathsForDiff(node *filetree.CommitFileNode) [
|
||||
if !node.IsFile() && self.context().IsFiltering() {
|
||||
var paths []string
|
||||
_ = node.ForEachFile(func(file *models.CommitFile) error {
|
||||
paths = append(paths, file.Path)
|
||||
// For a rename we need to pass both paths so that git detects it as
|
||||
// a rename rather than an unrelated delete and add.
|
||||
paths = append(paths, file.Names()...)
|
||||
return nil
|
||||
})
|
||||
return paths
|
||||
}
|
||||
if file := node.GetFile(); file != nil {
|
||||
return file.Names()
|
||||
}
|
||||
return []string{node.GetPath()}
|
||||
}
|
||||
|
||||
|
||||
@@ -66,20 +66,21 @@ func (self *PatchBuildingHelper) RefreshPatchBuildingPanel(opts types.OnFocusOpt
|
||||
}
|
||||
|
||||
// get diff from commit file that's currently selected
|
||||
path := self.c.Contexts().CommitFiles.GetSelectedPath()
|
||||
if path == "" {
|
||||
file := self.c.Contexts().CommitFiles.GetSelectedFile()
|
||||
if file == nil {
|
||||
return
|
||||
}
|
||||
|
||||
from, to := self.c.Contexts().CommitFiles.GetFromAndToForDiff()
|
||||
from, reverse := self.c.Modes().Diffing.GetFromAndReverseArgsForDiff(from)
|
||||
diff, err := self.c.Git().WorkingTree.ShowFileDiff(from, to, reverse, path, true)
|
||||
diff, err := self.c.Git().WorkingTree.ShowFileDiff(from, to, reverse, file.Path, file.PreviousPath, true)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
secondaryDiff := self.c.Git().Patch.PatchBuilder.RenderPatchForFile(patch.RenderPatchForFileOpts{
|
||||
Filename: path,
|
||||
Filename: file.Path,
|
||||
PreviousPath: file.PreviousPath,
|
||||
Plain: false,
|
||||
Reverse: false,
|
||||
TurnAddedFilesIntoDiffAgainstEmptyFile: true,
|
||||
|
||||
@@ -138,8 +138,8 @@ func (self *PatchBuildingController) toggleSelection() error {
|
||||
self.context().GetMutex().Lock()
|
||||
defer self.context().GetMutex().Unlock()
|
||||
|
||||
filename := self.c.Contexts().CommitFiles.GetSelectedPath()
|
||||
if filename == "" {
|
||||
file := self.c.Contexts().CommitFiles.GetSelectedFile()
|
||||
if file == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -152,7 +152,7 @@ func (self *PatchBuildingController) toggleSelection() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
includedLineIndices, err := self.c.Git().Patch.PatchBuilder.GetFileIncLineIndices(filename)
|
||||
includedLineIndices, err := self.c.Git().Patch.PatchBuilder.GetFileIncLineIndices(file.Path, file.PreviousPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -164,7 +164,7 @@ func (self *PatchBuildingController) toggleSelection() error {
|
||||
}
|
||||
|
||||
// add range of lines to those set for the file
|
||||
if err := toggleFunc(filename, lineIndicesToToggle); err != nil {
|
||||
if err := toggleFunc(file.Path, file.PreviousPath, lineIndicesToToggle); err != nil {
|
||||
// might actually want to return an error here
|
||||
self.c.Log.Error(err)
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package controllers
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/context"
|
||||
@@ -49,6 +50,10 @@ func (self *RenameSimilarityThresholdController) Context() types.Context {
|
||||
}
|
||||
|
||||
func (self *RenameSimilarityThresholdController) Increase() error {
|
||||
if err := self.checkCanChangeThreshold(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
old_size := self.c.UserConfig().Git.RenameSimilarityThreshold
|
||||
|
||||
if old_size < 100 {
|
||||
@@ -59,6 +64,10 @@ func (self *RenameSimilarityThresholdController) Increase() error {
|
||||
}
|
||||
|
||||
func (self *RenameSimilarityThresholdController) Decrease() error {
|
||||
if err := self.checkCanChangeThreshold(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
old_size := self.c.UserConfig().Git.RenameSimilarityThreshold
|
||||
|
||||
if old_size > 5 {
|
||||
@@ -73,11 +82,23 @@ func (self *RenameSimilarityThresholdController) applyChange() error {
|
||||
|
||||
currentContext := self.c.Context().CurrentSide()
|
||||
switch currentContext.GetKey() {
|
||||
// we make an exception for our files context, because it actually need to refresh its state afterwards.
|
||||
// we make an exception for the files and commit-files contexts, because
|
||||
// they actually need to refresh their state afterwards: a changed threshold
|
||||
// can turn a rename into a separate delete and add, or vice versa.
|
||||
case context.FILES_CONTEXT_KEY:
|
||||
self.c.Refresh(types.RefreshOptions{Scope: []types.RefreshableView{types.FILES}})
|
||||
case context.COMMIT_FILES_CONTEXT_KEY:
|
||||
self.c.Refresh(types.RefreshOptions{Scope: []types.RefreshableView{types.COMMIT_FILES}})
|
||||
default:
|
||||
currentContext.HandleRenderToMain()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (self *RenameSimilarityThresholdController) checkCanChangeThreshold() error {
|
||||
if self.c.Git().Patch.PatchBuilder.Active() {
|
||||
return errors.New(self.c.Tr.CantChangeRenameThresholdError)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -328,7 +328,8 @@ func fileNameAtDepth(node *filetree.Node[models.File], depth int, showRootItem b
|
||||
|
||||
func commitFileNameAtDepth(node *filetree.Node[models.CommitFile], depth int) string {
|
||||
splitName := split(node.GetInternalPath())
|
||||
if depth == 0 && splitName[0] == "." {
|
||||
showRootItem := splitName[0] == "."
|
||||
if depth == 0 && showRootItem {
|
||||
if len(splitName) == 1 {
|
||||
return "/"
|
||||
}
|
||||
@@ -336,6 +337,20 @@ func commitFileNameAtDepth(node *filetree.Node[models.CommitFile], depth int) st
|
||||
}
|
||||
name := join(splitName[depth:])
|
||||
|
||||
if node.File != nil && node.File.IsRename() {
|
||||
splitPrevName := filetree.SplitFileTreePath(node.File.PreviousPath, showRootItem)
|
||||
|
||||
prevName := node.File.PreviousPath
|
||||
// if the file has just been renamed inside the same directory, we can shave off
|
||||
// the prefix for the previous path too. Otherwise we'll keep it unchanged
|
||||
sameParentDir := len(splitName) == len(splitPrevName) && join(splitName[0:depth]) == join(splitPrevName[0:depth])
|
||||
if sameParentDir {
|
||||
prevName = join(splitPrevName[depth:])
|
||||
}
|
||||
|
||||
return prevName + " → " + name
|
||||
}
|
||||
|
||||
return name
|
||||
}
|
||||
|
||||
|
||||
@@ -151,6 +151,14 @@ func TestRenderCommitFileTree(t *testing.T) {
|
||||
showRootItem: true,
|
||||
expected: []string{"A test"},
|
||||
},
|
||||
{
|
||||
name: "renamed file",
|
||||
files: []*models.CommitFile{
|
||||
{Path: "new.txt", PreviousPath: "old.txt", ChangeStatus: "R"},
|
||||
},
|
||||
showRootItem: false,
|
||||
expected: []string{"R old.txt → new.txt"},
|
||||
},
|
||||
{
|
||||
name: "big example",
|
||||
files: []*models.CommitFile{
|
||||
@@ -219,7 +227,7 @@ M file1
|
||||
}
|
||||
patchBuilder := patch.NewPatchBuilder(
|
||||
utils.NewDummyLog(),
|
||||
func(from string, to string, reverse bool, filename string, plain bool) (string, error) {
|
||||
func(from string, to string, reverse bool, filename string, previousPath string, plain bool) (string, error) {
|
||||
return "", nil
|
||||
},
|
||||
)
|
||||
|
||||
@@ -819,6 +819,7 @@ type TranslationSet struct {
|
||||
SortCommits string
|
||||
SortCommitsTooltip string
|
||||
CantChangeContextSizeError string
|
||||
CantChangeRenameThresholdError string
|
||||
OpenCommitInBrowser string
|
||||
ViewBisectOptions string
|
||||
ConfirmRevertCommit string
|
||||
@@ -1966,6 +1967,7 @@ func EnglishTranslationSet() *TranslationSet {
|
||||
SortCommits: "Commit sort order",
|
||||
SortCommitsTooltip: "Change the sort order of the commits in the commit log.\n\nThe default can be changed in the config file with the key 'git.log.sortOrder'.",
|
||||
CantChangeContextSizeError: "Cannot change context while in patch building mode because we were too lazy to support it when releasing the feature. If you really want it, please let us know!",
|
||||
CantChangeRenameThresholdError: "Cannot change the rename similarity threshold while in patch building mode, because the custom patch can't cope with a rename turning into a delete and add underneath it.",
|
||||
OpenCommitInBrowser: "Open commit in browser",
|
||||
ViewBisectOptions: "View bisect options",
|
||||
ConfirmRevertCommit: "Are you sure you want to revert {{.selectedCommit}}?",
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
package commit
|
||||
|
||||
import (
|
||||
"github.com/jesseduffield/lazygit/pkg/config"
|
||||
. "github.com/jesseduffield/lazygit/pkg/integration/components"
|
||||
)
|
||||
|
||||
var DiscardRenamedFile = NewIntegrationTest(NewIntegrationTestArgs{
|
||||
Description: "Discard a renamed file from an old commit; both the new and the old path are handled so the rename is undone",
|
||||
ExtraCmdArgs: []string{},
|
||||
Skip: false,
|
||||
SetupConfig: func(config *config.AppConfig) {},
|
||||
SetupRepo: func(shell *Shell) {
|
||||
shell.CreateFileAndAdd("original", "line1\nline2\nline3\nline4\nline5\n")
|
||||
shell.CreateFileAndAdd("other", "other content\n")
|
||||
shell.Commit("first commit")
|
||||
|
||||
shell.RenameFileInGit("original", "renamed")
|
||||
shell.UpdateFileAndAdd("renamed", "line1\nline2 changed\nline3\nline4\nline5\n")
|
||||
shell.Commit("rename with modification")
|
||||
},
|
||||
Run: func(t *TestDriver, keys config.KeybindingConfig) {
|
||||
t.Views().Commits().
|
||||
Focus().
|
||||
Lines(
|
||||
Contains("rename with modification").IsSelected(),
|
||||
Contains("first commit"),
|
||||
).
|
||||
PressEnter()
|
||||
|
||||
t.Views().CommitFiles().
|
||||
IsFocused().
|
||||
Lines(
|
||||
Contains("original → renamed").IsSelected(),
|
||||
).
|
||||
Press(keys.Universal.Remove)
|
||||
|
||||
t.ExpectPopup().Confirmation().
|
||||
Title(Equals("Discard file changes")).
|
||||
Content(Contains("Are you sure you want to discard changes to the selected file(s) from this commit?")).
|
||||
Confirm()
|
||||
|
||||
// The rename is undone: the commit no longer touches any file. (If only
|
||||
// the new path were discarded, the commit would still delete the old
|
||||
// path and show "D original" here instead.)
|
||||
t.Views().CommitFiles().
|
||||
IsFocused().
|
||||
Lines(
|
||||
Contains("(none)"),
|
||||
).
|
||||
PressEscape()
|
||||
|
||||
// The working tree is clean; the original file is back at HEAD.
|
||||
t.Views().Files().
|
||||
IsEmpty()
|
||||
},
|
||||
})
|
||||
@@ -0,0 +1,60 @@
|
||||
package patch_building
|
||||
|
||||
import (
|
||||
"github.com/jesseduffield/lazygit/pkg/config"
|
||||
. "github.com/jesseduffield/lazygit/pkg/integration/components"
|
||||
)
|
||||
|
||||
// note: this is required to simulate the clipboard during CI
|
||||
func expectClipboard(t *TestDriver, matcher *TextMatcher) {
|
||||
defer t.Shell().DeleteFile("clipboard")
|
||||
|
||||
t.FileSystem().FileContent("clipboard", matcher)
|
||||
}
|
||||
|
||||
var CopyRenamedFileDiff = NewIntegrationTest(NewIntegrationTestArgs{
|
||||
Description: "Copy the diff of a renamed file to the clipboard; the diff shows the rename rather than a delete and add",
|
||||
ExtraCmdArgs: []string{},
|
||||
Skip: false,
|
||||
SetupConfig: func(config *config.AppConfig) {
|
||||
config.GetUserConfig().OS.CopyToClipboardCmd = "printf '%s' {{text}} > clipboard"
|
||||
},
|
||||
SetupRepo: func(shell *Shell) {
|
||||
shell.CreateFileAndAdd("original", "line1\nline2\nline3\nline4\nline5\n")
|
||||
shell.Commit("first commit")
|
||||
|
||||
shell.RenameFileInGit("original", "renamed")
|
||||
shell.UpdateFileAndAdd("renamed", "line1\nline2 changed\nline3\nline4\nline5\n")
|
||||
shell.Commit("rename with modification")
|
||||
},
|
||||
Run: func(t *TestDriver, keys config.KeybindingConfig) {
|
||||
t.Views().Commits().
|
||||
Focus().
|
||||
Lines(
|
||||
Contains("rename with modification").IsSelected(),
|
||||
Contains("first commit"),
|
||||
).
|
||||
PressEnter()
|
||||
|
||||
t.Views().CommitFiles().
|
||||
IsFocused().
|
||||
Lines(
|
||||
Contains("original → renamed").IsSelected(),
|
||||
).
|
||||
Press(keys.Files.CopyFileInfoToClipboard)
|
||||
|
||||
t.ExpectPopup().Menu().
|
||||
Title(Equals("Copy to clipboard")).
|
||||
Select(Contains("Diff of selected file")).
|
||||
Confirm()
|
||||
|
||||
t.ExpectToast(Contains("File diff copied to clipboard"))
|
||||
|
||||
expectClipboard(t,
|
||||
Contains("rename from original").
|
||||
Contains("rename to renamed").
|
||||
Contains("-line2").
|
||||
Contains("+line2 changed"),
|
||||
)
|
||||
},
|
||||
})
|
||||
@@ -0,0 +1,66 @@
|
||||
package patch_building
|
||||
|
||||
import (
|
||||
"github.com/jesseduffield/lazygit/pkg/config"
|
||||
. "github.com/jesseduffield/lazygit/pkg/integration/components"
|
||||
)
|
||||
|
||||
var RenameSimilarityThresholdChange = NewIntegrationTest(NewIntegrationTestArgs{
|
||||
Description: "Changing the rename similarity threshold refreshes the commit files panel, but is disabled while building a patch",
|
||||
ExtraCmdArgs: []string{},
|
||||
Skip: false,
|
||||
SetupConfig: func(config *config.AppConfig) {},
|
||||
SetupRepo: func(shell *Shell) {
|
||||
shell.CreateFileAndAdd("original", "one\ntwo\nthree\nfour\nfive\n")
|
||||
shell.Commit("add original")
|
||||
|
||||
shell.RenameFileInGit("original", "renamed")
|
||||
shell.UpdateFileAndAdd("renamed", "one\ntwo\nthree\nfour\nfive\nsix\nseven\neight\nnine\nten\n")
|
||||
shell.Commit("change name and contents")
|
||||
},
|
||||
Run: func(t *TestDriver, keys config.KeybindingConfig) {
|
||||
t.Views().Commits().
|
||||
Focus().
|
||||
Lines(
|
||||
Contains("change name and contents").IsSelected(),
|
||||
Contains("add original"),
|
||||
).
|
||||
PressEnter()
|
||||
|
||||
// At the default threshold of 50% the 50%-similar change is not detected
|
||||
// as a rename.
|
||||
t.Views().CommitFiles().
|
||||
IsFocused().
|
||||
Lines(
|
||||
Equals("▼ /"),
|
||||
Equals(" D original"),
|
||||
Equals(" A renamed"),
|
||||
).
|
||||
// Lowering the threshold turns it into a rename; the panel refreshes.
|
||||
Press(keys.Universal.DecreaseRenameSimilarityThreshold).
|
||||
Tap(func() {
|
||||
t.ExpectToast(Equals("Changed rename similarity threshold to 45%"))
|
||||
}).
|
||||
Lines(
|
||||
Equals("R original → renamed"),
|
||||
).
|
||||
// Start building a patch from the renamed file.
|
||||
PressPrimaryAction().
|
||||
Tap(func() {
|
||||
t.Views().Information().Content(Contains("Building patch"))
|
||||
|
||||
// Changing the threshold is now disabled: the patch builder
|
||||
// can't cope with the rename turning into a delete and add.
|
||||
t.Views().CommitFiles().
|
||||
Press(keys.Universal.IncreaseRenameSimilarityThreshold)
|
||||
t.ExpectPopup().Alert().
|
||||
Title(Equals("Error")).
|
||||
Content(Contains("Cannot change the rename similarity threshold while in patch building mode")).
|
||||
Confirm()
|
||||
}).
|
||||
// The file is unchanged: still a rename, still in the patch.
|
||||
Lines(
|
||||
Contains("original → renamed").IsSelected(),
|
||||
)
|
||||
},
|
||||
})
|
||||
@@ -0,0 +1,73 @@
|
||||
package patch_building
|
||||
|
||||
import (
|
||||
"github.com/jesseduffield/lazygit/pkg/config"
|
||||
. "github.com/jesseduffield/lazygit/pkg/integration/components"
|
||||
)
|
||||
|
||||
var RenamedFilePartial = NewIntegrationTest(NewIntegrationTestArgs{
|
||||
Description: "Select part of a renamed file's changes into a custom patch and remove it from the commit, keeping the rename in place",
|
||||
ExtraCmdArgs: []string{},
|
||||
Skip: false,
|
||||
SetupConfig: func(config *config.AppConfig) {},
|
||||
SetupRepo: func(shell *Shell) {
|
||||
shell.CreateFileAndAdd("original", "line1\nline2\nline3\nline4\nline5\n")
|
||||
shell.Commit("first commit")
|
||||
|
||||
shell.RenameFileInGit("original", "renamed")
|
||||
shell.UpdateFileAndAdd("renamed", "line1\nline2 changed\nline3\nline4\nline5\n")
|
||||
shell.Commit("rename with modification")
|
||||
},
|
||||
Run: func(t *TestDriver, keys config.KeybindingConfig) {
|
||||
t.Views().Commits().
|
||||
Focus().
|
||||
Lines(
|
||||
Contains("rename with modification").IsSelected(),
|
||||
Contains("first commit"),
|
||||
).
|
||||
PressEnter()
|
||||
|
||||
t.Views().CommitFiles().
|
||||
IsFocused().
|
||||
Lines(
|
||||
Contains("original → renamed").IsSelected(),
|
||||
).
|
||||
PressEnter()
|
||||
|
||||
// The main view shows the rename together with its content change.
|
||||
t.Views().PatchBuilding().
|
||||
IsFocused().
|
||||
Content(Contains("rename from original").Contains("rename to renamed")).
|
||||
ContainsLines(
|
||||
Contains(" line1"),
|
||||
Contains("-line2"),
|
||||
Contains("+line2 changed"),
|
||||
Contains(" line3"),
|
||||
).
|
||||
// Add the hunk (a line selection, as opposed to adding the whole
|
||||
// file), so this is a partial patch.
|
||||
PressPrimaryAction()
|
||||
|
||||
t.Views().Information().Content(Contains("Building patch"))
|
||||
|
||||
t.Common().SelectPatchOption(Contains("Remove patch from original commit"))
|
||||
|
||||
// The rename is preserved; only the content change is gone, so the file
|
||||
// is still shown as a rename but now has no content change.
|
||||
t.Views().CommitFiles().
|
||||
IsFocused().
|
||||
Lines(
|
||||
Contains("original → renamed").IsSelected(),
|
||||
)
|
||||
|
||||
t.Views().Main().
|
||||
Content(DoesNotContain("line2 changed"))
|
||||
|
||||
t.Views().Commits().
|
||||
Focus().
|
||||
Lines(
|
||||
Contains("rename with modification").IsSelected(),
|
||||
Contains("first commit"),
|
||||
)
|
||||
},
|
||||
})
|
||||
@@ -0,0 +1,62 @@
|
||||
package patch_building
|
||||
|
||||
import (
|
||||
"github.com/jesseduffield/lazygit/pkg/config"
|
||||
. "github.com/jesseduffield/lazygit/pkg/integration/components"
|
||||
)
|
||||
|
||||
var RenamedFileWhole = NewIntegrationTest(NewIntegrationTestArgs{
|
||||
Description: "Add a whole renamed file to a custom patch and remove it from the commit, taking the rename with it",
|
||||
ExtraCmdArgs: []string{},
|
||||
Skip: false,
|
||||
SetupConfig: func(config *config.AppConfig) {},
|
||||
SetupRepo: func(shell *Shell) {
|
||||
shell.CreateFileAndAdd("original", "line1\nline2\nline3\nline4\nline5\n")
|
||||
shell.Commit("first commit")
|
||||
|
||||
shell.RenameFileInGit("original", "renamed")
|
||||
shell.UpdateFileAndAdd("renamed", "line1\nline2 changed\nline3\nline4\nline5\n")
|
||||
shell.Commit("rename with modification")
|
||||
},
|
||||
Run: func(t *TestDriver, keys config.KeybindingConfig) {
|
||||
t.Views().Commits().
|
||||
Focus().
|
||||
Lines(
|
||||
Contains("rename with modification").IsSelected(),
|
||||
Contains("first commit"),
|
||||
).
|
||||
PressEnter()
|
||||
|
||||
t.Views().CommitFiles().
|
||||
IsFocused().
|
||||
Lines(
|
||||
Contains("original → renamed").IsSelected(),
|
||||
).
|
||||
PressPrimaryAction()
|
||||
|
||||
t.Views().Information().Content(Contains("Building patch"))
|
||||
|
||||
// The whole file is added, so the patch carries the rename itself.
|
||||
t.Views().Secondary().
|
||||
ContainsLines(
|
||||
Contains("rename from original"),
|
||||
Contains("rename to renamed"),
|
||||
)
|
||||
|
||||
t.Common().SelectPatchOption(Contains("Remove patch from original commit"))
|
||||
|
||||
// The rename went with the patch, so the commit no longer touches the file.
|
||||
t.Views().CommitFiles().
|
||||
IsFocused().
|
||||
Lines(
|
||||
Contains("(none)"),
|
||||
)
|
||||
|
||||
t.Views().Commits().
|
||||
Focus().
|
||||
Lines(
|
||||
Contains("rename with modification").IsSelected(),
|
||||
Contains("first commit"),
|
||||
)
|
||||
},
|
||||
})
|
||||
@@ -128,6 +128,7 @@ var tests = []*components.IntegrationTest{
|
||||
commit.CreateTag,
|
||||
commit.DisableCopyCommitMessageBody,
|
||||
commit.DiscardOldFileChanges,
|
||||
commit.DiscardRenamedFile,
|
||||
commit.DiscardSubmoduleChanges,
|
||||
commit.DoNotShowBranchMarkerForHeadCommit,
|
||||
commit.FailHooksThenCommitNoHooks,
|
||||
@@ -351,6 +352,7 @@ var tests = []*components.IntegrationTest{
|
||||
patch_building.ApplyInReverseWithConflict,
|
||||
patch_building.ApplyWithModifiedFileConflict,
|
||||
patch_building.ApplyWithModifiedFileNoConflict,
|
||||
patch_building.CopyRenamedFileDiff,
|
||||
patch_building.DiscardLinesFromCommit,
|
||||
patch_building.EditLineInPatchBuildingPanel,
|
||||
patch_building.MoveRangeToIndex,
|
||||
@@ -373,6 +375,9 @@ var tests = []*components.IntegrationTest{
|
||||
patch_building.MoveToNewCommitPartialHunk,
|
||||
patch_building.RemoveFromCommit,
|
||||
patch_building.RemovePartsOfAddedFile,
|
||||
patch_building.RenameSimilarityThresholdChange,
|
||||
patch_building.RenamedFilePartial,
|
||||
patch_building.RenamedFileWhole,
|
||||
patch_building.ResetWithEscape,
|
||||
patch_building.SelectAllFiles,
|
||||
patch_building.SpecificSelection,
|
||||
|
||||
Reference in New Issue
Block a user