Skip to content

Commit 91d8c25

Browse files
authored
Fix applying custom patches to a dirty working tree (#4674)
- **PR Description** Applying or reverting a custom patch when one of the files contained in the patch had unstaged modifications in the working tree would fail with the confusing error message "does not match index", regardless of whether those modifications conflicted with the patch or not. You would expect this to just work if there are no conflicts, or to get the usual conflict markers if there are. It was possible to work around this by manually staging those files, but few people knew about this. Fix this by staging the files (after asking for confirmation). Also, fix a minor problem where an auto-stash for the "move patch to index" command was forgotten to be dropped.
2 parents eebc39d + 3df894e commit 91d8c25

File tree

8 files changed

+271
-16
lines changed

8 files changed

+271
-16
lines changed

pkg/commands/git_commands/patch.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -259,7 +259,7 @@ func (self *PatchCommands) MovePatchIntoIndex(commits []*models.Commit, commitId
259259
}
260260

261261
if stash {
262-
if err := self.stash.Apply(0); err != nil {
262+
if err := self.stash.Pop(0); err != nil {
263263
return err
264264
}
265265
}

pkg/commands/patch/patch_builder.go

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -286,11 +286,5 @@ func (p *PatchBuilder) NewPatchRequired(from string, to string, reverse bool) bo
286286
}
287287

288288
func (p *PatchBuilder) AllFilesInPatch() []string {
289-
files := make([]string, 0, len(p.fileInfoMap))
290-
291-
for filename := range p.fileInfoMap {
292-
files = append(files, filename)
293-
}
294-
295-
return files
289+
return lo.Keys(p.fileInfoMap)
296290
}

pkg/gui/controllers/custom_patch_options_menu_action.go

Lines changed: 51 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,13 @@ import (
44
"errors"
55
"fmt"
66

7+
"github.com/jesseduffield/generics/set"
78
"github.com/jesseduffield/gocui"
9+
"github.com/jesseduffield/lazygit/pkg/commands/models"
810
"github.com/jesseduffield/lazygit/pkg/gui/controllers/helpers"
911
"github.com/jesseduffield/lazygit/pkg/gui/types"
1012
"github.com/jesseduffield/lazygit/pkg/utils"
13+
"github.com/samber/lo"
1114
)
1215

1316
type CustomPatchOptionsMenuAction struct {
@@ -267,16 +270,42 @@ func (self *CustomPatchOptionsMenuAction) handlePullPatchIntoNewCommitBefore() e
267270
func (self *CustomPatchOptionsMenuAction) handleApplyPatch(reverse bool) error {
268271
self.returnFocusFromPatchExplorerIfNecessary()
269272

270-
action := self.c.Tr.Actions.ApplyPatch
271-
if reverse {
272-
action = "Apply patch in reverse"
273+
affectedUnstagedFiles := self.getAffectedUnstagedFiles()
274+
275+
apply := func() error {
276+
action := self.c.Tr.Actions.ApplyPatch
277+
if reverse {
278+
action = "Apply patch in reverse"
279+
}
280+
self.c.LogAction(action)
281+
282+
if len(affectedUnstagedFiles) > 0 {
283+
if err := self.c.Git().WorkingTree.StageFiles(affectedUnstagedFiles, nil); err != nil {
284+
return err
285+
}
286+
}
287+
288+
if err := self.c.Git().Patch.ApplyCustomPatch(reverse, true); err != nil {
289+
return err
290+
}
291+
292+
self.c.Refresh(types.RefreshOptions{Mode: types.ASYNC})
293+
return nil
273294
}
274-
self.c.LogAction(action)
275-
if err := self.c.Git().Patch.ApplyCustomPatch(reverse, true); err != nil {
276-
return err
295+
296+
if len(affectedUnstagedFiles) > 0 {
297+
self.c.Confirm(types.ConfirmOpts{
298+
Title: self.c.Tr.MustStageFilesAffectedByPatchTitle,
299+
Prompt: self.c.Tr.MustStageFilesAffectedByPatchWarning,
300+
HandleConfirm: func() error {
301+
return apply()
302+
},
303+
})
304+
305+
return nil
277306
}
278-
self.c.Refresh(types.RefreshOptions{Mode: types.ASYNC})
279-
return nil
307+
308+
return apply()
280309
}
281310

282311
func (self *CustomPatchOptionsMenuAction) copyPatchToClipboard() error {
@@ -291,3 +320,17 @@ func (self *CustomPatchOptionsMenuAction) copyPatchToClipboard() error {
291320

292321
return nil
293322
}
323+
324+
// Returns a list of files that have unstaged changes and are contained in the patch.
325+
func (self *CustomPatchOptionsMenuAction) getAffectedUnstagedFiles() []string {
326+
unstagedFiles := set.NewFromSlice(lo.FilterMap(self.c.Model().Files, func(f *models.File, _ int) (string, bool) {
327+
if f.GetHasUnstagedChanges() {
328+
return f.GetPath(), true
329+
}
330+
return "", false
331+
}))
332+
333+
return lo.Filter(self.c.Git().Patch.PatchBuilder.AllFilesInPatch(), func(patchFile string, _ int) bool {
334+
return unstagedFiles.Includes(patchFile)
335+
})
336+
}

pkg/i18n/english.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -820,6 +820,8 @@ type TranslationSet struct {
820820
MovePatchToSelectedCommit string
821821
MovePatchToSelectedCommitTooltip string
822822
CopyPatchToClipboard string
823+
MustStageFilesAffectedByPatchTitle string
824+
MustStageFilesAffectedByPatchWarning string
823825
NoMatchesFor string
824826
MatchesFor string
825827
SearchKeybindings string
@@ -1909,6 +1911,8 @@ func EnglishTranslationSet() *TranslationSet {
19091911
MovePatchToSelectedCommit: "Move patch to selected commit (%s)",
19101912
MovePatchToSelectedCommitTooltip: "Move the patch out of its original commit and into the selected commit. This is achieved by starting an interactive rebase at the original commit, applying the patch in reverse, then continuing the rebase up to the selected commit, before applying the patch forward and amending the selected commit. The rebase is then continued to completion. If commits between the source and destination commit depend on the patch, you may need to resolve conflicts.",
19111913
CopyPatchToClipboard: "Copy patch to clipboard",
1914+
MustStageFilesAffectedByPatchTitle: "Must stage files",
1915+
MustStageFilesAffectedByPatchWarning: "Applying a patch to the index requires staging the unstaged files that are affected by the patch. Note that you might get conflicts when applying the patch. Continue?",
19121916
NoMatchesFor: "No matches for '%s' %s",
19131917
ExitSearchMode: "%s: Exit search mode",
19141918
ExitTextFilterMode: "%s: Exit filter mode",
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
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 ApplyWithModifiedFileConflict = NewIntegrationTest(NewIntegrationTestArgs{
9+
Description: "Apply a custom patch, with a modified file in the working tree that conflicts with the patch",
10+
ExtraCmdArgs: []string{},
11+
Skip: false,
12+
SetupConfig: func(config *config.AppConfig) {},
13+
SetupRepo: func(shell *Shell) {
14+
shell.NewBranch("branch-a")
15+
shell.CreateFileAndAdd("file1", "1\n2\n3\n")
16+
shell.Commit("first commit")
17+
18+
shell.NewBranch("branch-b")
19+
shell.UpdateFileAndAdd("file1", "11\n2\n3\n")
20+
shell.Commit("update")
21+
22+
shell.Checkout("branch-a")
23+
shell.UpdateFile("file1", "111\n2\n3\n")
24+
},
25+
Run: func(t *TestDriver, keys config.KeybindingConfig) {
26+
t.Views().Branches().
27+
Focus().
28+
Lines(
29+
Contains("branch-a").IsSelected(),
30+
Contains("branch-b"),
31+
).
32+
Press(keys.Universal.NextItem).
33+
PressEnter()
34+
35+
t.Views().SubCommits().
36+
IsFocused().
37+
Lines(
38+
Contains("update").IsSelected(),
39+
Contains("first commit"),
40+
).
41+
PressEnter()
42+
43+
t.Views().CommitFiles().
44+
IsFocused().
45+
Lines(
46+
Equals("M file1").IsSelected(),
47+
).
48+
PressPrimaryAction()
49+
50+
t.Views().Information().Content(Contains("Building patch"))
51+
52+
t.Views().Secondary().Content(Contains("-1\n+11\n"))
53+
54+
t.Common().SelectPatchOption(MatchesRegexp(`Apply patch$`))
55+
56+
t.ExpectPopup().Confirmation().Title(Equals("Must stage files")).
57+
Content(Contains("Applying a patch to the index requires staging the unstaged files that are affected by the patch.")).
58+
Confirm()
59+
60+
t.ExpectPopup().Alert().Title(Equals("Error")).
61+
Content(Contains("Applied patch to 'file1' with conflicts.")).
62+
Confirm()
63+
64+
t.Views().Files().
65+
Focus().
66+
Lines(
67+
Equals("UU file1").IsSelected(),
68+
).
69+
PressEnter()
70+
71+
t.Views().MergeConflicts().
72+
IsFocused().
73+
Lines(
74+
Equals("<<<<<<< ours"),
75+
Equals("111"),
76+
Equals("======="),
77+
Equals("11"),
78+
Equals(">>>>>>> theirs"),
79+
Equals("2"),
80+
Equals("3"),
81+
)
82+
},
83+
})
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
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 ApplyWithModifiedFileNoConflict = NewIntegrationTest(NewIntegrationTestArgs{
9+
Description: "Apply a custom patch, with a modified file in the working tree that does not conflict with the patch",
10+
ExtraCmdArgs: []string{},
11+
Skip: false,
12+
SetupConfig: func(config *config.AppConfig) {},
13+
SetupRepo: func(shell *Shell) {
14+
shell.NewBranch("branch-a")
15+
shell.CreateFileAndAdd("file1", "1\n2\n3\n")
16+
shell.Commit("first commit")
17+
18+
shell.NewBranch("branch-b")
19+
shell.UpdateFileAndAdd("file1", "1\n2\n3\n4\n")
20+
shell.Commit("update")
21+
22+
shell.Checkout("branch-a")
23+
shell.UpdateFile("file1", "11\n2\n3\n")
24+
},
25+
Run: func(t *TestDriver, keys config.KeybindingConfig) {
26+
t.Views().Branches().
27+
Focus().
28+
Lines(
29+
Contains("branch-a").IsSelected(),
30+
Contains("branch-b"),
31+
).
32+
Press(keys.Universal.NextItem).
33+
PressEnter()
34+
35+
t.Views().SubCommits().
36+
IsFocused().
37+
Lines(
38+
Contains("update").IsSelected(),
39+
Contains("first commit"),
40+
).
41+
PressEnter()
42+
43+
t.Views().CommitFiles().
44+
IsFocused().
45+
Lines(
46+
Equals("M file1").IsSelected(),
47+
).
48+
PressPrimaryAction()
49+
50+
t.Views().Information().Content(Contains("Building patch"))
51+
52+
t.Views().Secondary().Content(Contains("3\n+4"))
53+
54+
t.Common().SelectPatchOption(MatchesRegexp(`Apply patch$`))
55+
56+
t.ExpectPopup().Confirmation().Title(Equals("Must stage files")).
57+
Content(Contains("Applying a patch to the index requires staging the unstaged files that are affected by the patch.")).
58+
Confirm()
59+
60+
t.Views().Files().
61+
Focus().
62+
Lines(
63+
Equals("M file1").IsSelected(),
64+
)
65+
66+
t.Views().Main().
67+
Content(Contains("-1\n+11\n 2\n 3\n+4"))
68+
},
69+
})
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
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 MoveToIndexWithModifiedFile = NewIntegrationTest(NewIntegrationTestArgs{
9+
Description: "Move a patch from a commit to the index, with a modified file in the working tree that conflicts with the patch",
10+
ExtraCmdArgs: []string{},
11+
Skip: false,
12+
SetupConfig: func(config *config.AppConfig) {},
13+
SetupRepo: func(shell *Shell) {
14+
shell.CreateFileAndAdd("file1", "1\n2\n3\n4\n")
15+
shell.Commit("first commit")
16+
shell.UpdateFileAndAdd("file1", "11\n2\n3\n4\n")
17+
shell.Commit("second commit")
18+
shell.UpdateFile("file1", "111\n2\n3\n4\n")
19+
},
20+
Run: func(t *TestDriver, keys config.KeybindingConfig) {
21+
t.Views().Commits().
22+
Focus().
23+
Lines(
24+
Contains("second commit").IsSelected(),
25+
Contains("first commit"),
26+
).
27+
PressEnter()
28+
29+
t.Views().CommitFiles().
30+
IsFocused().
31+
Lines(
32+
Equals("M file1"),
33+
).
34+
PressPrimaryAction()
35+
36+
t.Views().Information().Content(Contains("Building patch"))
37+
38+
t.Views().Secondary().Content(Contains("-1\n+11"))
39+
40+
t.Common().SelectPatchOption(Contains("Move patch out into index"))
41+
42+
t.ExpectPopup().Confirmation().Title(Equals("Must stash")).
43+
Content(Contains("Pulling a patch out into the index requires stashing and unstashing your changes.")).
44+
Confirm()
45+
46+
t.Views().Files().
47+
Focus().
48+
Lines(
49+
Equals("MM file1"),
50+
)
51+
52+
t.Views().Main().
53+
Content(Contains("-11\n+111\n"))
54+
t.Views().Secondary().
55+
Content(Contains("-1\n+11\n"))
56+
57+
t.Views().Stash().IsEmpty()
58+
},
59+
})

pkg/integration/tests/test_list.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -300,6 +300,8 @@ var tests = []*components.IntegrationTest{
300300
patch_building.Apply,
301301
patch_building.ApplyInReverse,
302302
patch_building.ApplyInReverseWithConflict,
303+
patch_building.ApplyWithModifiedFileConflict,
304+
patch_building.ApplyWithModifiedFileNoConflict,
303305
patch_building.EditLineInPatchBuildingPanel,
304306
patch_building.MoveRangeToIndex,
305307
patch_building.MoveToEarlierCommit,
@@ -310,6 +312,7 @@ var tests = []*components.IntegrationTest{
310312
patch_building.MoveToIndexPartOfAdjacentAddedLines,
311313
patch_building.MoveToIndexPartial,
312314
patch_building.MoveToIndexWithConflict,
315+
patch_building.MoveToIndexWithModifiedFile,
313316
patch_building.MoveToIndexWorksEvenIfNoprefixIsSet,
314317
patch_building.MoveToLaterCommit,
315318
patch_building.MoveToLaterCommitPartialHunk,

0 commit comments

Comments
 (0)