Skip to content

Commit bb4d03d

Browse files
committed
Show todos (and conflicting commit) for cherry-pick and revert
1 parent 687bae4 commit bb4d03d

File tree

6 files changed

+426
-26
lines changed

6 files changed

+426
-26
lines changed

pkg/commands/git_commands/commit_loader.go

Lines changed: 112 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -169,19 +169,26 @@ func (self *CommitLoader) MergeRebasingCommits(commits []*models.Commit) ([]*mod
169169
}
170170
}
171171

172-
if !self.getWorkingTreeState().Rebasing {
173-
// not in rebase mode so return original commits
174-
return result, nil
172+
workingTreeState := self.getWorkingTreeState()
173+
addConflictedRebasingCommit := true
174+
if workingTreeState.CherryPicking || workingTreeState.Reverting {
175+
sequencerCommits, err := self.getHydratedSequencerCommits(workingTreeState)
176+
if err != nil {
177+
return nil, err
178+
}
179+
result = append(sequencerCommits, result...)
180+
addConflictedRebasingCommit = false
175181
}
176182

177-
rebasingCommits, err := self.getHydratedRebasingCommits()
178-
if err != nil {
179-
return nil, err
180-
}
181-
if len(rebasingCommits) > 0 {
182-
result = append(rebasingCommits, result...)
183+
if workingTreeState.Rebasing {
184+
rebasingCommits, err := self.getHydratedRebasingCommits(addConflictedRebasingCommit)
185+
if err != nil {
186+
return nil, err
187+
}
188+
if len(rebasingCommits) > 0 {
189+
result = append(rebasingCommits, result...)
190+
}
183191
}
184-
185192
return result, nil
186193
}
187194

@@ -240,14 +247,36 @@ func (self *CommitLoader) extractCommitFromLine(line string, showDivergence bool
240247
}
241248
}
242249

