Skip to content

Commit a08c86c

Browse files
authored
Fix custom patch operations for added files (#3684)
- **PR Description** Several custom patch commands on parts of an added file would fail with the confusing error message "error: new file XXX depends on old contents". These were dropping the custom patch from the original commit, moving the patch to a new commit, moving it to a later commit, or moving it to the index. We fix this by converting the patch header from an added file to a diff against an empty file. We do this not just for the purpose of applying the patch, but also for rendering it and copying it to the clip board. I'm not sure it matters much in these cases, but it does feel more correct for a filtered patch to be presented this way. Fixes #3679. - **Please check if the PR fulfills these requirements** * [x] Cheatsheets are up-to-date (run `go generate ./...`) * [x] Code has been formatted (see [here](https://github.com/jesseduffield/lazygit/blob/master/CONTRIBUTING.md#code-formatting)) * [x] Tests have been added/updated (see [here](https://github.com/jesseduffield/lazygit/blob/master/pkg/integration/README.md) for the integration test guide) * [ ] Text is internationalised (see [here](https://github.com/jesseduffield/lazygit/blob/master/CONTRIBUTING.md#internationalisation)) * [ ] Docs have been updated if necessary * [x] You've read through your own file changes for silly mistakes etc
2 parents b2c4573 + 4cd15a3 commit a08c86c

15 files changed

+516
-26
lines changed

pkg/commands/git_commands/patch.go

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -47,8 +47,8 @@ type ApplyPatchOpts struct {
4747
Reverse bool
4848
}
4949

50-
func (self *PatchCommands) ApplyCustomPatch(reverse bool) error {
51-
patch := self.PatchBuilder.PatchToApply(reverse)
50+
func (self *PatchCommands) ApplyCustomPatch(reverse bool, turnAddedFilesIntoDiffAgainstEmptyFile bool) error {
51+
patch := self.PatchBuilder.PatchToApply(reverse, turnAddedFilesIntoDiffAgainstEmptyFile)
5252

5353
return self.ApplyPatch(patch, ApplyPatchOpts{
5454
Index: true,
@@ -94,7 +94,7 @@ func (self *PatchCommands) DeletePatchesFromCommit(commits []*models.Commit, com
9494
}
9595

9696
// apply each patch in reverse
97-
if err := self.ApplyCustomPatch(true); err != nil {
97+
if err := self.ApplyCustomPatch(true, true); err != nil {
9898
_ = self.rebase.AbortRebase()
9999
return err
100100
}
@@ -123,7 +123,7 @@ func (self *PatchCommands) MovePatchToSelectedCommit(commits []*models.Commit, s
123123
}
124124

125125
// apply each patch forward
126-
if err := self.ApplyCustomPatch(false); err != nil {
126+
if err := self.ApplyCustomPatch(false, false); err != nil {
127127
// Don't abort the rebase here; this might cause conflicts, so give
128128
// the user a chance to resolve them
129129
return err
@@ -172,7 +172,7 @@ func (self *PatchCommands) MovePatchToSelectedCommit(commits []*models.Commit, s
172172
}
173173

174174
// apply each patch in reverse
175-
if err := self.ApplyCustomPatch(true); err != nil {
175+
if err := self.ApplyCustomPatch(true, true); err != nil {
176176
_ = self.rebase.AbortRebase()
177177
return err
178178
}
@@ -228,7 +228,7 @@ func (self *PatchCommands) MovePatchIntoIndex(commits []*models.Commit, commitId
228228
return err
229229
}
230230

231-
if err := self.ApplyCustomPatch(true); err != nil {
231+
if err := self.ApplyCustomPatch(true, true); err != nil {
232232
if self.status.WorkingTreeState() == enums.REBASE_MODE_REBASING {
233233
_ = self.rebase.AbortRebase()
234234
}
@@ -282,7 +282,7 @@ func (self *PatchCommands) PullPatchIntoNewCommit(
282282
return err
283283
}
284284

285-
if err := self.ApplyCustomPatch(true); err != nil {
285+
if err := self.ApplyCustomPatch(true, true); err != nil {
286286
_ = self.rebase.AbortRebase()
287287
return err
288288
}

pkg/commands/patch/patch_builder.go

Lines changed: 27 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -65,15 +65,20 @@ func (p *PatchBuilder) Start(from, to string, reverse bool, canRebase bool) {
6565
p.fileInfoMap = map[string]*fileInfo{}
6666
}
6767

68-
func (p *PatchBuilder) PatchToApply(reverse bool) string {
68+
func (p *PatchBuilder) PatchToApply(reverse bool, turnAddedFilesIntoDiffAgainstEmptyFile bool) string {
6969
patch := ""
7070

7171
for filename, info := range p.fileInfoMap {
7272
if info.mode == UNSELECTED {
7373
continue
7474
}
7575

76-
patch += p.RenderPatchForFile(filename, true, reverse)
76+
patch += p.RenderPatchForFile(RenderPatchForFileOpts{
77+
Filename: filename,
78+
Plain: true,
79+
Reverse: reverse,
80+
TurnAddedFilesIntoDiffAgainstEmptyFile: turnAddedFilesIntoDiffAgainstEmptyFile,
81+
})
7782
}
7883

7984
return patch
@@ -172,8 +177,15 @@ func (p *PatchBuilder) RemoveFileLineRange(filename string, firstLineIdx, lastLi
172177
return nil
173178
}
174179

175-
func (p *PatchBuilder) RenderPatchForFile(filename string, plain bool, reverse bool) string {
176-
info, err := p.getFileInfo(filename)
180+
type RenderPatchForFileOpts struct {
181+
Filename string
182+
Plain bool
183+
Reverse bool
184+
TurnAddedFilesIntoDiffAgainstEmptyFile bool
185+
}
186+
187+
func (p *PatchBuilder) RenderPatchForFile(opts RenderPatchForFileOpts) string {
188+
info, err := p.getFileInfo(opts.Filename)
177189
if err != nil {
178190
p.Log.Error(err)
179191
return ""
@@ -183,7 +195,7 @@ func (p *PatchBuilder) RenderPatchForFile(filename string, plain bool, reverse b
183195
return ""
184196
}
185197

186-
if info.mode == WHOLE && plain {
198+
if info.mode == WHOLE && opts.Plain {
187199
// Use the whole diff (spares us parsing it and then formatting it).
188200
// TODO: see if this is actually noticeably faster.
189201
// The reverse flag is only for part patches so we're ignoring it here.
@@ -192,11 +204,12 @@ func (p *PatchBuilder) RenderPatchForFile(filename string, plain bool, reverse b
192204

193205
patch := Parse(info.diff).
194206
Transform(TransformOpts{
195-
Reverse: reverse,
196-
IncludedLineIndices: info.includedLineIndices,
207+
Reverse: opts.Reverse,
208+
TurnAddedFilesIntoDiffAgainstEmptyFile: opts.TurnAddedFilesIntoDiffAgainstEmptyFile,
209+
IncludedLineIndices: info.includedLineIndices,
197210
})
198211

199-
if plain {
212+
if opts.Plain {
200213
return patch.FormatPlain()
201214
} else {
202215
return patch.FormatView(FormatViewOpts{})
@@ -209,7 +222,12 @@ func (p *PatchBuilder) renderEachFilePatch(plain bool) []string {
209222

210223
sort.Strings(filenames)
211224
patches := lo.Map(filenames, func(filename string, _ int) string {
212-
return p.RenderPatchForFile(filename, plain, false)
225+
return p.RenderPatchForFile(RenderPatchForFileOpts{
226+
Filename: filename,
227+
Plain: plain,
228+
Reverse: false,
229+
TurnAddedFilesIntoDiffAgainstEmptyFile: true,
230+
})
213231
})
214232
output := lo.Filter(patches, func(patch string, _ int) bool {
215233
return patch != ""

pkg/commands/patch/transform.go

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
package patch
22

3-
import "github.com/samber/lo"
3+
import (
4+
"strings"
5+
6+
"github.com/samber/lo"
7+
)
48

59
type patchTransformer struct {
610
patch *Patch
@@ -22,6 +26,13 @@ type TransformOpts struct {
2226
// information it needs to cleanly apply patches
2327
FileNameOverride string
2428

29+
// Custom patches tend to work better when treating new files as diffs
30+
// against an empty file. The only case where we need this to be false is
31+
// when moving a custom patch to an earlier commit; in that case the patch
32+
// command would fail with the error "file does not exist in index" if we
33+
// treat it as a diff against an empty file.
34+
TurnAddedFilesIntoDiffAgainstEmptyFile bool
35+
2536
// The indices of lines that should be included in the patch.
2637
IncludedLineIndices []int
2738
}
@@ -61,6 +72,18 @@ func (self *patchTransformer) transformHeader() []string {
6172
"--- a/" + self.opts.FileNameOverride,
6273
"+++ b/" + self.opts.FileNameOverride,
6374
}
75+
} else if self.opts.TurnAddedFilesIntoDiffAgainstEmptyFile {
76+
result := make([]string, 0, len(self.patch.header))
77+
for idx, line := range self.patch.header {
78+
if strings.HasPrefix(line, "new file mode") {
79+
continue
80+
}
81+
if line == "--- /dev/null" && strings.HasPrefix(self.patch.header[idx+1], "+++ b/") {
82+
line = "--- a/" + self.patch.header[idx+1][6:]
83+
}
84+
result = append(result, line)
85+
}
86+
return result
6487
} else {
6588
return self.patch.header
6689
}

pkg/gui/controllers/custom_patch_options_menu_action.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -237,7 +237,7 @@ func (self *CustomPatchOptionsMenuAction) handleApplyPatch(reverse bool) error {
237237
action = "Apply patch in reverse"
238238
}
239239
self.c.LogAction(action)
240-
if err := self.c.Git().Patch.ApplyCustomPatch(reverse); err != nil {
240+
if err := self.c.Git().Patch.ApplyCustomPatch(reverse, true); err != nil {
241241
return err
242242
}
243243
return self.c.Refresh(types.RefreshOptions{Mode: types.ASYNC})

pkg/gui/controllers/helpers/patch_building_helper.go

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package helpers
33
import (
44
"errors"
55

6+
"github.com/jesseduffield/lazygit/pkg/commands/patch"
67
"github.com/jesseduffield/lazygit/pkg/commands/types/enums"
78
"github.com/jesseduffield/lazygit/pkg/gui/patch_exploring"
89
"github.com/jesseduffield/lazygit/pkg/gui/types"
@@ -80,7 +81,12 @@ func (self *PatchBuildingHelper) RefreshPatchBuildingPanel(opts types.OnFocusOpt
8081
return err
8182
}
8283

83-
secondaryDiff := self.c.Git().Patch.PatchBuilder.RenderPatchForFile(path, false, false)
84+
secondaryDiff := self.c.Git().Patch.PatchBuilder.RenderPatchForFile(patch.RenderPatchForFileOpts{
85+
Filename: path,
86+
Plain: false,
87+
Reverse: false,
88+
TurnAddedFilesIntoDiffAgainstEmptyFile: true,
89+
})
8490

8591
context := self.c.Contexts().CustomPatchBuilder
8692

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
package patch_building
2+
3+
import (
4+
"github.com/jesseduffield/lazygit/pkg/config"
5+
. "github.com/jesseduffield/lazygit/pkg/integration/components"
6+
)
7+
8+
var MoveToEarlierCommitFromAddedFile = NewIntegrationTest(NewIntegrationTestArgs{
9+
Description: "Move a patch from a file that was added in a commit to an earlier commit",
10+
ExtraCmdArgs: []string{},
11+
Skip: false,
12+
SetupConfig: func(config *config.AppConfig) {},
13+
SetupRepo: func(shell *Shell) {
14+
shell.EmptyCommit("first commit")
15+
shell.EmptyCommit("destination commit")
16+
shell.CreateFileAndAdd("file1", "1st line\n2nd line\n3rd line\n")
17+
shell.Commit("commit to move from")
18+
},
19+
Run: func(t *TestDriver, keys config.KeybindingConfig) {
20+
t.Views().Commits().
21+
Focus().
22+
Lines(
23+
Contains("commit to move from").IsSelected(),
24+
Contains("destination commit"),
25+
Contains("first commit"),
26+
).
27+
PressEnter()
28+
29+
t.Views().CommitFiles().
30+
IsFocused().
31+
Lines(
32+
Contains("A file").IsSelected(),
33+
).
34+
PressEnter()
35+
36+
t.Views().PatchBuilding().
37+
IsFocused().
38+
SelectNextItem().
39+
PressPrimaryAction()
40+
41+
t.Views().Information().Content(Contains("Building patch"))
42+
43+
t.Views().Commits().
44+
Focus().
45+
SelectNextItem()
46+
47+
t.Common().SelectPatchOption(Contains("Move patch to selected commit"))
48+
49+
// This results in a conflict at the commit we're moving from, because
50+
// it tries to add a file that already exists
51+
t.Common().AcknowledgeConflicts()
52+
53+
t.Views().Files().
54+
IsFocused().
55+
Lines(
56+
Contains("AA").Contains("file"),
57+
).
58+
PressEnter()
59+
60+
t.Views().MergeConflicts().
61+
IsFocused().
62+
TopLines(
63+
Contains("<<<<<<< HEAD"),
64+
Contains("2nd line"),
65+
Contains("======="),
66+
Contains("1st line"),
67+
Contains("2nd line"),
68+
Contains("3rd line"),
69+
Contains(">>>>>>>"),
70+
).
71+
SelectNextItem().
72+
PressPrimaryAction() // choose the version with all three lines
73+
74+
t.Common().ContinueOnConflictsResolved()
75+
76+
t.Views().Commits().
77+
Focus().
78+
Lines(
79+
Contains("commit to move from"),
80+
Contains("destination commit").IsSelected(),
81+
Contains("first commit"),
82+
).
83+
PressEnter()
84+
85+
t.Views().CommitFiles().
86+
IsFocused().
87+
Lines(
88+
Contains("A file").IsSelected(),
89+
).
90+
Tap(func() {
91+
t.Views().Main().ContainsLines(
92+
Equals("+2nd line"),
93+
)
94+
}).
95+
PressEscape()
96+
97+
t.Views().Commits().
98+
IsFocused().
99+
NavigateToLine(Contains("commit to move from")).
100+
PressEnter()
101+
102+
t.Views().CommitFiles().
103+
IsFocused().
104+
Lines(
105+
Contains("M file").IsSelected(),
106+
).
107+
Tap(func() {
108+
t.Views().Main().ContainsLines(
109+
Equals("+1st line"),
110+
Equals(" 2nd line"),
111+
Equals("+3rd line"),
112+
)
113+
})
114+
},
115+
})

0 commit comments

Comments
 (0)