Skip to content

Commit 3df894e

Browse files
committed
Stage affected unstaged files when applying or reverting a patch
Unlike moving a patch to the index, applying or reverting a patch didn't auto-stash, which means that applying a patch when there's a modified (but unstaged) file in the working tree would error out with the message "error: file1: does not match index", regardless of whether those modifications conflict with the patch or not. To fix this, we *could* add auto-stashing like we do for the "move patch to index" command. However, in this case we rather simply stage the affected files (after asking for confirmation). This has a few advantages: - it only changes the staging state of those files that are contained in the patch (whereas auto-stashing always changes all files to unstaged) - it doesn't unnecessarily show a confirmation if none of the modified files are affected by the patch - if the patch conflicts with the modified files, the conflicts were "backwards" ("ours" was the patch, "theirs" the modified file); it is more logical if "ours" is the current state of the file, and "theirs" is the patch. It's a little unfortunate that the behavior isn't exactly the same as for "move patch to index", but for that one we do need the auto-stash because of the rebase that runs behind the scenes.
1 parent c440a20 commit 3df894e

File tree

4 files changed

+90
-11
lines changed

4 files changed

+90
-11
lines changed

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",

pkg/integration/tests/patch_building/apply_with_modified_file_conflict.go

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,8 +53,31 @@ var ApplyWithModifiedFileConflict = NewIntegrationTest(NewIntegrationTestArgs{
5353

5454
t.Common().SelectPatchOption(MatchesRegexp(`Apply patch$`))
5555

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+
5660
t.ExpectPopup().Alert().Title(Equals("Error")).
57-
Content(Equals("error: file1: does not match index")).
61+
Content(Contains("Applied patch to 'file1' with conflicts.")).
5862
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+
)
5982
},
6083
})

pkg/integration/tests/patch_building/apply_with_modified_file_no_conflict.go

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -53,8 +53,17 @@ var ApplyWithModifiedFileNoConflict = NewIntegrationTest(NewIntegrationTestArgs{
5353

5454
t.Common().SelectPatchOption(MatchesRegexp(`Apply patch$`))
5555

56-
t.ExpectPopup().Alert().Title(Equals("Error")).
57-
Content(Equals("error: file1: does not match index")).
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.")).
5858
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"))
5968
},
6069
})

0 commit comments

Comments
 (0)