Skip to content

Commit b7d01d6

Browse files
authored
Show todo items for pending cherry-picks and reverts (#4442)
- **PR Description** This is part two of a four part series of PRs that improve the cherry-pick and revert experience. With this PR we include pending cherry-picks and reverts in the commit list (like rebase todos) when a cherry-pick or revert stops with conflicts; also, we show the conflicting item in the list like we do with conflicting rebase todos. As with the previous PR, this is not really very useful yet because you can't revert a range of commits, and we don't use git cherry-pick for copy/paste. Both of these will change in later PRs in this series, so again this is preparation for that.
2 parents 3dc045b + b350876 commit b7d01d6

27 files changed

+684
-106
lines changed

pkg/commands/git_commands/commit_loader.go

Lines changed: 140 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -71,15 +71,13 @@ type GetCommitsOptions struct {
7171
// GetCommits obtains the commits of the current branch
7272
func (self *CommitLoader) GetCommits(opts GetCommitsOptions) ([]*models.Commit, error) {
7373
commits := []*models.Commit{}
74-
var rebasingCommits []*models.Commit
7574

7675
if opts.IncludeRebaseCommits && opts.FilterPath == "" {
7776
var err error
78-
rebasingCommits, err = self.MergeRebasingCommits(commits)
77+
commits, err = self.MergeRebasingCommits(commits)
7978
if err != nil {
8079
return nil, err
8180
}
82-
commits = append(commits, rebasingCommits...)
8381
}
8482

8583
wg := sync.WaitGroup{}
@@ -126,7 +124,7 @@ func (self *CommitLoader) GetCommits(opts GetCommitsOptions) ([]*models.Commit,
126124
if commit.Hash == firstPushedCommit {
127125
passedFirstPushedCommit = true
128126
}
129-
if commit.Status != models.StatusRebasing {
127+
if !commit.IsTODO() {
130128
if passedFirstPushedCommit {
131129
commit.Status = models.StatusPushed
132130
} else {
@@ -171,19 +169,26 @@ func (self *CommitLoader) MergeRebasingCommits(commits []*models.Commit) ([]*mod
171169
}
172170
}
173171

174-
if !self.getWorkingTreeState().Rebasing {
175-
// not in rebase mode so return original commits
176-
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
177181
}
178182

179-
rebasingCommits, err := self.getHydratedRebasingCommits()
180-
if err != nil {
181-
return nil, err
182-
}
183-
if len(rebasingCommits) > 0 {
184-
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+
}
185191
}
186-
187192
return result, nil
188193
}
189194

@@ -242,14 +247,36 @@ func (self *CommitLoader) extractCommitFromLine(line string, showDivergence bool
242247
}
243248
}
244249

245-
func (self *CommitLoader) getHydratedRebasingCommits() ([]*models.Commit, error) {
246-
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+
}
247254

248-
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 {
249276
return nil, nil
250277
}
251278

252-
commitHashes := lo.FilterMap(commits, func(commit *models.Commit, _ int) (string, bool) {
279+
commitHashes := lo.FilterMap(todoCommits, func(commit *models.Commit, _ int) (string, bool) {
253280
return commit.Hash, commit.Hash != ""
254281
})
255282

@@ -273,7 +300,7 @@ func (self *CommitLoader) getHydratedRebasingCommits() ([]*models.Commit, error)
273300
return nil, err
274301
}
275302

276-
findFullCommit := lo.Ternary(self.version.IsOlderThan(2, 25, 2),
303+
findFullCommit := lo.Ternary(todoFileHasShortHashes,
277304
func(hash string) *models.Commit {
278305
for s, c := range fullCommits {
279306
if strings.HasPrefix(s, hash) {
@@ -286,8 +313,8 @@ func (self *CommitLoader) getHydratedRebasingCommits() ([]*models.Commit, error)
286313
return fullCommits[hash]
287314
})
288315

289-
hydratedCommits := make([]*models.Commit, 0, len(commits))
290-
for _, rebasingCommit := range commits {
316+
hydratedCommits := make([]*models.Commit, 0, len(todoCommits))
317+
for _, rebasingCommit := range todoCommits {
291318
if rebasingCommit.Hash == "" {
292319
hydratedCommits = append(hydratedCommits, rebasingCommit)
293320
} else if commit := findFullCommit(rebasingCommit.Hash); commit != nil {
@@ -304,7 +331,7 @@ func (self *CommitLoader) getHydratedRebasingCommits() ([]*models.Commit, error)
304331
// git-rebase-todo example:
305332
// pick ac446ae94ee560bdb8d1d057278657b251aaef17 ac446ae
306333
// pick afb893148791a2fbd8091aeb81deba4930c73031 afb8931
307-
func (self *CommitLoader) getRebasingCommits() []*models.Commit {
334+
func (self *CommitLoader) getRebasingCommits(addConflictingCommit bool) []*models.Commit {
308335
bytesContent, err := self.readFile(filepath.Join(self.repoPaths.WorktreeGitDirPath(), "rebase-merge/git-rebase-todo"))
309336
if err != nil {
310337
self.Log.Error(fmt.Sprintf("error occurred reading git-rebase-todo: %s", err.Error()))
@@ -322,13 +349,10 @@ func (self *CommitLoader) getRebasingCommits() []*models.Commit {
322349

323350
// See if the current commit couldn't be applied because it conflicted; if
324351
// so, add a fake entry for it
325-
if conflictedCommitHash := self.getConflictedCommit(todos); conflictedCommitHash != "" {
326-
commits = append(commits, &models.Commit{
327-
Hash: conflictedCommitHash,
328-
Name: "",
329-
Status: models.StatusRebasing,
330-
Action: models.ActionConflict,
331-
})
352+
if addConflictingCommit {
353+
if conflictedCommit := self.getConflictedCommit(todos); conflictedCommit != nil {
354+
commits = append(commits, conflictedCommit)
355+
}
332356
}
333357

334358
for _, t := range todos {
@@ -351,36 +375,34 @@ func (self *CommitLoader) getRebasingCommits() []*models.Commit {
351375
return commits
352376
}
353377

354-
func (self *CommitLoader) getConflictedCommit(todos []todo.Todo) string {
378+
func (self *CommitLoader) getConflictedCommit(todos []todo.Todo) *models.Commit {
355379
bytesContent, err := self.readFile(filepath.Join(self.repoPaths.WorktreeGitDirPath(), "rebase-merge/done"))
356380
if err != nil {
357381
self.Log.Error(fmt.Sprintf("error occurred reading rebase-merge/done: %s", err.Error()))
358-
return ""
382+
return nil
359383
}
360384

361385
doneTodos, err := todo.Parse(bytes.NewBuffer(bytesContent), self.config.GetCoreCommentChar())
362386
if err != nil {
363387
self.Log.Error(fmt.Sprintf("error occurred while parsing rebase-merge/done file: %s", err.Error()))
364-
return ""
388+
return nil
365389
}
366390

367-
amendFileExists := false
368-
if _, err := os.Stat(filepath.Join(self.repoPaths.WorktreeGitDirPath(), "rebase-merge/amend")); err == nil {
369-
amendFileExists = true
370-
}
391+
amendFileExists, _ := self.os.FileExists(filepath.Join(self.repoPaths.WorktreeGitDirPath(), "rebase-merge/amend"))
392+
messageFileExists, _ := self.os.FileExists(filepath.Join(self.repoPaths.WorktreeGitDirPath(), "rebase-merge/message"))
371393

372-
return self.getConflictedCommitImpl(todos, doneTodos, amendFileExists)
394+
return self.getConflictedCommitImpl(todos, doneTodos, amendFileExists, messageFileExists)
373395
}
374396

375-
func (self *CommitLoader) getConflictedCommitImpl(todos []todo.Todo, doneTodos []todo.Todo, amendFileExists bool) string {
397+
func (self *CommitLoader) getConflictedCommitImpl(todos []todo.Todo, doneTodos []todo.Todo, amendFileExists bool, messageFileExists bool) *models.Commit {
376398
// Should never be possible, but just to be safe:
377399
if len(doneTodos) == 0 {
378400
self.Log.Error("no done entries in rebase-merge/done file")
379-
return ""
401+
return nil
380402
}
381403
lastTodo := doneTodos[len(doneTodos)-1]
382404
if lastTodo.Command == todo.Break || lastTodo.Command == todo.Exec || lastTodo.Command == todo.Reword {
383-
return ""
405+
return nil
384406
}
385407

386408
// In certain cases, git reschedules commands that failed. One example is if
@@ -391,7 +413,7 @@ func (self *CommitLoader) getConflictedCommitImpl(todos []todo.Todo, doneTodos [
391413
// same, the command was rescheduled.
392414
if len(doneTodos) > 0 && len(todos) > 0 && doneTodos[len(doneTodos)-1] == todos[0] {
393415
// Command was rescheduled, no need to display it
394-
return ""
416+
return nil
395417
}
396418

397419
// Older versions of git have a bug whereby, if a command is rescheduled,
@@ -416,26 +438,99 @@ func (self *CommitLoader) getConflictedCommitImpl(todos []todo.Todo, doneTodos [
416438
if len(doneTodos) >= 3 && len(todos) > 0 && doneTodos[len(doneTodos)-2] == todos[0] &&
417439
doneTodos[len(doneTodos)-1] == doneTodos[len(doneTodos)-3] {
418440
// Command was rescheduled, no need to display it
419-
return ""
441+
return nil
420442
}
421443

422444
if lastTodo.Command == todo.Edit {
423445
if amendFileExists {
424446
// Special case for "edit": if the "amend" file exists, the "edit"
425447
// command was successful, otherwise it wasn't
426-
return ""
448+
return nil
449+
}
450+
451+
if !messageFileExists {
452+
// As an additional check, see if the "message" file exists; if it
453+
// doesn't, it must be because a multi-commit cherry-pick or revert
454+
// was performed in the meantime, which deleted both the amend file
455+
// and the message file.
456+
return nil
427457
}
428458
}
429459

430460
// I don't think this is ever possible, but again, just to be safe:
431461
if lastTodo.Commit == "" {
432462
self.Log.Error("last command in rebase-merge/done file doesn't have a commit")
433-
return ""
463+
return nil
434464
}
435465

436466
// Any other todo that has a commit associated with it must have failed with
437467
// a conflict, otherwise we wouldn't have stopped the rebase:
438-
return lastTodo.Commit
468+
return &models.Commit{
469+
Hash: lastTodo.Commit,
470+
Action: lastTodo.Command,
471+
Status: models.StatusConflicted,
472+
}
473+
}
474+
475+
func (self *CommitLoader) getSequencerCommits() []*models.Commit {
476+
bytesContent, err := self.readFile(filepath.Join(self.repoPaths.WorktreeGitDirPath(), "sequencer/todo"))
477+
if err != nil {
478+
self.Log.Error(fmt.Sprintf("error occurred reading sequencer/todo: %s", err.Error()))
479+
// we assume an error means the file doesn't exist so we just return
480+
return nil
481+
}
482+
483+
commits := []*models.Commit{}
484+
485+
todos, err := todo.Parse(bytes.NewBuffer(bytesContent), self.config.GetCoreCommentChar())
486+
if err != nil {
487+
self.Log.Error(fmt.Sprintf("error occurred while parsing sequencer/todo file: %s", err.Error()))
488+
return nil
489+
}
490+
491+
for _, t := range todos {
492+
if t.Commit == "" {
493+
// Command does not have a commit associated, skip
494+
continue
495+
}
496+
commits = utils.Prepend(commits, &models.Commit{
497+
Hash: t.Commit,
498+
Name: t.Msg,
499+
Status: models.StatusRebasing,
500+
Action: t.Command,
501+
})
502+
}
503+
504+
return commits
505+
}
506+
507+
func (self *CommitLoader) getConflictedSequencerCommit(workingTreeState models.WorkingTreeState) *models.Commit {
508+
var shaFile string
509+
var action todo.TodoCommand
510+
if workingTreeState.CherryPicking {
511+
shaFile = "CHERRY_PICK_HEAD"
512+
action = todo.Pick
513+
} else if workingTreeState.Reverting {
514+
shaFile = "REVERT_HEAD"
515+
action = todo.Revert
516+
} else {
517+
return nil
518+
}
519+
bytesContent, err := self.readFile(filepath.Join(self.repoPaths.WorktreeGitDirPath(), shaFile))
520+
if err != nil {
521+
self.Log.Error(fmt.Sprintf("error occurred reading %s: %s", shaFile, err.Error()))
522+
// we assume an error means the file doesn't exist so we just return
523+
return nil
524+
}
525+
lines := strings.Split(string(bytesContent), "\n")
526+
if len(lines) == 0 {
527+
return nil
528+
}
529+
return &models.Commit{
530+
Hash: lines[0],
531+
Status: models.StatusConflicted,
532+
Action: action,
533+
}
439534
}
440535

441536
func setCommitMergedStatuses(ancestor string, commits []*models.Commit) {

0 commit comments

Comments
 (0)