1
0
mirror of https://github.com/jesseduffield/lazygit.git synced 2025-06-06 23:46:13 +02:00

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.
This commit is contained in:
Stefan Haller 2025-04-20 15:58:54 +02:00 committed by GitHub
commit b7d01d67a6
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
27 changed files with 684 additions and 106 deletions

View File

@ -71,15 +71,13 @@ type GetCommitsOptions struct {
// GetCommits obtains the commits of the current branch // GetCommits obtains the commits of the current branch
func (self *CommitLoader) GetCommits(opts GetCommitsOptions) ([]*models.Commit, error) { func (self *CommitLoader) GetCommits(opts GetCommitsOptions) ([]*models.Commit, error) {
commits := []*models.Commit{} commits := []*models.Commit{}
var rebasingCommits []*models.Commit
if opts.IncludeRebaseCommits && opts.FilterPath == "" { if opts.IncludeRebaseCommits && opts.FilterPath == "" {
var err error var err error
rebasingCommits, err = self.MergeRebasingCommits(commits) commits, err = self.MergeRebasingCommits(commits)
if err != nil { if err != nil {
return nil, err return nil, err
} }
commits = append(commits, rebasingCommits...)
} }
wg := sync.WaitGroup{} wg := sync.WaitGroup{}
@ -126,7 +124,7 @@ func (self *CommitLoader) GetCommits(opts GetCommitsOptions) ([]*models.Commit,
if commit.Hash == firstPushedCommit { if commit.Hash == firstPushedCommit {
passedFirstPushedCommit = true passedFirstPushedCommit = true
} }
if commit.Status != models.StatusRebasing { if !commit.IsTODO() {
if passedFirstPushedCommit { if passedFirstPushedCommit {
commit.Status = models.StatusPushed commit.Status = models.StatusPushed
} else { } else {
@ -171,19 +169,26 @@ func (self *CommitLoader) MergeRebasingCommits(commits []*models.Commit) ([]*mod
} }
} }
if !self.getWorkingTreeState().Rebasing { workingTreeState := self.getWorkingTreeState()
// not in rebase mode so return original commits addConflictedRebasingCommit := true
return result, nil if workingTreeState.CherryPicking || workingTreeState.Reverting {
sequencerCommits, err := self.getHydratedSequencerCommits(workingTreeState)
if err != nil {
return nil, err
}
result = append(sequencerCommits, result...)
addConflictedRebasingCommit = false
} }
rebasingCommits, err := self.getHydratedRebasingCommits() if workingTreeState.Rebasing {
if err != nil { rebasingCommits, err := self.getHydratedRebasingCommits(addConflictedRebasingCommit)
return nil, err if err != nil {
return nil, err
}
if len(rebasingCommits) > 0 {
result = append(rebasingCommits, result...)
}
} }
if len(rebasingCommits) > 0 {
result = append(rebasingCommits, result...)
}
return result, nil return result, nil
} }
@ -242,14 +247,36 @@ func (self *CommitLoader) extractCommitFromLine(line string, showDivergence bool
} }
} }
func (self *CommitLoader) getHydratedRebasingCommits() ([]*models.Commit, error) { func (self *CommitLoader) getHydratedRebasingCommits(addConflictingCommit bool) ([]*models.Commit, error) {
commits := self.getRebasingCommits() todoFileHasShortHashes := self.version.IsOlderThan(2, 25, 2)
return self.getHydratedTodoCommits(self.getRebasingCommits(addConflictingCommit), todoFileHasShortHashes)
}
if len(commits) == 0 { func (self *CommitLoader) getHydratedSequencerCommits(workingTreeState models.WorkingTreeState) ([]*models.Commit, error) {
commits := self.getSequencerCommits()
if len(commits) > 0 {
// If we have any commits in .git/sequencer/todo, then the last one of
// those is the conflicting one.
commits[len(commits)-1].Status = models.StatusConflicted
} else {
// For single-commit cherry-picks and reverts, git apparently doesn't
// use the sequencer; in that case, CHERRY_PICK_HEAD or REVERT_HEAD is
// our conflicting commit, so synthesize it here.
conflicedCommit := self.getConflictedSequencerCommit(workingTreeState)
if conflicedCommit != nil {
commits = append(commits, conflicedCommit)
}
}
return self.getHydratedTodoCommits(commits, true)
}
func (self *CommitLoader) getHydratedTodoCommits(todoCommits []*models.Commit, todoFileHasShortHashes bool) ([]*models.Commit, error) {
if len(todoCommits) == 0 {
return nil, nil return nil, nil
} }
commitHashes := lo.FilterMap(commits, func(commit *models.Commit, _ int) (string, bool) { commitHashes := lo.FilterMap(todoCommits, func(commit *models.Commit, _ int) (string, bool) {
return commit.Hash, commit.Hash != "" return commit.Hash, commit.Hash != ""
}) })
@ -273,7 +300,7 @@ func (self *CommitLoader) getHydratedRebasingCommits() ([]*models.Commit, error)
return nil, err return nil, err
} }
findFullCommit := lo.Ternary(self.version.IsOlderThan(2, 25, 2), findFullCommit := lo.Ternary(todoFileHasShortHashes,
func(hash string) *models.Commit { func(hash string) *models.Commit {
for s, c := range fullCommits { for s, c := range fullCommits {
if strings.HasPrefix(s, hash) { if strings.HasPrefix(s, hash) {
@ -286,8 +313,8 @@ func (self *CommitLoader) getHydratedRebasingCommits() ([]*models.Commit, error)
return fullCommits[hash] return fullCommits[hash]
}) })
hydratedCommits := make([]*models.Commit, 0, len(commits)) hydratedCommits := make([]*models.Commit, 0, len(todoCommits))
for _, rebasingCommit := range commits { for _, rebasingCommit := range todoCommits {
if rebasingCommit.Hash == "" { if rebasingCommit.Hash == "" {
hydratedCommits = append(hydratedCommits, rebasingCommit) hydratedCommits = append(hydratedCommits, rebasingCommit)
} else if commit := findFullCommit(rebasingCommit.Hash); commit != nil { } else if commit := findFullCommit(rebasingCommit.Hash); commit != nil {
@ -304,7 +331,7 @@ func (self *CommitLoader) getHydratedRebasingCommits() ([]*models.Commit, error)
// git-rebase-todo example: // git-rebase-todo example:
// pick ac446ae94ee560bdb8d1d057278657b251aaef17 ac446ae // pick ac446ae94ee560bdb8d1d057278657b251aaef17 ac446ae
// pick afb893148791a2fbd8091aeb81deba4930c73031 afb8931 // pick afb893148791a2fbd8091aeb81deba4930c73031 afb8931
func (self *CommitLoader) getRebasingCommits() []*models.Commit { func (self *CommitLoader) getRebasingCommits(addConflictingCommit bool) []*models.Commit {
bytesContent, err := self.readFile(filepath.Join(self.repoPaths.WorktreeGitDirPath(), "rebase-merge/git-rebase-todo")) bytesContent, err := self.readFile(filepath.Join(self.repoPaths.WorktreeGitDirPath(), "rebase-merge/git-rebase-todo"))
if err != nil { if err != nil {
self.Log.Error(fmt.Sprintf("error occurred reading git-rebase-todo: %s", err.Error())) self.Log.Error(fmt.Sprintf("error occurred reading git-rebase-todo: %s", err.Error()))
@ -322,13 +349,10 @@ func (self *CommitLoader) getRebasingCommits() []*models.Commit {
// See if the current commit couldn't be applied because it conflicted; if // See if the current commit couldn't be applied because it conflicted; if
// so, add a fake entry for it // so, add a fake entry for it
if conflictedCommitHash := self.getConflictedCommit(todos); conflictedCommitHash != "" { if addConflictingCommit {
commits = append(commits, &models.Commit{ if conflictedCommit := self.getConflictedCommit(todos); conflictedCommit != nil {
Hash: conflictedCommitHash, commits = append(commits, conflictedCommit)
Name: "", }
Status: models.StatusRebasing,
Action: models.ActionConflict,
})
} }
for _, t := range todos { for _, t := range todos {
@ -351,36 +375,34 @@ func (self *CommitLoader) getRebasingCommits() []*models.Commit {
return commits return commits
} }
func (self *CommitLoader) getConflictedCommit(todos []todo.Todo) string { func (self *CommitLoader) getConflictedCommit(todos []todo.Todo) *models.Commit {
bytesContent, err := self.readFile(filepath.Join(self.repoPaths.WorktreeGitDirPath(), "rebase-merge/done")) bytesContent, err := self.readFile(filepath.Join(self.repoPaths.WorktreeGitDirPath(), "rebase-merge/done"))
if err != nil { if err != nil {
self.Log.Error(fmt.Sprintf("error occurred reading rebase-merge/done: %s", err.Error())) self.Log.Error(fmt.Sprintf("error occurred reading rebase-merge/done: %s", err.Error()))
return "" return nil
} }
doneTodos, err := todo.Parse(bytes.NewBuffer(bytesContent), self.config.GetCoreCommentChar()) doneTodos, err := todo.Parse(bytes.NewBuffer(bytesContent), self.config.GetCoreCommentChar())
if err != nil { if err != nil {
self.Log.Error(fmt.Sprintf("error occurred while parsing rebase-merge/done file: %s", err.Error())) self.Log.Error(fmt.Sprintf("error occurred while parsing rebase-merge/done file: %s", err.Error()))
return "" return nil
} }
amendFileExists := false amendFileExists, _ := self.os.FileExists(filepath.Join(self.repoPaths.WorktreeGitDirPath(), "rebase-merge/amend"))
if _, err := os.Stat(filepath.Join(self.repoPaths.WorktreeGitDirPath(), "rebase-merge/amend")); err == nil { messageFileExists, _ := self.os.FileExists(filepath.Join(self.repoPaths.WorktreeGitDirPath(), "rebase-merge/message"))
amendFileExists = true
}
return self.getConflictedCommitImpl(todos, doneTodos, amendFileExists) return self.getConflictedCommitImpl(todos, doneTodos, amendFileExists, messageFileExists)
} }
func (self *CommitLoader) getConflictedCommitImpl(todos []todo.Todo, doneTodos []todo.Todo, amendFileExists bool) string { func (self *CommitLoader) getConflictedCommitImpl(todos []todo.Todo, doneTodos []todo.Todo, amendFileExists bool, messageFileExists bool) *models.Commit {
// Should never be possible, but just to be safe: // Should never be possible, but just to be safe:
if len(doneTodos) == 0 { if len(doneTodos) == 0 {
self.Log.Error("no done entries in rebase-merge/done file") self.Log.Error("no done entries in rebase-merge/done file")
return "" return nil
} }
lastTodo := doneTodos[len(doneTodos)-1] lastTodo := doneTodos[len(doneTodos)-1]
if lastTodo.Command == todo.Break || lastTodo.Command == todo.Exec || lastTodo.Command == todo.Reword { if lastTodo.Command == todo.Break || lastTodo.Command == todo.Exec || lastTodo.Command == todo.Reword {
return "" return nil
} }
// In certain cases, git reschedules commands that failed. One example is if // In certain cases, git reschedules commands that failed. One example is if
@ -391,7 +413,7 @@ func (self *CommitLoader) getConflictedCommitImpl(todos []todo.Todo, doneTodos [
// same, the command was rescheduled. // same, the command was rescheduled.
if len(doneTodos) > 0 && len(todos) > 0 && doneTodos[len(doneTodos)-1] == todos[0] { if len(doneTodos) > 0 && len(todos) > 0 && doneTodos[len(doneTodos)-1] == todos[0] {
// Command was rescheduled, no need to display it // Command was rescheduled, no need to display it
return "" return nil
} }
// Older versions of git have a bug whereby, if a command is rescheduled, // 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 [
if len(doneTodos) >= 3 && len(todos) > 0 && doneTodos[len(doneTodos)-2] == todos[0] && if len(doneTodos) >= 3 && len(todos) > 0 && doneTodos[len(doneTodos)-2] == todos[0] &&
doneTodos[len(doneTodos)-1] == doneTodos[len(doneTodos)-3] { doneTodos[len(doneTodos)-1] == doneTodos[len(doneTodos)-3] {
// Command was rescheduled, no need to display it // Command was rescheduled, no need to display it
return "" return nil
} }
if lastTodo.Command == todo.Edit { if lastTodo.Command == todo.Edit {
if amendFileExists { if amendFileExists {
// Special case for "edit": if the "amend" file exists, the "edit" // Special case for "edit": if the "amend" file exists, the "edit"
// command was successful, otherwise it wasn't // command was successful, otherwise it wasn't
return "" return nil
}
if !messageFileExists {
// As an additional check, see if the "message" file exists; if it
// doesn't, it must be because a multi-commit cherry-pick or revert
// was performed in the meantime, which deleted both the amend file
// and the message file.
return nil
} }
} }
// I don't think this is ever possible, but again, just to be safe: // I don't think this is ever possible, but again, just to be safe:
if lastTodo.Commit == "" { if lastTodo.Commit == "" {
self.Log.Error("last command in rebase-merge/done file doesn't have a commit") self.Log.Error("last command in rebase-merge/done file doesn't have a commit")
return "" return nil
} }
// Any other todo that has a commit associated with it must have failed with // Any other todo that has a commit associated with it must have failed with
// a conflict, otherwise we wouldn't have stopped the rebase: // a conflict, otherwise we wouldn't have stopped the rebase:
return lastTodo.Commit return &models.Commit{
Hash: lastTodo.Commit,
Action: lastTodo.Command,
Status: models.StatusConflicted,
}
}
func (self *CommitLoader) getSequencerCommits() []*models.Commit {
bytesContent, err := self.readFile(filepath.Join(self.repoPaths.WorktreeGitDirPath(), "sequencer/todo"))
if err != nil {
self.Log.Error(fmt.Sprintf("error occurred reading sequencer/todo: %s", err.Error()))
// we assume an error means the file doesn't exist so we just return
return nil
}
commits := []*models.Commit{}
todos, err := todo.Parse(bytes.NewBuffer(bytesContent), self.config.GetCoreCommentChar())
if err != nil {
self.Log.Error(fmt.Sprintf("error occurred while parsing sequencer/todo file: %s", err.Error()))
return nil
}
for _, t := range todos {
if t.Commit == "" {
// Command does not have a commit associated, skip
continue
}
commits = utils.Prepend(commits, &models.Commit{
Hash: t.Commit,
Name: t.Msg,
Status: models.StatusRebasing,
Action: t.Command,
})
}
return commits
}
func (self *CommitLoader) getConflictedSequencerCommit(workingTreeState models.WorkingTreeState) *models.Commit {
var shaFile string
var action todo.TodoCommand
if workingTreeState.CherryPicking {
shaFile = "CHERRY_PICK_HEAD"
action = todo.Pick
} else if workingTreeState.Reverting {
shaFile = "REVERT_HEAD"
action = todo.Revert
} else {
return nil
}
bytesContent, err := self.readFile(filepath.Join(self.repoPaths.WorktreeGitDirPath(), shaFile))
if err != nil {
self.Log.Error(fmt.Sprintf("error occurred reading %s: %s", shaFile, err.Error()))
// we assume an error means the file doesn't exist so we just return
return nil
}
lines := strings.Split(string(bytesContent), "\n")
if len(lines) == 0 {
return nil
}
return &models.Commit{
Hash: lines[0],
Status: models.StatusConflicted,
Action: action,
}
} }
func setCommitMergedStatuses(ancestor string, commits []*models.Commit) { func setCommitMergedStatuses(ancestor string, commits []*models.Commit) {

View File

@ -328,18 +328,19 @@ func TestGetCommits(t *testing.T) {
func TestCommitLoader_getConflictedCommitImpl(t *testing.T) { func TestCommitLoader_getConflictedCommitImpl(t *testing.T) {
scenarios := []struct { scenarios := []struct {
testName string testName string
todos []todo.Todo todos []todo.Todo
doneTodos []todo.Todo doneTodos []todo.Todo
amendFileExists bool amendFileExists bool
expectedHash string messageFileExists bool
expectedResult *models.Commit
}{ }{
{ {
testName: "no done todos", testName: "no done todos",
todos: []todo.Todo{}, todos: []todo.Todo{},
doneTodos: []todo.Todo{}, doneTodos: []todo.Todo{},
amendFileExists: false, amendFileExists: false,
expectedHash: "", expectedResult: nil,
}, },
{ {
testName: "common case (conflict)", testName: "common case (conflict)",
@ -355,7 +356,11 @@ func TestCommitLoader_getConflictedCommitImpl(t *testing.T) {
}, },
}, },
amendFileExists: false, amendFileExists: false,
expectedHash: "fa1afe1", expectedResult: &models.Commit{
Hash: "fa1afe1",
Action: todo.Pick,
Status: models.StatusConflicted,
},
}, },
{ {
testName: "last command was 'break'", testName: "last command was 'break'",
@ -364,7 +369,7 @@ func TestCommitLoader_getConflictedCommitImpl(t *testing.T) {
{Command: todo.Break}, {Command: todo.Break},
}, },
amendFileExists: false, amendFileExists: false,
expectedHash: "", expectedResult: nil,
}, },
{ {
testName: "last command was 'exec'", testName: "last command was 'exec'",
@ -376,7 +381,7 @@ func TestCommitLoader_getConflictedCommitImpl(t *testing.T) {
}, },
}, },
amendFileExists: false, amendFileExists: false,
expectedHash: "", expectedResult: nil,
}, },
{ {
testName: "last command was 'reword'", testName: "last command was 'reword'",
@ -385,7 +390,7 @@ func TestCommitLoader_getConflictedCommitImpl(t *testing.T) {
{Command: todo.Reword}, {Command: todo.Reword},
}, },
amendFileExists: false, amendFileExists: false,
expectedHash: "", expectedResult: nil,
}, },
{ {
testName: "'pick' was rescheduled", testName: "'pick' was rescheduled",
@ -402,7 +407,7 @@ func TestCommitLoader_getConflictedCommitImpl(t *testing.T) {
}, },
}, },
amendFileExists: false, amendFileExists: false,
expectedHash: "", expectedResult: nil,
}, },
{ {
testName: "'pick' was rescheduled, buggy git version", testName: "'pick' was rescheduled, buggy git version",
@ -427,7 +432,7 @@ func TestCommitLoader_getConflictedCommitImpl(t *testing.T) {
}, },
}, },
amendFileExists: false, amendFileExists: false,
expectedHash: "", expectedResult: nil,
}, },
{ {
testName: "conflicting 'pick' after 'exec'", testName: "conflicting 'pick' after 'exec'",
@ -452,7 +457,11 @@ func TestCommitLoader_getConflictedCommitImpl(t *testing.T) {
}, },
}, },
amendFileExists: false, amendFileExists: false,
expectedHash: "fa1afe1", expectedResult: &models.Commit{
Hash: "fa1afe1",
Action: todo.Pick,
Status: models.StatusConflicted,
},
}, },
{ {
testName: "'edit' with amend file", testName: "'edit' with amend file",
@ -464,10 +473,10 @@ func TestCommitLoader_getConflictedCommitImpl(t *testing.T) {
}, },
}, },
amendFileExists: true, amendFileExists: true,
expectedHash: "", expectedResult: nil,
}, },
{ {
testName: "'edit' without amend file", testName: "'edit' without amend file but message file",
todos: []todo.Todo{}, todos: []todo.Todo{},
doneTodos: []todo.Todo{ doneTodos: []todo.Todo{
{ {
@ -475,8 +484,26 @@ func TestCommitLoader_getConflictedCommitImpl(t *testing.T) {
Commit: "fa1afe1", Commit: "fa1afe1",
}, },
}, },
amendFileExists: false, amendFileExists: false,
expectedHash: "fa1afe1", messageFileExists: true,
expectedResult: &models.Commit{
Hash: "fa1afe1",
Action: todo.Edit,
Status: models.StatusConflicted,
},
},
{
testName: "'edit' without amend and without message file",
todos: []todo.Todo{},
doneTodos: []todo.Todo{
{
Command: todo.Edit,
Commit: "fa1afe1",
},
},
amendFileExists: false,
messageFileExists: false,
expectedResult: nil,
}, },
} }
for _, scenario := range scenarios { for _, scenario := range scenarios {
@ -496,8 +523,8 @@ func TestCommitLoader_getConflictedCommitImpl(t *testing.T) {
}, },
} }
hash := builder.getConflictedCommitImpl(scenario.todos, scenario.doneTodos, scenario.amendFileExists) hash := builder.getConflictedCommitImpl(scenario.todos, scenario.doneTodos, scenario.amendFileExists, scenario.messageFileExists)
assert.Equal(t, scenario.expectedHash, hash) assert.Equal(t, scenario.expectedResult, hash)
}) })
} }
} }

