mirror of
https://github.com/jesseduffield/lazygit.git
synced 2025-05-11 20:36:03 +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:
commit
b7d01d67a6
27 changed files with 684 additions and 106 deletions
|
@ -71,15 +71,13 @@ type GetCommitsOptions struct {
|
|||
// GetCommits obtains the commits of the current branch
|
||||
func (self *CommitLoader) GetCommits(opts GetCommitsOptions) ([]*models.Commit, error) {
|
||||
commits := []*models.Commit{}
|
||||
var rebasingCommits []*models.Commit
|
||||
|
||||
if opts.IncludeRebaseCommits && opts.FilterPath == "" {
|
||||
var err error
|
||||
rebasingCommits, err = self.MergeRebasingCommits(commits)
|
||||
commits, err = self.MergeRebasingCommits(commits)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
commits = append(commits, rebasingCommits...)
|
||||
}
|
||||
|
||||
wg := sync.WaitGroup{}
|
||||
|
@ -126,7 +124,7 @@ func (self *CommitLoader) GetCommits(opts GetCommitsOptions) ([]*models.Commit,
|
|||
if commit.Hash == firstPushedCommit {
|
||||
passedFirstPushedCommit = true
|
||||
}
|
||||
if commit.Status != models.StatusRebasing {
|
||||
if !commit.IsTODO() {
|
||||
if passedFirstPushedCommit {
|
||||
commit.Status = models.StatusPushed
|
||||
} else {
|
||||
|
@ -171,19 +169,26 @@ func (self *CommitLoader) MergeRebasingCommits(commits []*models.Commit) ([]*mod
|
|||
}
|
||||
}
|
||||
|
||||
if !self.getWorkingTreeState().Rebasing {
|
||||
// not in rebase mode so return original commits
|
||||
return result, nil
|
||||
workingTreeState := self.getWorkingTreeState()
|
||||
addConflictedRebasingCommit := true
|
||||
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 err != nil {
|
||||
return nil, err
|
||||
if workingTreeState.Rebasing {
|
||||
rebasingCommits, err := self.getHydratedRebasingCommits(addConflictedRebasingCommit)
|
||||
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
|
||||
}
|
||||
|
||||
|
@ -242,14 +247,36 @@ func (self *CommitLoader) extractCommitFromLine(line string, showDivergence bool
|
|||
}
|
||||
}
|
||||
|
||||
func (self *CommitLoader) getHydratedRebasingCommits() ([]*models.Commit, error) {
|
||||
commits := self.getRebasingCommits()
|
||||
func (self *CommitLoader) getHydratedRebasingCommits(addConflictingCommit bool) ([]*models.Commit, error) {
|
||||
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
|
||||
}
|
||||
|
||||
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 != ""
|
||||
})
|
||||
|
||||
|
@ -273,7 +300,7 @@ func (self *CommitLoader) getHydratedRebasingCommits() ([]*models.Commit, error)
|
|||
return nil, err
|
||||
}
|
||||
|
||||
findFullCommit := lo.Ternary(self.version.IsOlderThan(2, 25, 2),
|
||||
findFullCommit := lo.Ternary(todoFileHasShortHashes,
|
||||
func(hash string) *models.Commit {
|
||||
for s, c := range fullCommits {
|
||||
if strings.HasPrefix(s, hash) {
|
||||
|
@ -286,8 +313,8 @@ func (self *CommitLoader) getHydratedRebasingCommits() ([]*models.Commit, error)
|
|||
return fullCommits[hash]
|
||||
})
|
||||
|
||||
hydratedCommits := make([]*models.Commit, 0, len(commits))
|
||||
for _, rebasingCommit := range commits {
|
||||
hydratedCommits := make([]*models.Commit, 0, len(todoCommits))
|
||||
for _, rebasingCommit := range todoCommits {
|
||||
if rebasingCommit.Hash == "" {
|
||||
hydratedCommits = append(hydratedCommits, rebasingCommit)
|
||||
} else if commit := findFullCommit(rebasingCommit.Hash); commit != nil {
|
||||
|
@ -304,7 +331,7 @@ func (self *CommitLoader) getHydratedRebasingCommits() ([]*models.Commit, error)
|
|||
// git-rebase-todo example:
|
||||
// pick ac446ae94ee560bdb8d1d057278657b251aaef17 ac446ae
|
||||
// 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"))
|
||||
if err != nil {
|
||||
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
|
||||
// so, add a fake entry for it
|
||||
if conflictedCommitHash := self.getConflictedCommit(todos); conflictedCommitHash != "" {
|
||||
commits = append(commits, &models.Commit{
|
||||
Hash: conflictedCommitHash,
|
||||
Name: "",
|
||||
Status: models.StatusRebasing,
|
||||
Action: models.ActionConflict,
|
||||
})
|
||||
if addConflictingCommit {
|
||||
if conflictedCommit := self.getConflictedCommit(todos); conflictedCommit != nil {
|
||||
commits = append(commits, conflictedCommit)
|
||||
}
|
||||
}
|
||||
|
||||
for _, t := range todos {
|
||||
|
@ -351,36 +375,34 @@ func (self *CommitLoader) getRebasingCommits() []*models.Commit {
|
|||
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"))
|
||||
if err != nil {
|
||||
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())
|
||||
if err != nil {
|
||||
self.Log.Error(fmt.Sprintf("error occurred while parsing rebase-merge/done file: %s", err.Error()))
|
||||
return ""
|
||||
return nil
|
||||
}
|
||||
|
||||
amendFileExists := false
|
||||
if _, err := os.Stat(filepath.Join(self.repoPaths.WorktreeGitDirPath(), "rebase-merge/amend")); err == nil {
|
||||
amendFileExists = true
|
||||
}
|
||||
amendFileExists, _ := self.os.FileExists(filepath.Join(self.repoPaths.WorktreeGitDirPath(), "rebase-merge/amend"))
|
||||
messageFileExists, _ := self.os.FileExists(filepath.Join(self.repoPaths.WorktreeGitDirPath(), "rebase-merge/message"))
|
||||
|
||||
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:
|
||||
if len(doneTodos) == 0 {
|
||||
self.Log.Error("no done entries in rebase-merge/done file")
|
||||
return ""
|
||||
return nil
|
||||
}
|
||||
lastTodo := doneTodos[len(doneTodos)-1]
|
||||
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
|
||||
|
@ -391,7 +413,7 @@ func (self *CommitLoader) getConflictedCommitImpl(todos []todo.Todo, doneTodos [
|
|||
// same, the command was rescheduled.
|
||||
if len(doneTodos) > 0 && len(todos) > 0 && doneTodos[len(doneTodos)-1] == todos[0] {
|
||||
// Command was rescheduled, no need to display it
|
||||
return ""
|
||||
return nil
|
||||
}
|
||||
|
||||
// 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] &&
|
||||
doneTodos[len(doneTodos)-1] == doneTodos[len(doneTodos)-3] {
|
||||
// Command was rescheduled, no need to display it
|
||||
return ""
|
||||
return nil
|
||||
}
|
||||
|
||||
if lastTodo.Command == todo.Edit {
|
||||
if amendFileExists {
|
||||
// Special case for "edit": if the "amend" file exists, the "edit"
|
||||
// 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:
|
||||
if lastTodo.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
|
||||
// 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) {
|
||||
|
|
|
@ -328,18 +328,19 @@ func TestGetCommits(t *testing.T) {
|
|||
|
||||
func TestCommitLoader_getConflictedCommitImpl(t *testing.T) {
|
||||
scenarios := []struct {
|
||||
testName string
|
||||
todos []todo.Todo
|
||||
doneTodos []todo.Todo
|
||||
amendFileExists bool
|
||||
expectedHash string
|
||||
testName string
|
||||
todos []todo.Todo
|
||||
doneTodos []todo.Todo
|
||||
amendFileExists bool
|
||||
messageFileExists bool
|
||||
expectedResult *models.Commit
|
||||
}{
|
||||
{
|
||||
testName: "no done todos",
|
||||
todos: []todo.Todo{},
|
||||
doneTodos: []todo.Todo{},
|
||||
amendFileExists: false,
|
||||
expectedHash: "",
|
||||
expectedResult: nil,
|
||||
},
|
||||
{
|
||||
testName: "common case (conflict)",
|
||||
|
@ -355,7 +356,11 @@ func TestCommitLoader_getConflictedCommitImpl(t *testing.T) {
|
|||
},
|
||||
},
|
||||
amendFileExists: false,
|
||||
expectedHash: "fa1afe1",
|
||||
expectedResult: &models.Commit{
|
||||
Hash: "fa1afe1",
|
||||
Action: todo.Pick,
|
||||
Status: models.StatusConflicted,
|
||||
},
|
||||
},
|
||||
{
|
||||
testName: "last command was 'break'",
|
||||
|
@ -364,7 +369,7 @@ func TestCommitLoader_getConflictedCommitImpl(t *testing.T) {
|
|||
{Command: todo.Break},
|
||||
},
|
||||
amendFileExists: false,
|
||||
expectedHash: "",
|
||||
expectedResult: nil,
|
||||
},
|
||||
{
|
||||
testName: "last command was 'exec'",
|
||||
|
@ -376,7 +381,7 @@ func TestCommitLoader_getConflictedCommitImpl(t *testing.T) {
|
|||
},
|
||||
},
|
||||
amendFileExists: false,
|
||||
expectedHash: "",
|
||||
expectedResult: nil,
|
||||
},
|
||||
{
|
||||
testName: "last command was 'reword'",
|
||||
|
@ -385,7 +390,7 @@ func TestCommitLoader_getConflictedCommitImpl(t *testing.T) {
|
|||
{Command: todo.Reword},
|
||||
},
|
||||
amendFileExists: false,
|
||||
expectedHash: "",
|
||||
expectedResult: nil,
|
||||
},
|
||||
{
|
||||
testName: "'pick' was rescheduled",
|
||||
|
@ -402,7 +407,7 @@ func TestCommitLoader_getConflictedCommitImpl(t *testing.T) {
|
|||
},
|
||||
},
|
||||
amendFileExists: false,
|
||||
expectedHash: "",
|
||||
expectedResult: nil,
|
||||
},
|
||||
{
|
||||
testName: "'pick' was rescheduled, buggy git version",
|
||||
|
@ -427,7 +432,7 @@ func TestCommitLoader_getConflictedCommitImpl(t *testing.T) {
|
|||
},
|
||||
},
|
||||
amendFileExists: false,
|
||||
expectedHash: "",
|
||||
expectedResult: nil,
|
||||
},
|
||||
{
|
||||
testName: "conflicting 'pick' after 'exec'",
|
||||
|
@ -452,7 +457,11 @@ func TestCommitLoader_getConflictedCommitImpl(t *testing.T) {
|
|||
},
|
||||
},
|
||||
amendFileExists: false,
|
||||
expectedHash: "fa1afe1",
|
||||
expectedResult: &models.Commit{
|
||||
Hash: "fa1afe1",
|
||||
Action: todo.Pick,
|
||||
Status: models.StatusConflicted,
|
||||
},
|
||||
},
|
||||
{
|
||||
testName: "'edit' with amend file",
|
||||
|
@ -464,10 +473,10 @@ func TestCommitLoader_getConflictedCommitImpl(t *testing.T) {
|
|||
},
|
||||
},
|
||||
amendFileExists: true,
|
||||
expectedHash: "",
|
||||
expectedResult: nil,
|
||||
},
|
||||
{
|
||||
testName: "'edit' without amend file",
|
||||
testName: "'edit' without amend file but message file",
|
||||
todos: []todo.Todo{},
|
||||
doneTodos: []todo.Todo{
|
||||
{
|
||||
|
@ -475,8 +484,26 @@ func TestCommitLoader_getConflictedCommitImpl(t *testing.T) {
|
|||
Commit: "fa1afe1",
|
||||
},
|
||||
},
|
||||
amendFileExists: false,
|
||||
expectedHash: "fa1afe1",
|
||||
amendFileExists: false,
|
||||
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 {
|
||||
|
@ -496,8 +523,8 @@ func TestCommitLoader_getConflictedCommitImpl(t *testing.T) {
|
|||
},
|
||||
}
|
||||
|
||||
hash := builder.getConflictedCommitImpl(scenario.todos, scenario.doneTodos, scenario.amendFileExists)
|
||||
assert.Equal(t, scenario.expectedHash, hash)
|
||||
hash := builder.getConflictedCommitImpl(scenario.todos, scenario.doneTodos, scenario.amendFileExists, scenario.messageFileExists)
|
||||
assert.Equal(t, scenario.expectedResult, hash)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -18,7 +18,7 @@ const (
|
|||
StatusPushed
|
||||
StatusMerged
|
||||
StatusRebasing
|
||||
StatusSelected
|
||||
StatusConflicted
|
||||
StatusReflog
|
||||
)
|
||||
|
||||
|
@ -26,8 +26,6 @@ const (
|
|||
// 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
|
||||
ActionNone todo.TodoCommand = 0
|
||||
// "Comment" is the last one of the todo package's enum entries
|
||||
ActionConflict = todo.Comment + 1
|
||||
)
|
||||
|
||||
type Divergence int
|
||||
|
|
|
@ -829,12 +829,12 @@ func (self *FilesController) handleAmendCommitPress() error {
|
|||
func (self *FilesController) isResolvingConflicts() bool {
|
||||
commits := self.c.Model().Commits
|
||||
for _, c := range commits {
|
||||
if c.Status != models.StatusRebasing {
|
||||
break
|
||||
}
|
||||
if c.Action == models.ActionConflict {
|
||||
if c.Status == models.StatusConflicted {
|
||||
return true
|
||||
}
|
||||
if !c.IsTODO() {
|
||||
break
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
|
|
@ -122,7 +122,7 @@ func (self *MergeAndRebaseHelper) genericMergeCommand(command string) error {
|
|||
|
||||
func (self *MergeAndRebaseHelper) hasExecTodos() bool {
|
||||
for _, commit := range self.c.Model().Commits {
|
||||
if commit.Status != models.StatusRebasing {
|
||||
if !commit.IsTODO() {
|
||||
break
|
||||
}
|
||||
if commit.Action == todo.Exec {
|
||||
|
|
|
@ -684,6 +684,11 @@ func (self *LocalCommitsController) isRebasing() bool {
|
|||
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 {
|
||||
if self.isRebasing() {
|
||||
if err := self.c.Git().Rebase.MoveTodosDown(selectedCommits); err != nil {
|
||||
|
@ -1389,7 +1394,7 @@ func (self *LocalCommitsController) canMoveDown(selectedCommits []*models.Commit
|
|||
if self.isRebasing() {
|
||||
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}
|
||||
}
|
||||
}
|
||||
|
@ -1405,7 +1410,7 @@ func (self *LocalCommitsController) canMoveUp(selectedCommits []*models.Commit,
|
|||
if self.isRebasing() {
|
||||
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}
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
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() {
|
||||
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
|
||||
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 lo.SomeBy(selectedCommits, func(c *models.Commit) bool { return c.IsMerge() }) {
|
||||
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 {
|
||||
if self.isCherryPickingOrReverting() {
|
||||
return &types.DisabledReason{Text: self.c.Tr.NotAllowedMidCherryPickOrRevert}
|
||||
}
|
||||
|
||||
if !self.isRebasing() {
|
||||
if len(selectedCommits) > 1 && lo.SomeBy(selectedCommits, func(c *models.Commit) bool { return c.IsMerge() }) {
|
||||
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 {
|
||||
if self.isCherryPickingOrReverting() {
|
||||
return &types.DisabledReason{Text: self.c.Tr.NotAllowedMidCherryPickOrRevert}
|
||||
}
|
||||
|
||||
if !self.isRebasing() {
|
||||
// if not rebasing, we're going to do a pull so we don't care about the selection
|
||||
return nil
|
||||
|
|
|
@ -186,7 +186,7 @@ func GetCommitListDisplayStrings(
|
|||
unfilteredIdx := i + startIdx
|
||||
bisectStatus = getBisectStatus(unfilteredIdx, commit.Hash, bisectInfo, bisectBounds)
|
||||
isYouAreHereCommit := false
|
||||
if showYouAreHereLabel && (commit.Action == models.ActionConflict || unfilteredIdx == rebaseOffset) {
|
||||
if showYouAreHereLabel && (commit.Status == models.StatusConflicted || unfilteredIdx == rebaseOffset) {
|
||||
isYouAreHereCommit = true
|
||||
showYouAreHereLabel = false
|
||||
}
|
||||
|
@ -395,8 +395,7 @@ func displayCommit(
|
|||
|
||||
actionString := ""
|
||||
if commit.Action != models.ActionNone {
|
||||
todoString := lo.Ternary(commit.Action == models.ActionConflict, "conflict", commit.Action.String())
|
||||
actionString = actionColorMap(commit.Action).Sprint(todoString) + " "
|
||||
actionString = actionColorMap(commit.Action, commit.Status).Sprint(commit.Action.String()) + " "
|
||||
}
|
||||
|
||||
tagString := ""
|
||||
|
@ -429,8 +428,13 @@ func displayCommit(
|
|||
|
||||
mark := ""
|
||||
if isYouAreHereCommit {
|
||||
color := lo.Ternary(commit.Action == models.ActionConflict, style.FgRed, style.FgYellow)
|
||||
youAreHere := color.Sprintf("<-- %s ---", common.Tr.YouAreHere)
|
||||
color := style.FgYellow
|
||||
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)
|
||||
} else if isMarkedBaseCommit {
|
||||
rebaseFromHere := style.FgYellow.Sprint(common.Tr.MarkedCommitMarker)
|
||||
|
@ -501,7 +505,7 @@ func getHashColor(
|
|||
hashColor = style.FgYellow
|
||||
case models.StatusMerged:
|
||||
hashColor = style.FgGreen
|
||||
case models.StatusRebasing:
|
||||
case models.StatusRebasing, models.StatusConflicted:
|
||||
hashColor = style.FgBlue
|
||||
case models.StatusReflog:
|
||||
hashColor = style.FgBlue
|
||||
|
@ -519,7 +523,11 @@ func getHashColor(
|
|||
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 {
|
||||
case todo.Pick:
|
||||
return style.FgCyan
|
||||
|
@ -529,8 +537,6 @@ func actionColorMap(action todo.TodoCommand) style.TextStyle {
|
|||
return style.FgGreen
|
||||
case todo.Fixup:
|
||||
return style.FgMagenta
|
||||
case models.ActionConflict:
|
||||
return style.FgRed
|
||||
default:
|
||||
return style.FgYellow
|
||||
}
|
||||
|
|
|
@ -349,9 +349,11 @@ type TranslationSet struct {
|
|||
ErrorOccurred string
|
||||
NoRoom string
|
||||
YouAreHere string
|
||||
ConflictLabel string
|
||||
YouDied string
|
||||
RewordNotSupported string
|
||||
ChangingThisActionIsNotAllowed string
|
||||
NotAllowedMidCherryPickOrRevert string
|
||||
DroppingMergeRequiresSingleSelection string
|
||||
CherryPickCopy string
|
||||
CherryPickCopyTooltip string
|
||||
|
@ -1416,9 +1418,11 @@ func EnglishTranslationSet() *TranslationSet {
|
|||
ErrorOccurred: "An error occurred! Please create an issue at",
|
||||
NoRoom: "Not enough room",
|
||||
YouAreHere: "YOU ARE HERE",
|
||||
ConflictLabel: "CONFLICT",
|
||||
YouDied: "YOU DIED!",
|
||||
RewordNotSupported: "Rewording commits while interactively rebasing is not currently supported",
|
||||
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",
|
||||
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.",
|
||||
|
|
|
@ -53,7 +53,7 @@ var RebaseAndDrop = NewIntegrationTest(NewIntegrationTestArgs{
|
|||
TopLines(
|
||||
MatchesRegexp(`pick.*to keep`).IsSelected(),
|
||||
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"),
|
||||
MatchesRegexp("original"),
|
||||
|
@ -63,7 +63,7 @@ var RebaseAndDrop = NewIntegrationTest(NewIntegrationTestArgs{
|
|||
TopLines(
|
||||
MatchesRegexp(`pick.*to keep`),
|
||||
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"),
|
||||
MatchesRegexp("original"),
|
||||
|
|
|
@ -30,7 +30,7 @@ var AmendWhenThereAreConflictsAndAmend = NewIntegrationTest(NewIntegrationTestAr
|
|||
Focus().
|
||||
Lines(
|
||||
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("file1 changed in master"),
|
||||
Contains("base commit"),
|
||||
|
|
|
@ -34,7 +34,7 @@ var AmendWhenThereAreConflictsAndCancel = NewIntegrationTest(NewIntegrationTestA
|
|||
Focus().
|
||||
Lines(
|
||||
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("file1 changed in master"),
|
||||
Contains("base commit"),
|
||||
|
|
|
@ -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"),
|
||||
)
|
||||
},
|
||||
})
|
|
@ -39,16 +39,10 @@ var RevertWithConflictSingleCommit = NewIntegrationTest(NewIntegrationTestArgs{
|
|||
Confirm()
|
||||
}).
|
||||
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("CI ◯ add second line"),
|
||||
Contains("CI ◯ add first line"),
|
||||
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"))
|
||||
|
|
|
@ -44,7 +44,7 @@ func doTheRebaseForAmendTests(t *TestDriver, keys config.KeybindingConfig) {
|
|||
t.Views().Commits().
|
||||
Lines(
|
||||
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("file1 changed in master"),
|
||||
Contains("base commit"),
|
||||
|
|
|
@ -35,7 +35,7 @@ var AmendCommitWithConflict = NewIntegrationTest(NewIntegrationTestArgs{
|
|||
}).
|
||||
Lines(
|
||||
Contains("pick").Contains("three"),
|
||||
Contains("conflict").Contains("<-- YOU ARE HERE --- fixup! two"),
|
||||
Contains("fixup").Contains("<-- CONFLICT --- fixup! two"),
|
||||
Contains("two"),
|
||||
Contains("one"),
|
||||
)
|
||||
|
@ -66,7 +66,7 @@ var AmendCommitWithConflict = NewIntegrationTest(NewIntegrationTestArgs{
|
|||
|
||||
t.Views().Commits().
|
||||
Lines(
|
||||
Contains("<-- YOU ARE HERE --- three"),
|
||||
Contains("<-- CONFLICT --- three"),
|
||||
Contains("two"),
|
||||
Contains("one"),
|
||||
)
|
||||
|
|
|
@ -33,10 +33,10 @@ var EditTheConflCommit = NewIntegrationTest(NewIntegrationTestArgs{
|
|||
Focus().
|
||||
Lines(
|
||||
Contains("pick").Contains("commit two"),
|
||||
Contains("conflict").Contains("<-- YOU ARE HERE --- commit three"),
|
||||
Contains("pick").Contains("<-- CONFLICT --- commit three"),
|
||||
Contains("commit one"),
|
||||
).
|
||||
NavigateToLine(Contains("<-- YOU ARE HERE --- commit three")).
|
||||
NavigateToLine(Contains("<-- CONFLICT --- commit three")).
|
||||
Press(keys.Commits.RenameCommit)
|
||||
|
||||
t.ExpectToast(Contains("Disabled: Rewording commits while interactively rebasing is not currently supported"))
|
||||
|
|
|
@ -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"),
|
||||
)
|
||||
},
|
||||
})
|
|
@ -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"),
|
||||
)
|
||||
},
|
||||
})
|
|
@ -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"),
|
||||
)
|
||||
},
|
||||
})
|
|
@ -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"),
|
||||
)
|
||||
},
|
||||
})
|
|
@ -4,13 +4,13 @@ import (
|
|||
. "github.com/jesseduffield/lazygit/pkg/integration/components"
|
||||
)
|
||||
|
||||
func handleConflictsFromSwap(t *TestDriver) {
|
||||
func handleConflictsFromSwap(t *TestDriver, expectedCommand string) {
|
||||
t.Common().AcknowledgeConflicts()
|
||||
|
||||
t.Views().Commits().
|
||||
Lines(
|
||||
Contains("pick").Contains("commit two"),
|
||||
Contains("conflict").Contains("<-- YOU ARE HERE --- commit three"),
|
||||
Contains(expectedCommand).Contains("<-- CONFLICT --- commit three"),
|
||||
Contains("commit one"),
|
||||
)
|
||||
|
||||
|
|
|
@ -44,6 +44,6 @@ var SwapInRebaseWithConflict = NewIntegrationTest(NewIntegrationTestArgs{
|
|||
t.Common().ContinueRebase()
|
||||
})
|
||||
|
||||
handleConflictsFromSwap(t)
|
||||
handleConflictsFromSwap(t, "pick")
|
||||
},
|
||||
})
|
||||
|
|
|
@ -47,6 +47,6 @@ var SwapInRebaseWithConflictAndEdit = NewIntegrationTest(NewIntegrationTestArgs{
|
|||
t.Common().ContinueRebase()
|
||||
})
|
||||
|
||||
handleConflictsFromSwap(t)
|
||||
handleConflictsFromSwap(t, "edit")
|
||||
},
|
||||
})
|
||||
|
|
|
@ -28,6 +28,6 @@ var SwapWithConflict = NewIntegrationTest(NewIntegrationTestArgs{
|
|||
).
|
||||
Press(keys.Commits.MoveDownCommit)
|
||||
|
||||
handleConflictsFromSwap(t)
|
||||
handleConflictsFromSwap(t, "pick")
|
||||
},
|
||||
})
|
||||
|
|
|
@ -49,7 +49,7 @@ var PullRebaseInteractiveConflict = NewIntegrationTest(NewIntegrationTestArgs{
|
|||
t.Views().Commits().
|
||||
Lines(
|
||||
Contains("pick").Contains("five"),
|
||||
Contains("conflict").Contains("YOU ARE HERE").Contains("four"),
|
||||
Contains("pick").Contains("CONFLICT").Contains("four"),
|
||||
Contains("three"),
|
||||
Contains("two"),
|
||||
Contains("one"),
|
||||
|
|
|
@ -50,7 +50,7 @@ var PullRebaseInteractiveConflictDrop = NewIntegrationTest(NewIntegrationTestArg
|
|||
Focus().
|
||||
Lines(
|
||||
Contains("pick").Contains("five").IsSelected(),
|
||||
Contains("conflict").Contains("YOU ARE HERE").Contains("four"),
|
||||
Contains("pick").Contains("CONFLICT").Contains("four"),
|
||||
Contains("three"),
|
||||
Contains("two"),
|
||||
Contains("one"),
|
||||
|
@ -58,7 +58,7 @@ var PullRebaseInteractiveConflictDrop = NewIntegrationTest(NewIntegrationTestArg
|
|||
Press(keys.Universal.Remove).
|
||||
Lines(
|
||||
Contains("drop").Contains("five").IsSelected(),
|
||||
Contains("conflict").Contains("YOU ARE HERE").Contains("four"),
|
||||
Contains("pick").Contains("CONFLICT").Contains("four"),
|
||||
Contains("three"),
|
||||
Contains("two"),
|
||||
Contains("one"),
|
||||
|
|
|
@ -128,6 +128,7 @@ var tests = []*components.IntegrationTest{
|
|||
commit.ResetAuthorRange,
|
||||
commit.Revert,
|
||||
commit.RevertMerge,
|
||||
commit.RevertWithConflictMultipleCommits,
|
||||
commit.RevertWithConflictSingleCommit,
|
||||
commit.Reword,
|
||||
commit.Search,
|
||||
|
@ -249,6 +250,7 @@ var tests = []*components.IntegrationTest{
|
|||
interactive_rebase.FixupFirstCommit,
|
||||
interactive_rebase.FixupSecondCommit,
|
||||
interactive_rebase.InteractiveRebaseOfCopiedBranch,
|
||||
interactive_rebase.InteractiveRebaseWithConflictForEditCommand,
|
||||
interactive_rebase.MidRebaseRangeSelect,
|
||||
interactive_rebase.Move,
|
||||
interactive_rebase.MoveAcrossBranchBoundaryOutsideRebase,
|
||||
|
@ -262,6 +264,9 @@ var tests = []*components.IntegrationTest{
|
|||
interactive_rebase.QuickStartKeepSelectionRange,
|
||||
interactive_rebase.Rebase,
|
||||
interactive_rebase.RebaseWithCommitThatBecomesEmpty,
|
||||
interactive_rebase.RevertDuringRebaseWhenStoppedOnEdit,
|
||||
interactive_rebase.RevertMultipleCommitsInInteractiveRebase,
|
||||
interactive_rebase.RevertSingleCommitInInteractiveRebase,
|
||||
interactive_rebase.RewordCommitWithEditorAndFail,
|
||||
interactive_rebase.RewordFirstCommit,
|
||||
interactive_rebase.RewordLastCommit,
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue