Skip to content

Commit 2677cfd

Browse files
committed
feat: enhance cherry-pick functionality with pre-paste head hash tracking and add integration tests for empty intermediate commits
1 parent f587a59 commit 2677cfd

File tree

5 files changed

+212
-3
lines changed

5 files changed

+212
-3
lines changed

pkg/gui/controllers/helpers/cherry_pick_helper.go

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@ package helpers
22

33
import (
44
"strconv"
5+
"strings"
56

7+
"github.com/jesseduffield/lazygit/pkg/commands/git_commands"
68
"github.com/jesseduffield/lazygit/pkg/commands/models"
79
"github.com/jesseduffield/lazygit/pkg/gui/modes/cherrypicking"
810
"github.com/jesseduffield/lazygit/pkg/gui/types"
@@ -19,6 +21,11 @@ type CherryPickHelper struct {
1921
postPasteShouldMarkDidPaste bool
2022

2123
postPasteSelection *postPasteSelection
24+
25+
prePasteHeadHash string
26+
pasteProducedCommits bool
27+
28+
deferPostPasteCleanup bool
2229
}
2330

2431
type postPasteSelection struct {
@@ -104,6 +111,7 @@ func (self *CherryPickHelper) Paste() error {
104111

105112
cherryPickedCommits := self.getData().CherryPickedCommits
106113
self.preparePostPasteSelection(cherryPickedCommits)
114+
self.capturePrePasteHeadHash()
107115

108116
self.setPostPasteCleanup(func(markDidPaste bool) error {
109117
if markDidPaste {
@@ -131,6 +139,8 @@ func (self *CherryPickHelper) Paste() error {
131139
return err
132140
}
133141

142+
self.markPasteProducedCommitsIfHeadChanged()
143+
134144
// If we're in the cherry-picking state at this point, it must
135145
// be because there were conflicts. Don't clear the copied
136146
// commits in this case, since we might want to abort and try
@@ -163,6 +173,9 @@ func (self *CherryPickHelper) Reset() error {
163173
self.postPasteCleanup = nil
164174
self.postPasteShouldMarkDidPaste = false
165175
self.postPasteSelection = nil
176+
self.prePasteHeadHash = ""
177+
self.pasteProducedCommits = false
178+
self.deferPostPasteCleanup = false
166179

167180
self.rerender()
168181
return nil
@@ -253,6 +266,9 @@ func (self *CherryPickHelper) runPostPasteCleanup(markDidPaste bool) error {
253266
defer func() {
254267
self.postPasteShouldMarkDidPaste = false
255268
self.postPasteSelection = nil
269+
self.prePasteHeadHash = ""
270+
self.pasteProducedCommits = false
271+
self.deferPostPasteCleanup = false
256272
}()
257273

258274
return cleanup(markDidPaste && self.postPasteShouldMarkDidPaste)
@@ -265,3 +281,80 @@ func (self *CherryPickHelper) setPostPasteShouldMarkDidPaste(mark bool) {
265281

266282
self.postPasteShouldMarkDidPaste = mark
267283
}
284+
285+
func (self *CherryPickHelper) capturePrePasteHeadHash() {
286+
headHash, err := self.getHeadHash()
287+
if err != nil {
288+
self.prePasteHeadHash = ""
289+
return
290+
}
291+
292+
self.prePasteHeadHash = headHash
293+
self.pasteProducedCommits = false
294+
}
295+
296+
func (self *CherryPickHelper) PasteProducedCommits() bool {
297+
if self.pasteProducedCommits {
298+
return true
299+
}
300+
301+
if self.prePasteHeadHash == "" {
302+
return true
303+
}
304+
305+
headHash, err := self.getHeadHash()
306+
if err != nil {
307+
return true
308+
}
309+
310+
if headHash != self.prePasteHeadHash {
311+
self.pasteProducedCommits = true
312+
return true
313+
}
314+
315+
return false
316+
}
317+
318+
func (self *CherryPickHelper) DeferPostPasteCleanup() {
319+
self.deferPostPasteCleanup = true
320+
}
321+
322+
func (self *CherryPickHelper) ClearDeferredPostPasteCleanup() {
323+
self.deferPostPasteCleanup = false
324+
}
325+
326+
func (self *CherryPickHelper) ShouldDeferPostPasteCleanup() bool {
327+
return self.deferPostPasteCleanup
328+
}
329+
330+
func (self *CherryPickHelper) markPasteProducedCommitsIfHeadChanged() {
331+
if self.pasteProducedCommits {
332+
return
333+
}
334+
335+
if self.prePasteHeadHash == "" {
336+
self.pasteProducedCommits = true
337+
return
338+
}
339+
340+
headHash, err := self.getHeadHash()
341+
if err != nil {
342+
self.pasteProducedCommits = true
343+
return
344+
}
345+
346+
if headHash != self.prePasteHeadHash {
347+
self.pasteProducedCommits = true
348+
}
349+
}
350+
351+
func (self *CherryPickHelper) getHeadHash() (string, error) {
352+
output, err := self.c.OS().Cmd.New(
353+
git_commands.NewGitCmd("rev-parse").Arg("HEAD").ToArgv(),
354+
).DontLog().RunWithOutput()
355+
if err != nil {
356+
return "", err
357+
}
358+
359+
return strings.TrimSpace(output), nil
360+
}

pkg/gui/controllers/helpers/merge_and_rebase_helper.go

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,10 @@ func (self *MergeAndRebaseHelper) genericMergeCommand(command string) error {
121121
return err
122122
}
123123

124+
if self.cherryPickHelper != nil {
125+
self.cherryPickHelper.markPasteProducedCommitsIfHeadChanged()
126+
}
127+
124128
if err := self.finalizeCherryPickIfDone(); err != nil {
125129
return err
126130
}
@@ -215,7 +219,7 @@ func (self *MergeAndRebaseHelper) handleEmptyCherryPick() error {
215219
OnPress: func() error {
216220
if self.cherryPickHelper != nil {
217221
self.cherryPickHelper.DisablePostPasteReselect()
218-
self.cherryPickHelper.setPostPasteShouldMarkDidPaste(false)
222+
self.cherryPickHelper.DeferPostPasteCleanup()
219223
}
220224
if err := self.genericMergeCommand(REBASE_OPTION_SKIP); err != nil {
221225
return err
@@ -266,6 +270,8 @@ func (self *MergeAndRebaseHelper) completeCherryPickAfterEmptyResolution(preserv
266270
return nil
267271
}
268272

273+
defer self.cherryPickHelper.ClearDeferredPostPasteCleanup()
274+
269275
if !preservePostPasteReselect {
270276
self.cherryPickHelper.DisablePostPasteReselect()
271277
}
@@ -274,7 +280,13 @@ func (self *MergeAndRebaseHelper) completeCherryPickAfterEmptyResolution(preserv
274280
return nil
275281
}
276282

277-
if err := self.cherryPickHelper.runPostPasteCleanup(preservePostPasteReselect); err != nil {
283+
self.cherryPickHelper.markPasteProducedCommitsIfHeadChanged()
284+
285+
if !self.cherryPickHelper.PasteProducedCommits() {
286+
self.cherryPickHelper.setPostPasteShouldMarkDidPaste(false)
287+
}
288+
289+
if err := self.cherryPickHelper.runPostPasteCleanup(true); err != nil {
278290
return err
279291
}
280292

@@ -286,6 +298,10 @@ func (self *MergeAndRebaseHelper) finalizeCherryPickIfDone() error {
286298
return nil
287299
}
288300

301+
if self.cherryPickHelper.ShouldDeferPostPasteCleanup() {
302+
return nil
303+
}
304+
289305
hasTodos, err := self.c.Git().Status.HasPendingSequencerTodos()
290306
if err != nil {
291307
return err
@@ -304,6 +320,8 @@ func (self *MergeAndRebaseHelper) finalizeCherryPickIfDone() error {
304320
return nil
305321
}
306322

323+
self.cherryPickHelper.markPasteProducedCommitsIfHeadChanged()
324+
307325
if !self.cherryPickHelper.ShouldRestorePostPasteSelection() {
308326
self.cherryPickHelper.DisablePostPasteReselect()
309327
}

pkg/integration/tests/cherry_pick/cherry_pick_conflicts.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,7 @@ var CherryPickConflicts = NewIntegrationTest(NewIntegrationTestArgs{
100100
Content(Contains("-First Change")).
101101
Content(Contains("+Second Change"))
102102

103-
t.Views().Information().Content(Contains("2 commits copied"))
103+
t.Views().Information().Content(DoesNotContain("commits copied"))
104104
}).
105105
PressEscape().
106106
Tap(func() {
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
package cherry_pick
2+
3+
import (
4+
"github.com/jesseduffield/lazygit/pkg/config"
5+
. "github.com/jesseduffield/lazygit/pkg/integration/components"
6+
)
7+
8+
var CherryPickRangeEmptyIntermediateSkip = NewIntegrationTest(NewIntegrationTestArgs{
9+
Description: "Cherry-picking a range with an intermediate empty commit and skipping it",
10+
ExtraCmdArgs: []string{},
11+
Skip: false,
12+
SetupConfig: func(config *config.AppConfig) {
13+
config.GetUserConfig().Git.LocalBranchSortOrder = "recency"
14+
},
15+
SetupRepo: func(shell *Shell) {
16+
shell.
17+
CreateFileAndAdd("shared.txt", "original\n").Commit("add shared file on master").
18+
NewBranch("target").
19+
UpdateFileAndAdd("shared.txt", "target change\n").Commit("update shared file on target").
20+
Checkout("master").
21+
NewBranch("source").
22+
CreateFileAndAdd("unique1.txt", "content1\n").Commit("add unique1 on source").
23+
UpdateFileAndAdd("shared.txt", "target change\n").Commit("match target change").
24+
CreateFileAndAdd("unique2.txt", "content2\n").Commit("add unique2 on source").
25+
Checkout("target")
26+
},
27+
Run: func(t *TestDriver, keys config.KeybindingConfig) {
28+
t.Views().Branches().
29+
Focus().
30+
Lines(
31+
Contains("target"),
32+
Contains("source"),
33+
Contains("master"),
34+
).
35+
SelectNextItem().
36+
PressEnter()
37+
38+
t.Views().SubCommits().
39+
IsFocused().
40+
Lines(
41+
Contains("add unique2 on source").IsSelected(),
42+
Contains("match target change"),
43+
Contains("add unique1 on source"),
44+
Contains("add shared file on master"),
45+
).
46+
Press(keys.Universal.RangeSelectDown).
47+
Lines(
48+
Contains("add unique2 on source").IsSelected(),
49+
Contains("match target change").IsSelected(),
50+
Contains("add unique1 on source"),
51+
Contains("add shared file on master"),
52+
).
53+
Press(keys.Universal.RangeSelectDown).
54+
Lines(
55+
Contains("add unique2 on source").IsSelected(),
56+
Contains("match target change").IsSelected(),
57+
Contains("add unique1 on source").IsSelected(),
58+
Contains("add shared file on master"),
59+
).
60+
Press(keys.Commits.CherryPickCopy)
61+
62+
t.Views().Information().Content(Contains("3 commits copied"))
63+
64+
t.Views().Commits().
65+
Focus().
66+
Lines(
67+
Contains("update shared file on target").IsSelected(),
68+
Contains("add shared file on master"),
69+
).
70+
Press(keys.Commits.PasteCommits).
71+
Tap(func() {
72+
t.ExpectPopup().Alert().
73+
Title(Equals("Cherry-pick")).
74+
Content(Contains("Are you sure you want to cherry-pick the 3 copied commit(s) onto this branch?")).
75+
Confirm()
76+
}).
77+
Tap(func() {
78+
t.ExpectPopup().Menu().
79+
Title(Equals("Cherry-pick produced no changes")).
80+
ContainsLines(
81+
Contains("Skip this cherry-pick"),
82+
Contains("Create empty commit and continue"),
83+
Contains("Cancel"),
84+
).
85+
Select(Contains("Skip this cherry-pick")).
86+
Confirm()
87+
}).
88+
Tap(func() {
89+
t.Views().Information().Content(DoesNotContain("commits copied"))
90+
}).
91+
TopLines(
92+
Contains("add unique2 on source").IsSelected(),
93+
Contains("add unique1 on source"),
94+
Contains("update shared file on target"),
95+
)
96+
},
97+
})

pkg/integration/tests/test_list.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,7 @@ var tests = []*components.IntegrationTest{
9595
cherry_pick.CherryPickMerge,
9696
cherry_pick.CherryPickRange,
9797
cherry_pick.CherryPickRangeEmptyIntermediate,
98+
cherry_pick.CherryPickRangeEmptyIntermediateSkip,
9899
commit.AddCoAuthor,
99100
commit.AddCoAuthorRange,
100101
commit.AddCoAuthorWhileCommitting,

0 commit comments

Comments
 (0)