View File

@ -18,7 +18,7 @@ const (
StatusPushed StatusPushed
StatusMerged StatusMerged
StatusRebasing StatusRebasing
StatusSelected StatusConflicted
StatusReflog StatusReflog
) )
@ -26,8 +26,6 @@ const (
// Conveniently for us, the todo package starts the enum at 1, and given // Conveniently for us, the todo package starts the enum at 1, and given
// that it doesn't have a "none" value, we're setting ours to 0 // that it doesn't have a "none" value, we're setting ours to 0
ActionNone todo.TodoCommand = 0 ActionNone todo.TodoCommand = 0
// "Comment" is the last one of the todo package's enum entries
ActionConflict = todo.Comment + 1
) )
type Divergence int type Divergence int

View File

@ -829,12 +829,12 @@ func (self *FilesController) handleAmendCommitPress() error {
func (self *FilesController) isResolvingConflicts() bool { func (self *FilesController) isResolvingConflicts() bool {
commits := self.c.Model().Commits commits := self.c.Model().Commits
for _, c := range commits { for _, c := range commits {
if c.Status != models.StatusRebasing { if c.Status == models.StatusConflicted {
break
}
if c.Action == models.ActionConflict {
return true return true
} }
if !c.IsTODO() {
break
}
} }
return false return false
} }

View File

@ -122,7 +122,7 @@ func (self *MergeAndRebaseHelper) genericMergeCommand(command string) error {
func (self *MergeAndRebaseHelper) hasExecTodos() bool { func (self *MergeAndRebaseHelper) hasExecTodos() bool {
for _, commit := range self.c.Model().Commits { for _, commit := range self.c.Model().Commits {
if commit.Status != models.StatusRebasing { if !commit.IsTODO() {
break break
} }
if commit.Action == todo.Exec { if commit.Action == todo.Exec {

View File

@ -684,6 +684,11 @@ func (self *LocalCommitsController) isRebasing() bool {
return self.c.Model().WorkingTreeStateAtLastCommitRefresh.Any() return self.c.Model().WorkingTreeStateAtLastCommitRefresh.Any()
} }
func (self *LocalCommitsController) isCherryPickingOrReverting() bool {
return self.c.Model().WorkingTreeStateAtLastCommitRefresh.CherryPicking ||
self.c.Model().WorkingTreeStateAtLastCommitRefresh.Reverting
}
func (self *LocalCommitsController) moveDown(selectedCommits []*models.Commit, startIdx int, endIdx int) error { func (self *LocalCommitsController) moveDown(selectedCommits []*models.Commit, startIdx int, endIdx int) error {
if self.isRebasing() { if self.isRebasing() {
if err := self.c.Git().Rebase.MoveTodosDown(selectedCommits); err != nil { if err := self.c.Git().Rebase.MoveTodosDown(selectedCommits); err != nil {
@ -1389,7 +1394,7 @@ func (self *LocalCommitsController) canMoveDown(selectedCommits []*models.Commit
if self.isRebasing() { if self.isRebasing() {
commits := self.c.Model().Commits commits := self.c.Model().Commits
if !commits[endIdx+1].IsTODO() || commits[endIdx+1].Action == models.ActionConflict { if !commits[endIdx+1].IsTODO() || commits[endIdx+1].Status == models.StatusConflicted {
return &types.DisabledReason{Text: self.c.Tr.CannotMoveAnyFurther} return &types.DisabledReason{Text: self.c.Tr.CannotMoveAnyFurther}
} }
} }
@ -1405,7 +1410,7 @@ func (self *LocalCommitsController) canMoveUp(selectedCommits []*models.Commit,
if self.isRebasing() { if self.isRebasing() {
commits := self.c.Model().Commits commits := self.c.Model().Commits
if !commits[startIdx-1].IsTODO() || commits[startIdx-1].Action == models.ActionConflict { if !commits[startIdx-1].IsTODO() || commits[startIdx-1].Status == models.StatusConflicted {
return &types.DisabledReason{Text: self.c.Tr.CannotMoveAnyFurther} return &types.DisabledReason{Text: self.c.Tr.CannotMoveAnyFurther}
} }
} }
@ -1415,6 +1420,10 @@ func (self *LocalCommitsController) canMoveUp(selectedCommits []*models.Commit,
// Ensures that if we are mid-rebase, we're only selecting valid commits (non-conflict TODO commits) // Ensures that if we are mid-rebase, we're only selecting valid commits (non-conflict TODO commits)
func (self *LocalCommitsController) midRebaseCommandEnabled(selectedCommits []*models.Commit, startIdx int, endIdx int) *types.DisabledReason { func (self *LocalCommitsController) midRebaseCommandEnabled(selectedCommits []*models.Commit, startIdx int, endIdx int) *types.DisabledReason {
if self.isCherryPickingOrReverting() {
return &types.DisabledReason{Text: self.c.Tr.NotAllowedMidCherryPickOrRevert}
}
if !self.isRebasing() { if !self.isRebasing() {
return nil return nil
} }
@ -1434,6 +1443,10 @@ func (self *LocalCommitsController) midRebaseCommandEnabled(selectedCommits []*m
// Ensures that if we are mid-rebase, we're only selecting commits that can be moved // Ensures that if we are mid-rebase, we're only selecting commits that can be moved
func (self *LocalCommitsController) midRebaseMoveCommandEnabled(selectedCommits []*models.Commit, startIdx int, endIdx int) *types.DisabledReason { func (self *LocalCommitsController) midRebaseMoveCommandEnabled(selectedCommits []*models.Commit, startIdx int, endIdx int) *types.DisabledReason {
if self.isCherryPickingOrReverting() {
return &types.DisabledReason{Text: self.c.Tr.NotAllowedMidCherryPickOrRevert}
}
if !self.isRebasing() { if !self.isRebasing() {
if lo.SomeBy(selectedCommits, func(c *models.Commit) bool { return c.IsMerge() }) { if lo.SomeBy(selectedCommits, func(c *models.Commit) bool { return c.IsMerge() }) {
return &types.DisabledReason{Text: self.c.Tr.CannotMoveMergeCommit} return &types.DisabledReason{Text: self.c.Tr.CannotMoveMergeCommit}
@ -1458,6 +1471,10 @@ func (self *LocalCommitsController) midRebaseMoveCommandEnabled(selectedCommits
} }
func (self *LocalCommitsController) canDropCommits(selectedCommits []*models.Commit, startIdx int, endIdx int) *types.DisabledReason { func (self *LocalCommitsController) canDropCommits(selectedCommits []*models.Commit, startIdx int, endIdx int) *types.DisabledReason {
if self.isCherryPickingOrReverting() {
return &types.DisabledReason{Text: self.c.Tr.NotAllowedMidCherryPickOrRevert}
}
if !self.isRebasing() { if !self.isRebasing() {
if len(selectedCommits) > 1 && lo.SomeBy(selectedCommits, func(c *models.Commit) bool { return c.IsMerge() }) { if len(selectedCommits) > 1 && lo.SomeBy(selectedCommits, func(c *models.Commit) bool { return c.IsMerge() }) {
return &types.DisabledReason{Text: self.c.Tr.DroppingMergeRequiresSingleSelection} return &types.DisabledReason{Text: self.c.Tr.DroppingMergeRequiresSingleSelection}
@ -1502,6 +1519,10 @@ func isChangeOfRebaseTodoAllowed(oldAction todo.TodoCommand) bool {
} }
func (self *LocalCommitsController) pickEnabled(selectedCommits []*models.Commit, startIdx int, endIdx int) *types.DisabledReason { func (self *LocalCommitsController) pickEnabled(selectedCommits []*models.Commit, startIdx int, endIdx int) *types.DisabledReason {
if self.isCherryPickingOrReverting() {
return &types.DisabledReason{Text: self.c.Tr.NotAllowedMidCherryPickOrRevert}
}
if !self.isRebasing() { if !self.isRebasing() {
// if not rebasing, we're going to do a pull so we don't care about the selection // if not rebasing, we're going to do a pull so we don't care about the selection
return nil return nil

View File

@ -186,7 +186,7 @@ func GetCommitListDisplayStrings(
unfilteredIdx := i + startIdx unfilteredIdx := i + startIdx
bisectStatus = getBisectStatus(unfilteredIdx, commit.Hash, bisectInfo, bisectBounds) bisectStatus = getBisectStatus(unfilteredIdx, commit.Hash, bisectInfo, bisectBounds)
isYouAreHereCommit := false isYouAreHereCommit := false
if showYouAreHereLabel && (commit.Action == models.ActionConflict || unfilteredIdx == rebaseOffset) { if showYouAreHereLabel && (commit.Status == models.StatusConflicted || unfilteredIdx == rebaseOffset) {
isYouAreHereCommit = true isYouAreHereCommit = true
showYouAreHereLabel = false showYouAreHereLabel = false
} }
@ -395,8 +395,7 @@ func displayCommit(
actionString := "" actionString := ""
if commit.Action != models.ActionNone { if commit.Action != models.ActionNone {
todoString := lo.Ternary(commit.Action == models.ActionConflict, "conflict", commit.Action.String()) actionString = actionColorMap(commit.Action, commit.Status).Sprint(commit.Action.String()) + " "
actionString = actionColorMap(commit.Action).Sprint(todoString) + " "
} }
tagString := "" tagString := ""
@ -429,8 +428,13 @@ func displayCommit(
mark := "" mark := ""
if isYouAreHereCommit { if isYouAreHereCommit {
color := lo.Ternary(commit.Action == models.ActionConflict, style.FgRed, style.FgYellow) color := style.FgYellow
youAreHere := color.Sprintf("<-- %s ---", common.Tr.YouAreHere) text := common.Tr.YouAreHere
if commit.Status == models.StatusConflicted {
color = style.FgRed
text = common.Tr.ConflictLabel
}
youAreHere := color.Sprintf("<-- %s ---", text)
mark = fmt.Sprintf("%s ", youAreHere) mark = fmt.Sprintf("%s ", youAreHere)
} else if isMarkedBaseCommit { } else if isMarkedBaseCommit {
rebaseFromHere := style.FgYellow.Sprint(common.Tr.MarkedCommitMarker) rebaseFromHere := style.FgYellow.Sprint(common.Tr.MarkedCommitMarker)
@ -501,7 +505,7 @@ func getHashColor(
hashColor = style.FgYellow hashColor = style.FgYellow
case models.StatusMerged: case models.StatusMerged:
hashColor = style.FgGreen hashColor = style.FgGreen
case models.StatusRebasing: case models.StatusRebasing, models.StatusConflicted:
hashColor = style.FgBlue hashColor = style.FgBlue
case models.StatusReflog: case models.StatusReflog:
hashColor = style.FgBlue hashColor = style.FgBlue
@ -519,7 +523,11 @@ func getHashColor(
return hashColor return hashColor
} }
func actionColorMap(action todo.TodoCommand) style.TextStyle { func actionColorMap(action todo.TodoCommand, status models.CommitStatus) style.TextStyle {
if status == models.StatusConflicted {
return style.FgRed
}
switch action { switch action {
case todo.Pick: case todo.Pick:
return style.FgCyan return style.FgCyan
@ -529,8 +537,6 @@ func actionColorMap(action todo.TodoCommand) style.TextStyle {
return style.FgGreen return style.FgGreen
case todo.Fixup: case todo.Fixup:
return style.FgMagenta return style.FgMagenta
case models.ActionConflict:
return style.FgRed
default: default:
return style.FgYellow return style.FgYellow
} }

View File

@ -349,9 +349,11 @@ type TranslationSet struct {
ErrorOccurred string ErrorOccurred string
NoRoom string NoRoom string
YouAreHere string YouAreHere string
ConflictLabel string
YouDied string YouDied string
RewordNotSupported string RewordNotSupported string
ChangingThisActionIsNotAllowed string ChangingThisActionIsNotAllowed string
NotAllowedMidCherryPickOrRevert string
DroppingMergeRequiresSingleSelection string DroppingMergeRequiresSingleSelection string
CherryPickCopy string CherryPickCopy string
CherryPickCopyTooltip string CherryPickCopyTooltip string
@ -1416,9 +1418,11 @@ func EnglishTranslationSet() *TranslationSet {
ErrorOccurred: "An error occurred! Please create an issue at", ErrorOccurred: "An error occurred! Please create an issue at",
NoRoom: "Not enough room", NoRoom: "Not enough room",
YouAreHere: "YOU ARE HERE", YouAreHere: "YOU ARE HERE",
ConflictLabel: "CONFLICT",
YouDied: "YOU DIED!", YouDied: "YOU DIED!",
RewordNotSupported: "Rewording commits while interactively rebasing is not currently supported", RewordNotSupported: "Rewording commits while interactively rebasing is not currently supported",
ChangingThisActionIsNotAllowed: "Changing this kind of rebase todo entry is not allowed", ChangingThisActionIsNotAllowed: "Changing this kind of rebase todo entry is not allowed",
NotAllowedMidCherryPickOrRevert: "This action is not allowed while cherry-picking or reverting",
DroppingMergeRequiresSingleSelection: "Dropping a merge commit requires a single selected item", DroppingMergeRequiresSingleSelection: "Dropping a merge commit requires a single selected item",
CherryPickCopy: "Copy (cherry-pick)", CherryPickCopy: "Copy (cherry-pick)",
CherryPickCopyTooltip: "Mark commit as copied. Then, within the local commits view, you can press `{{.paste}}` to paste (cherry-pick) the copied commit(s) into your checked out branch. At any time you can press `{{.escape}}` to cancel the selection.", CherryPickCopyTooltip: "Mark commit as copied. Then, within the local commits view, you can press `{{.paste}}` to paste (cherry-pick) the copied commit(s) into your checked out branch. At any time you can press `{{.escape}}` to cancel the selection.",

View File

@ -53,7 +53,7 @@ var RebaseAndDrop = NewIntegrationTest(NewIntegrationTestArgs{
TopLines( TopLines(
MatchesRegexp(`pick.*to keep`).IsSelected(), MatchesRegexp(`pick.*to keep`).IsSelected(),
MatchesRegexp(`pick.*to remove`), MatchesRegexp(`pick.*to remove`),
MatchesRegexp(`conflict.*YOU ARE HERE.*first change`), MatchesRegexp(`pick.*CONFLICT.*first change`),
MatchesRegexp("second-change-branch unrelated change"), MatchesRegexp("second-change-branch unrelated change"),
MatchesRegexp("second change"), MatchesRegexp("second change"),
MatchesRegexp("original"), MatchesRegexp("original"),
@ -63,7 +63,7 @@ var RebaseAndDrop = NewIntegrationTest(NewIntegrationTestArgs{
TopLines( TopLines(
MatchesRegexp(`pick.*to keep`), MatchesRegexp(`pick.*to keep`),
MatchesRegexp(`drop.*to remove`).IsSelected(), MatchesRegexp(`drop.*to remove`).IsSelected(),
MatchesRegexp(`conflict.*YOU ARE HERE.*first change`), MatchesRegexp(`pick.*CONFLICT.*first change`),
MatchesRegexp("second-change-branch unrelated change"), MatchesRegexp("second-change-branch unrelated change"),
MatchesRegexp("second change"), MatchesRegexp("second change"),
MatchesRegexp("original"), MatchesRegexp("original"),

View File

@ -30,7 +30,7 @@ var AmendWhenThereAreConflictsAndAmend = NewIntegrationTest(NewIntegrationTestAr
Focus(). Focus().
Lines( Lines(
Contains("pick").Contains("commit three"), Contains("pick").Contains("commit three"),
Contains("conflict").Contains("<-- YOU ARE HERE --- file1 changed in branch"), Contains("pick").Contains("<-- CONFLICT --- file1 changed in branch"),
Contains("commit two"), Contains("commit two"),
Contains("file1 changed in master"), Contains("file1 changed in master"),
Contains("base commit"), Contains("base commit"),

View File

@ -34,7 +34,7 @@ var AmendWhenThereAreConflictsAndCancel = NewIntegrationTest(NewIntegrationTestA
Focus(). Focus().
Lines( Lines(
Contains("pick").Contains("commit three"), Contains("pick").Contains("commit three"),
Contains("conflict").Contains("<-- YOU ARE HERE --- file1 changed in branch"), Contains("pick").Contains("<-- CONFLICT --- file1 changed in branch"),
Contains("commit two"), Contains("commit two"),
Contains("file1 changed in master"), Contains("file1 changed in master"),
Contains("base commit"), Contains("base commit"),

View File

@ -0,0 +1,87 @@
package commit
import (
"github.com/jesseduffield/lazygit/pkg/config"
. "github.com/jesseduffield/lazygit/pkg/integration/components"
)
var RevertWithConflictMultipleCommits = NewIntegrationTest(NewIntegrationTestArgs{
Description: "Reverts a range of commits, the first of which conflicts",
ExtraCmdArgs: []string{},
Skip: false,
SetupConfig: func(cfg *config.AppConfig) {
// TODO: use our revert UI once we support range-select for reverts
cfg.GetUserConfig().CustomCommands = []config.CustomCommand{
{
Key: "X",
Context: "commits",
Command: "git -c core.editor=: revert HEAD^ HEAD^^",
},
}
},
SetupRepo: func(shell *Shell) {
shell.CreateFileAndAdd("myfile", "")
shell.Commit("add empty file")
shell.CreateFileAndAdd("otherfile", "")
shell.Commit("unrelated change")
shell.CreateFileAndAdd("myfile", "first line\n")
shell.Commit("add first line")
shell.UpdateFileAndAdd("myfile", "first line\nsecond line\n")
shell.Commit("add second line")
},
Run: func(t *TestDriver, keys config.KeybindingConfig) {
t.Views().Commits().
Focus().
Lines(
Contains("CI ◯ add second line").IsSelected(),
Contains("CI ◯ add first line"),
Contains("CI ◯ unrelated change"),
Contains("CI ◯ add empty file"),
).
Press("X").
Tap(func() {
t.ExpectPopup().Alert().
Title(Equals("Error")).
// The exact error message is different on different git versions,
// but they all contain the word 'conflict' somewhere.
Content(Contains("conflict")).
Confirm()
}).
Lines(
Contains("revert").Contains("CI unrelated change"),
Contains("revert").Contains("CI <-- CONFLICT --- add first line"),
Contains("CI ◯ add second line"),
Contains("CI ◯ add first line"),
Contains("CI ◯ unrelated change"),
Contains("CI ◯ add empty file"),
)
t.Views().Options().Content(Contains("View revert options: m"))
t.Views().Information().Content(Contains("Reverting (Reset)"))
t.Views().Files().Focus().
Lines(
Contains("UU myfile").IsSelected(),
).
PressEnter()
t.Views().MergeConflicts().IsFocused().
SelectNextItem().
PressPrimaryAction()
t.ExpectPopup().Alert().
Title(Equals("Continue")).
Content(Contains("All merge conflicts resolved. Continue the revert?")).
Confirm()
t.Views().Commits().
Lines(
Contains(`CI ◯ Revert "unrelated change"`),
Contains(`CI ◯ Revert "add first line"`),
Contains("CI ◯ add second line"),
Contains("CI ◯ add first line"),
Contains("CI ◯ unrelated change"),
Contains("CI ◯ add empty file"),
)
},
})

View File

@ -39,16 +39,10 @@ var RevertWithConflictSingleCommit = NewIntegrationTest(NewIntegrationTestArgs{
Confirm() Confirm()
}). }).
Lines( Lines(
/* EXPECTED:
Proper display of revert commits is not implemented yet; we'll do this in the next PR
Contains("revert").Contains("CI <-- CONFLICT --- add first line"), Contains("revert").Contains("CI <-- CONFLICT --- add first line"),
Contains("CI ◯ add second line"), Contains("CI ◯ add second line"),
Contains("CI ◯ add first line"), Contains("CI ◯ add first line"),
Contains("CI ◯ add empty file"), Contains("CI ◯ add empty file"),
ACTUAL: */
Contains("CI ◯ <-- YOU ARE HERE --- add second line"),
Contains("CI ◯ add first line"),
Contains("CI ◯ add empty file"),
) )
t.Views().Options().Content(Contains("View revert options: m")) t.Views().Options().Content(Contains("View revert options: m"))

View File

@ -44,7 +44,7 @@ func doTheRebaseForAmendTests(t *TestDriver, keys config.KeybindingConfig) {
t.Views().Commits(). t.Views().Commits().
Lines( Lines(
Contains("pick").Contains("commit three"), Contains("pick").Contains("commit three"),
Contains("conflict").Contains("<-- YOU ARE HERE --- file1 changed in branch"), Contains("pick").Contains("<-- CONFLICT --- file1 changed in branch"),
Contains("commit two"), Contains("commit two"),
Contains("file1 changed in master"), Contains("file1 changed in master"),
Contains("base commit"), Contains("base commit"),

View File

@ -35,7 +35,7 @@ var AmendCommitWithConflict = NewIntegrationTest(NewIntegrationTestArgs{
}). }).
Lines( Lines(
Contains("pick").Contains("three"), Contains("pick").Contains("three"),
Contains("conflict").Contains("<-- YOU ARE HERE --- fixup! two"), Contains("fixup").Contains("<-- CONFLICT --- fixup! two"),
Contains("two"), Contains("two"),
Contains("one"), Contains("one"),
) )
@ -66,7 +66,7 @@ var AmendCommitWithConflict = NewIntegrationTest(NewIntegrationTestArgs{
t.Views().Commits(). t.Views().Commits().
Lines( Lines(
Contains("<-- YOU ARE HERE --- three"), Contains("<-- CONFLICT --- three"),
Contains("two"), Contains("two"),
Contains("one"), Contains("one"),
) )

View File

@ -33,10 +33,10 @@ var EditTheConflCommit = NewIntegrationTest(NewIntegrationTestArgs{
Focus(). Focus().
Lines( Lines(
Contains("pick").Contains("commit two"), Contains("pick").Contains("commit two"),
Contains("conflict").Contains("<-- YOU ARE HERE --- commit three"), Contains("pick").Contains("<-- CONFLICT --- commit three"),
Contains("commit one"), Contains("commit one"),
). ).
NavigateToLine(Contains("<-- YOU ARE HERE --- commit three")). NavigateToLine(Contains("<-- CONFLICT --- commit three")).
Press(keys.Commits.RenameCommit) Press(keys.Commits.RenameCommit)
t.ExpectToast(Contains("Disabled: Rewording commits while interactively rebasing is not currently supported")) t.ExpectToast(Contains("Disabled: Rewording commits while interactively rebasing is not currently supported"))

View File

@ -0,0 +1,63 @@
package interactive_rebase
import (
"github.com/jesseduffield/lazygit/pkg/config"
. "github.com/jesseduffield/lazygit/pkg/integration/components"
)
var InteractiveRebaseWithConflictForEditCommand = NewIntegrationTest(NewIntegrationTestArgs{
Description: "Rebase a branch interactively, and edit a commit that will conflict",
ExtraCmdArgs: []string{},
Skip: false,
SetupConfig: func(cfg *config.AppConfig) {},
SetupRepo: func(shell *Shell) {
shell.EmptyCommit("initial commit")
shell.CreateFileAndAdd("file.txt", "master content")
shell.Commit("master commit")
shell.NewBranchFrom("branch", "master^")
shell.CreateNCommits(3)
shell.CreateFileAndAdd("file.txt", "branch content")
shell.Commit("this will conflict")
},
Run: func(t *TestDriver, keys config.KeybindingConfig) {
t.Views().Commits().
Focus().
Lines(
Contains("this will conflict").IsSelected(),
Contains("commit 03"),
Contains("commit 02"),
Contains("commit 01"),
Contains("initial commit"),
)
t.Views().Branches().
Focus().
NavigateToLine(Contains("master")).
Press(keys.Branches.RebaseBranch)
t.ExpectPopup().Menu().
Title(Equals("Rebase 'branch'")).
Select(Contains("Interactive rebase")).
Confirm()
t.Views().Commits().
IsFocused().
NavigateToLine(Contains("this will conflict")).
Press(keys.Universal.Edit)
t.Common().ContinueRebase()
t.ExpectPopup().Menu().
Title(Equals("Conflicts!")).
Cancel()
t.Views().Commits().
Lines(
Contains("edit").Contains("<-- CONFLICT --- this will conflict").IsSelected(),
Contains("commit 03"),
Contains("commit 02"),
Contains("commit 01"),
Contains("master commit"),
Contains("initial commit"),
)
},
})

View File

@ -0,0 +1,57 @@
package interactive_rebase
import (
"github.com/jesseduffield/lazygit/pkg/config"
. "github.com/jesseduffield/lazygit/pkg/integration/components"
)
var RevertDuringRebaseWhenStoppedOnEdit = NewIntegrationTest(NewIntegrationTestArgs{
Description: "Revert a series of commits while stopped in a rebase",
ExtraCmdArgs: []string{},
Skip: false,
SetupConfig: func(cfg *config.AppConfig) {
// TODO: use our revert UI once we support range-select for reverts
cfg.GetUserConfig().CustomCommands = []config.CustomCommand{
{
Key: "X",
Context: "commits",
Command: "git -c core.editor=: revert HEAD^ HEAD^^",
},
}
},
SetupRepo: func(shell *Shell) {
shell.EmptyCommit("master commit")
shell.NewBranch("branch")
shell.CreateNCommits(4)
},
Run: func(t *TestDriver, keys config.KeybindingConfig) {
t.Views().Commits().
Focus().
Lines(
Contains("commit 04").IsSelected(),
Contains("commit 03"),
Contains("commit 02"),
Contains("commit 01"),
Contains("master commit"),
).
NavigateToLine(Contains("commit 03")).
Press(keys.Universal.Edit).
Lines(
Contains("pick").Contains("commit 04"),
Contains("<-- YOU ARE HERE --- commit 03").IsSelected(),
Contains("commit 02"),
Contains("commit 01"),
Contains("master commit"),
).
Press("X").
Lines(
Contains("pick").Contains("commit 04"),
Contains(`<-- YOU ARE HERE --- Revert "commit 01"`).IsSelected(),
Contains(`Revert "commit 02"`),
Contains("commit 03"),
Contains("commit 02"),
Contains("commit 01"),
Contains("master commit"),
)
},
})

View File

@ -0,0 +1,114 @@
package interactive_rebase
import (
"github.com/jesseduffield/lazygit/pkg/config"
. "github.com/jesseduffield/lazygit/pkg/integration/components"
)
var RevertMultipleCommitsInInteractiveRebase = NewIntegrationTest(NewIntegrationTestArgs{
Description: "Reverts a range of commits, the first of which conflicts, in the middle of an interactive rebase",
ExtraCmdArgs: []string{},
Skip: false,
SetupConfig: func(cfg *config.AppConfig) {
// TODO: use our revert UI once we support range-select for reverts
cfg.GetUserConfig().CustomCommands = []config.CustomCommand{
{
Key: "X",
Context: "commits",
Command: "git -c core.editor=: revert HEAD^ HEAD^^",
},
}
},
SetupRepo: func(shell *Shell) {
shell.CreateFileAndAdd("myfile", "")
shell.Commit("add empty file")
shell.CreateFileAndAdd("otherfile", "")
shell.Commit("unrelated change 1")
shell.CreateFileAndAdd("myfile", "first line\n")
shell.Commit("add first line")
shell.UpdateFileAndAdd("myfile", "first line\nsecond line\n")
shell.Commit("add second line")
shell.EmptyCommit("unrelated change 2")
shell.EmptyCommit("unrelated change 3")
},
Run: func(t *TestDriver, keys config.KeybindingConfig) {
t.Views().Commits().
Focus().
Lines(
Contains("CI ◯ unrelated change 3").IsSelected(),
Contains("CI ◯ unrelated change 2"),
Contains("CI ◯ add second line"),
Contains("CI ◯ add first line"),
Contains("CI ◯ unrelated change 1"),
Contains("CI ◯ add empty file"),
).
NavigateToLine(Contains("add second line")).
Press(keys.Universal.Edit).
Press("X").
Tap(func() {
t.ExpectPopup().Alert().
Title(Equals("Error")).
// The exact error message is different on different git versions,
// but they all contain the word 'conflict' somewhere.
Content(Contains("conflict")).
Confirm()
}).
Lines(
Contains("CI unrelated change 3"),
Contains("CI unrelated change 2"),
Contains("revert").Contains("CI unrelated change 1"),
Contains("revert").Contains("CI <-- CONFLICT --- add first line"),
Contains("CI ◯ add second line"),
Contains("CI ◯ add first line"),
Contains("CI ◯ unrelated change 1"),
Contains("CI ◯ add empty file"),
)
t.Views().Options().Content(Contains("View revert options: m"))
t.Views().Information().Content(Contains("Reverting (Reset)"))
t.Views().Files().Focus().
Lines(
Contains("UU myfile").IsSelected(),
).
PressEnter()
t.Views().MergeConflicts().IsFocused().
SelectNextItem().
PressPrimaryAction()
t.ExpectPopup().Alert().
Title(Equals("Continue")).
Content(Contains("All merge conflicts resolved. Continue the revert?")).
Confirm()
t.Views().Commits().
Lines(
Contains("pick").Contains("CI unrelated change 3"),
Contains("pick").Contains("CI unrelated change 2"),
Contains(`CI ◯ <-- YOU ARE HERE --- Revert "unrelated change 1"`),
Contains(`CI ◯ Revert "add first line"`),
Contains("CI ◯ add second line"),
Contains("CI ◯ add first line"),
Contains("CI ◯ unrelated change 1"),
Contains("CI ◯ add empty file"),
)
t.Views().Options().Content(Contains("View rebase options: m"))
t.Views().Information().Content(Contains("Rebasing (Reset)"))
t.Common().ContinueRebase()
t.Views().Commits().
Lines(
Contains("CI ◯ unrelated change 3"),
Contains("CI ◯ unrelated change 2"),
Contains(`CI ◯ Revert "unrelated change 1"`),
Contains(`CI ◯ Revert "add first line"`),
Contains("CI ◯ add second line"),
Contains("CI ◯ add first line"),
Contains("CI ◯ unrelated change 1"),
Contains("CI ◯ add empty file"),
)
},
})

View File

@ -0,0 +1,107 @@
package interactive_rebase
import (
"github.com/jesseduffield/lazygit/pkg/config"
. "github.com/jesseduffield/lazygit/pkg/integration/components"
)
var RevertSingleCommitInInteractiveRebase = NewIntegrationTest(NewIntegrationTestArgs{
Description: "Reverts a commit that conflicts in the middle of an interactive rebase",
ExtraCmdArgs: []string{},
Skip: false,
SetupConfig: func(config *config.AppConfig) {},
SetupRepo: func(shell *Shell) {
shell.CreateFileAndAdd("myfile", "")
shell.Commit("add empty file")
shell.CreateFileAndAdd("myfile", "first line\n")
shell.Commit("add first line")
shell.UpdateFileAndAdd("myfile", "first line\nsecond line\n")
shell.Commit("add second line")
shell.EmptyCommit("unrelated change 1")
shell.EmptyCommit("unrelated change 2")
},
Run: func(t *TestDriver, keys config.KeybindingConfig) {
t.Views().Commits().
Focus().
Lines(
Contains("CI ◯ unrelated change 2").IsSelected(),
Contains("CI ◯ unrelated change 1"),
Contains("CI ◯ add second line"),
Contains("CI ◯ add first line"),
Contains("CI ◯ add empty file"),
).
NavigateToLine(Contains("add second line")).
Press(keys.Universal.Edit).
SelectNextItem().
Press(keys.Commits.RevertCommit).
Tap(func() {
t.ExpectPopup().Confirmation().
Title(Equals("Revert commit")).
Content(MatchesRegexp(`Are you sure you want to revert \w+?`)).
Confirm()
t.ExpectPopup().Menu().
Title(Equals("Conflicts!")).
Select(Contains("View conflicts")).
Cancel() // stay in commits panel
}).
Lines(
Contains("CI unrelated change 2"),
Contains("CI unrelated change 1"),
Contains("revert").Contains("CI <-- CONFLICT --- add first line"),
Contains("CI ◯ add second line"),
Contains("CI ◯ add first line").IsSelected(),
Contains("CI ◯ add empty file"),
).
Press(keys.Commits.MoveDownCommit).
Tap(func() {
t.ExpectToast(Equals("Disabled: This action is not allowed while cherry-picking or reverting"))
}).
Press(keys.Universal.Remove).
Tap(func() {
t.ExpectToast(Equals("Disabled: This action is not allowed while cherry-picking or reverting"))
})
t.Views().Options().Content(Contains("View revert options: m"))
t.Views().Information().Content(Contains("Reverting (Reset)"))
t.Views().Files().Focus().
Lines(
Contains("UU myfile").IsSelected(),
).
PressEnter()
t.Views().MergeConflicts().IsFocused().
SelectNextItem().
PressPrimaryAction()
t.ExpectPopup().Alert().
Title(Equals("Continue")).
Content(Contains("All merge conflicts resolved. Continue the revert?")).
Confirm()
t.Views().Commits().
Lines(
Contains("pick").Contains("CI unrelated change 2"),
Contains("pick").Contains("CI unrelated change 1"),
Contains(`CI ◯ <-- YOU ARE HERE --- Revert "add first line"`),
Contains("CI ◯ add second line"),
Contains("CI ◯ add first line"),
Contains("CI ◯ add empty file"),
)
t.Views().Options().Content(Contains("View rebase options: m"))
t.Views().Information().Content(Contains("Rebasing (Reset)"))
t.Common().ContinueRebase()
t.Views().Commits().
Lines(
Contains("CI ◯ unrelated change 2"),
Contains("CI ◯ unrelated change 1"),
Contains(`CI ◯ Revert "add first line"`),
Contains("CI ◯ add second line"),
Contains("CI ◯ add first line"),
Contains("CI ◯ add empty file"),
)
},
})

View File

@ -4,13 +4,13 @@ import (
. "github.com/jesseduffield/lazygit/pkg/integration/components" . "github.com/jesseduffield/lazygit/pkg/integration/components"
) )
func handleConflictsFromSwap(t *TestDriver) { func handleConflictsFromSwap(t *TestDriver, expectedCommand string) {
t.Common().AcknowledgeConflicts() t.Common().AcknowledgeConflicts()
t.Views().Commits(). t.Views().Commits().
Lines( Lines(
Contains("pick").Contains("commit two"), Contains("pick").Contains("commit two"),
Contains("conflict").Contains("<-- YOU ARE HERE --- commit three"), Contains(expectedCommand).Contains("<-- CONFLICT --- commit three"),
Contains("commit one"), Contains("commit one"),
) )

View File

@ -44,6 +44,6 @@ var SwapInRebaseWithConflict = NewIntegrationTest(NewIntegrationTestArgs{
t.Common().ContinueRebase() t.Common().ContinueRebase()
}) })
handleConflictsFromSwap(t) handleConflictsFromSwap(t, "pick")
}, },
}) })

View File

@ -47,6 +47,6 @@ var SwapInRebaseWithConflictAndEdit = NewIntegrationTest(NewIntegrationTestArgs{
t.Common().ContinueRebase() t.Common().ContinueRebase()
}) })
handleConflictsFromSwap(t) handleConflictsFromSwap(t, "edit")
}, },
}) })

View File

@ -28,6 +28,6 @@ var SwapWithConflict = NewIntegrationTest(NewIntegrationTestArgs{
). ).
Press(keys.Commits.MoveDownCommit) Press(keys.Commits.MoveDownCommit)
handleConflictsFromSwap(t) handleConflictsFromSwap(t, "pick")
}, },
}) })

View File

@ -49,7 +49,7 @@ var PullRebaseInteractiveConflict = NewIntegrationTest(NewIntegrationTestArgs{
t.Views().Commits(). t.Views().Commits().
Lines( Lines(
Contains("pick").Contains("five"), Contains("pick").Contains("five"),
Contains("conflict").Contains("YOU ARE HERE").Contains("four"), Contains("pick").Contains("CONFLICT").Contains("four"),
Contains("three"), Contains("three"),
Contains("two"), Contains("two"),
Contains("one"), Contains("one"),

View File

@ -50,7 +50,7 @@ var PullRebaseInteractiveConflictDrop = NewIntegrationTest(NewIntegrationTestArg
Focus(). Focus().
Lines( Lines(
Contains("pick").Contains("five").IsSelected(), Contains("pick").Contains("five").IsSelected(),
Contains("conflict").Contains("YOU ARE HERE").Contains("four"), Contains("pick").Contains("CONFLICT").Contains("four"),
Contains("three"), Contains("three"),
Contains("two"), Contains("two"),
Contains("one"), Contains("one"),
@ -58,7 +58,7 @@ var PullRebaseInteractiveConflictDrop = NewIntegrationTest(NewIntegrationTestArg
Press(keys.Universal.Remove). Press(keys.Universal.Remove).
Lines( Lines(
Contains("drop").Contains("five").IsSelected(), Contains("drop").Contains("five").IsSelected(),
Contains("conflict").Contains("YOU ARE HERE").Contains("four"), Contains("pick").Contains("CONFLICT").Contains("four"),
Contains("three"), Contains("three"),
Contains("two"), Contains("two"),
Contains("one"), Contains("one"),

View File

@ -128,6 +128,7 @@ var tests = []*components.IntegrationTest{
commit.ResetAuthorRange, commit.ResetAuthorRange,
commit.Revert, commit.Revert,
commit.RevertMerge, commit.RevertMerge,
commit.RevertWithConflictMultipleCommits,
commit.RevertWithConflictSingleCommit, commit.RevertWithConflictSingleCommit,
commit.Reword, commit.Reword,
commit.Search, commit.Search,
@ -249,6 +250,7 @@ var tests = []*components.IntegrationTest{
interactive_rebase.FixupFirstCommit, interactive_rebase.FixupFirstCommit,
interactive_rebase.FixupSecondCommit, interactive_rebase.FixupSecondCommit,
interactive_rebase.InteractiveRebaseOfCopiedBranch, interactive_rebase.InteractiveRebaseOfCopiedBranch,
interactive_rebase.InteractiveRebaseWithConflictForEditCommand,
interactive_rebase.MidRebaseRangeSelect, interactive_rebase.MidRebaseRangeSelect,
interactive_rebase.Move, interactive_rebase.Move,
interactive_rebase.MoveAcrossBranchBoundaryOutsideRebase, interactive_rebase.MoveAcrossBranchBoundaryOutsideRebase,
@ -262,6 +264,9 @@ var tests = []*components.IntegrationTest{
interactive_rebase.QuickStartKeepSelectionRange, interactive_rebase.QuickStartKeepSelectionRange,
interactive_rebase.Rebase, interactive_rebase.Rebase,
interactive_rebase.RebaseWithCommitThatBecomesEmpty, interactive_rebase.RebaseWithCommitThatBecomesEmpty,
interactive_rebase.RevertDuringRebaseWhenStoppedOnEdit,
interactive_rebase.RevertMultipleCommitsInInteractiveRebase,
interactive_rebase.RevertSingleCommitInInteractiveRebase,
interactive_rebase.RewordCommitWithEditorAndFail, interactive_rebase.RewordCommitWithEditorAndFail,
interactive_rebase.RewordFirstCommit, interactive_rebase.RewordFirstCommit,
interactive_rebase.RewordLastCommit, interactive_rebase.RewordLastCommit,