243-
func (self *CommitLoader) getHydratedRebasingCommits() ([]*models.Commit, error) {
244-
commits := self.getRebasingCommits()
250+
func (self *CommitLoader) getHydratedRebasingCommits(addConflictingCommit bool) ([]*models.Commit, error) {
251+
todoFileHasShortHashes := self.version.IsOlderThan(2, 25, 2)
252+
return self.getHydratedTodoCommits(self.getRebasingCommits(addConflictingCommit), todoFileHasShortHashes)
253+
}
245254

246-
if len(commits) == 0 {
255+
func (self *CommitLoader) getHydratedSequencerCommits(workingTreeState models.WorkingTreeState) ([]*models.Commit, error) {
256+
commits := self.getSequencerCommits()
257+
if len(commits) > 0 {
258+
// If we have any commits in .git/sequencer/todo, then the last one of
259+
// those is the conflicting one.
260+
commits[len(commits)-1].Status = models.StatusConflicted
261+
} else {
262+
// For single-commit cherry-picks and reverts, git apparently doesn't
263+
// use the sequencer; in that case, CHERRY_PICK_HEAD or REVERT_HEAD is
264+
// our conflicting commit, so synthesize it here.
265+
conflicedCommit := self.getConflictedSequencerCommit(workingTreeState)
266+
if conflicedCommit != nil {
267+
commits = append(commits, conflicedCommit)
268+
}
269+
}
270+
271+
return self.getHydratedTodoCommits(commits, true)
272+
}
273+
274+
func (self *CommitLoader) getHydratedTodoCommits(todoCommits []*models.Commit, todoFileHasShortHashes bool) ([]*models.Commit, error) {
275+
if len(todoCommits) == 0 {
247276
return nil, nil
248277
}
249278

250-
commitHashes := lo.FilterMap(commits, func(commit *models.Commit, _ int) (string, bool) {
279+
commitHashes := lo.FilterMap(todoCommits, func(commit *models.Commit, _ int) (string, bool) {
251280
return commit.Hash, commit.Hash != ""
252281
})
253282

@@ -271,7 +300,7 @@ func (self *CommitLoader) getHydratedRebasingCommits() ([]*models.Commit, error)
271300
return nil, err
272301
}
273302

274-
findFullCommit := lo.Ternary(self.version.IsOlderThan(2, 25, 2),
303+
findFullCommit := lo.Ternary(todoFileHasShortHashes,
275304
func(hash string) *models.Commit {
276305
for s, c := range fullCommits {
277306
if strings.HasPrefix(s, hash) {
@@ -284,8 +313,8 @@ func (self *CommitLoader) getHydratedRebasingCommits() ([]*models.Commit, error)
284313
return fullCommits[hash]
285314
})
286315

287-
hydratedCommits := make([]*models.Commit, 0, len(commits))
288-
for _, rebasingCommit := range commits {
316+
hydratedCommits := make([]*models.Commit, 0, len(todoCommits))
317+
for _, rebasingCommit := range todoCommits {
289318
if rebasingCommit.Hash == "" {
290319
hydratedCommits = append(hydratedCommits, rebasingCommit)
291320
} else if commit := findFullCommit(rebasingCommit.Hash); commit != nil {
@@ -302,7 +331,7 @@ func (self *CommitLoader) getHydratedRebasingCommits() ([]*models.Commit, error)
302331
// git-rebase-todo example:
303332
// pick ac446ae94ee560bdb8d1d057278657b251aaef17 ac446ae
304333
// pick afb893148791a2fbd8091aeb81deba4930c73031 afb8931
305-
func (self *CommitLoader) getRebasingCommits() []*models.Commit {
334+
func (self *CommitLoader) getRebasingCommits(addConflictingCommit bool) []*models.Commit {
306335
bytesContent, err := self.readFile(filepath.Join(self.repoPaths.WorktreeGitDirPath(), "rebase-merge/git-rebase-todo"))
307336
if err != nil {
308337
self.Log.Error(fmt.Sprintf("error occurred reading git-rebase-todo: %s", err.Error()))
@@ -320,8 +349,10 @@ func (self *CommitLoader) getRebasingCommits() []*models.Commit {
320349

321350
// See if the current commit couldn't be applied because it conflicted; if
322351
// so, add a fake entry for it
323-
if conflictedCommit := self.getConflictedCommit(todos); conflictedCommit != nil {
324-
commits = append(commits, conflictedCommit)
352+
if addConflictingCommit {
353+
if conflictedCommit := self.getConflictedCommit(todos); conflictedCommit != nil {
354+
commits = append(commits, conflictedCommit)
355+
}
325356
}
326357

327358
for _, t := range todos {
@@ -435,6 +466,67 @@ func (self *CommitLoader) getConflictedCommitImpl(todos []todo.Todo, doneTodos [
435466
}
436467
}
437468

469+
func (self *CommitLoader) getSequencerCommits() []*models.Commit {
470+
bytesContent, err := self.readFile(filepath.Join(self.repoPaths.WorktreeGitDirPath(), "sequencer/todo"))
471+
if err != nil {
472+
self.Log.Error(fmt.Sprintf("error occurred reading sequencer/todo: %s", err.Error()))
473+
// we assume an error means the file doesn't exist so we just return
474+
return nil
475+
}
476+
477+
commits := []*models.Commit{}
478+
479+
todos, err := todo.Parse(bytes.NewBuffer(bytesContent), self.config.GetCoreCommentChar())
480+
if err != nil {
481+
self.Log.Error(fmt.Sprintf("error occurred while parsing sequencer/todo file: %s", err.Error()))
482+
return nil
483+
}
484+
485+
for _, t := range todos {
486+
if t.Commit == "" {
487+
// Command does not have a commit associated, skip
488+
continue
489+
}
490+
commits = utils.Prepend(commits, &models.Commit{
491+
Hash: t.Commit,
492+
Name: t.Msg,
493+
Status: models.StatusRebasing,
494+
Action: t.Command,
495+
})
496+
}
497+
498+
return commits
499+
}
500+
501+
func (self *CommitLoader) getConflictedSequencerCommit(workingTreeState models.WorkingTreeState) *models.Commit {
502+
var shaFile string
503+
var action todo.TodoCommand
504+
if workingTreeState.CherryPicking {
505+
shaFile = "CHERRY_PICK_HEAD"
506+
action = todo.Pick
507+
} else if workingTreeState.Reverting {
508+
shaFile = "REVERT_HEAD"
509+
action = todo.Revert
510+
} else {
511+
return nil
512+
}
513+
bytesContent, err := self.readFile(filepath.Join(self.repoPaths.WorktreeGitDirPath(), shaFile))
514+
if err != nil {
515+
self.Log.Error(fmt.Sprintf("error occurred reading %s: %s", shaFile, err.Error()))
516+
// we assume an error means the file doesn't exist so we just return
517+
return nil
518+
}
519+
lines := strings.Split(string(bytesContent), "\n")
520+
if len(lines) == 0 {
521+
return nil
522+
}
523+
return &models.Commit{
524+
Hash: lines[0],
525+
Status: models.StatusConflicted,
526+
Action: action,
527+
}
528+
}
529+
438530
func setCommitMergedStatuses(ancestor string, commits []*models.Commit) {
439531
if ancestor == "" {
440532
return
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
package commit
2+
3+
import (
4+
"github.com/jesseduffield/lazygit/pkg/config"
5+
. "github.com/jesseduffield/lazygit/pkg/integration/components"
6+
)
7+
8+
var RevertWithConflictMultipleCommits = NewIntegrationTest(NewIntegrationTestArgs{
9+
Description: "Reverts a range of commits, the first of which conflicts",
10+
ExtraCmdArgs: []string{},
11+
Skip: false,
12+
SetupConfig: func(cfg *config.AppConfig) {
13+
// TODO: use our revert UI once we support range-select for reverts
14+
cfg.GetUserConfig().CustomCommands = []config.CustomCommand{
15+
{
16+
Key: "X",
17+
Context: "commits",
18+
Command: "git -c core.editor=: revert HEAD^ HEAD^^",
19+
},
20+
}
21+
},
22+
SetupRepo: func(shell *Shell) {
23+
shell.CreateFileAndAdd("myfile", "")
24+
shell.Commit("add empty file")
25+
shell.CreateFileAndAdd("otherfile", "")
26+
shell.Commit("unrelated change")
27+
shell.CreateFileAndAdd("myfile", "first line\n")
28+
shell.Commit("add first line")
29+
shell.UpdateFileAndAdd("myfile", "first line\nsecond line\n")
30+
shell.Commit("add second line")
31+
},
32+
Run: func(t *TestDriver, keys config.KeybindingConfig) {
33+
t.Views().Commits().
34+
Focus().
35+
Lines(
36+
Contains("CI ◯ add second line").IsSelected(),
37+
Contains("CI ◯ add first line"),
38+
Contains("CI ◯ unrelated change"),
39+
Contains("CI ◯ add empty file"),
40+
).
41+
Press("X").
42+
Tap(func() {
43+
t.ExpectPopup().Alert().
44+
Title(Equals("Error")).
45+
// The exact error message is different on different git versions,
46+
// but they all contain the word 'conflict' somewhere.
47+
Content(Contains("conflict")).
48+
Confirm()
49+
}).
50+
Lines(
51+
Contains("revert").Contains("CI unrelated change"),
52+
Contains("revert").Contains("CI <-- CONFLICT --- add first line"),
53+
Contains("CI ◯ add second line"),
54+
Contains("CI ◯ add first line"),
55+
Contains("CI ◯ unrelated change"),
56+
Contains("CI ◯ add empty file"),
57+
)
58+
59+
t.Views().Options().Content(Contains("View revert options: m"))
60+
t.Views().Information().Content(Contains("Reverting (Reset)"))
61+
62+
t.Views().Files().Focus().
63+
Lines(
64+
Contains("UU myfile").IsSelected(),
65+
).
66+
PressEnter()
67+
68+
t.Views().MergeConflicts().IsFocused().
69+
SelectNextItem().
70+
PressPrimaryAction()
71+
72+
t.ExpectPopup().Alert().
73+
Title(Equals("Continue")).
74+
Content(Contains("All merge conflicts resolved. Continue the revert?")).
75+
Confirm()
76+
77+
t.Views().Commits().
78+
Lines(
79+
Contains(`CI ◯ Revert "unrelated change"`),
80+
Contains(`CI ◯ Revert "add first line"`),
81+
Contains("CI ◯ add second line"),
82+
Contains("CI ◯ add first line"),
83+
Contains("CI ◯ unrelated change"),
84+
Contains("CI ◯ add empty file"),
85+
)
86+
},
87+
})

pkg/integration/tests/commit/revert_with_conflict_single_commit.go

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -39,16 +39,10 @@ var RevertWithConflictSingleCommit = NewIntegrationTest(NewIntegrationTestArgs{
3939
Confirm()
4040
}).
4141
Lines(
42-
/* EXPECTED:
43-
Proper display of revert commits is not implemented yet; we'll do this in the next PR
4442
Contains("revert").Contains("CI <-- CONFLICT --- add first line"),
4543
Contains("CI ◯ add second line"),
4644
Contains("CI ◯ add first line"),
4745
Contains("CI ◯ add empty file"),
48-
ACTUAL: */
49-
Contains("CI ◯ <-- YOU ARE HERE --- add second line"),
50-
Contains("CI ◯ add first line"),
51-
Contains("CI ◯ add empty file"),
5246
)
5347

5448
t.Views().Options().Content(Contains("View revert options: m"))

0 commit comments

Comments
 (0)