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

Insert fake todo entry for a conflicting commit that is being applied

When stopping in a rebase because of a conflict, it is nice to see the commit
that git is trying to apply. Create a fake todo entry labelled "conflict" for
this, and show the "<-- YOU ARE HERE ---" string for that one (in red) instead
of for the real current head.
This commit is contained in:
Stefan Haller 2023-02-26 11:51:02 +01:00
parent d66ca7751c
commit 3928d0ebda
12 changed files with 357 additions and 15 deletions

View File

@ -310,6 +310,17 @@ func (self *CommitLoader) getInteractiveRebasingCommits() ([]*models.Commit, err
return nil, nil return nil, nil
} }
// See if the current commit couldn't be applied because it conflicted; if
// so, add a fake entry for it
if conflictedCommitSha := self.getConflictedCommit(todos); conflictedCommitSha != "" {
commits = append(commits, &models.Commit{
Sha: conflictedCommitSha,
Name: "",
Status: models.StatusRebasing,
Action: models.ActionConflict,
})
}
for _, t := range todos { for _, t := range todos {
if t.Command == todo.UpdateRef { if t.Command == todo.UpdateRef {
t.Msg = strings.TrimPrefix(t.Ref, "refs/heads/") t.Msg = strings.TrimPrefix(t.Ref, "refs/heads/")
@ -328,6 +339,93 @@ func (self *CommitLoader) getInteractiveRebasingCommits() ([]*models.Commit, err
return commits, nil return commits, nil
} }
func (self *CommitLoader) getConflictedCommit(todos []todo.Todo) string {
bytesContent, err := self.readFile(filepath.Join(self.dotGitDir, "rebase-merge/done"))
if err != nil {
self.Log.Error(fmt.Sprintf("error occurred reading rebase-merge/done: %s", err.Error()))
return ""
}
doneTodos, err := todo.Parse(bytes.NewBuffer(bytesContent))
if err != nil {
self.Log.Error(fmt.Sprintf("error occurred while parsing rebase-merge/done file: %s", err.Error()))
return ""
}
amendFileExists := false
if _, err := os.Stat(filepath.Join(self.dotGitDir, "rebase-merge/amend")); err == nil {
amendFileExists = true
}
return self.getConflictedCommitImpl(todos, doneTodos, amendFileExists)
}
func (self *CommitLoader) getConflictedCommitImpl(todos []todo.Todo, doneTodos []todo.Todo, amendFileExists bool) string {
// 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 ""
}
lastTodo := doneTodos[len(doneTodos)-1]
if lastTodo.Command == todo.Break || lastTodo.Command == todo.Exec || lastTodo.Command == todo.Reword {
return ""
}
// In certain cases, git reschedules commands that failed. One example is if
// a patch would overwrite an untracked file (another one is an "exec" that
// failed, but we don't care about that here because we dealt with exec
// already above). To detect this, compare the last command of the "done"
// file against the first command of "git-rebase-todo"; if they are the
// 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 ""
}
// Older versions of git have a bug whereby, if a command is rescheduled,
// the last successful command is appended to the "done" file again. To
// detect this, we need to compare the second-to-last done entry against the
// first todo entry, and also compare the last done entry against the
// last-but-two done entry; this latter check is needed for the following
// case:
// pick A
// exec make test
// pick B
// exec make test
// If pick B fails with conflicts, then the "done" file contains
// pick A
// exec make test
// pick B
// and git-rebase-todo contains
// exec make test
// Without the last condition we would erroneously treat this as the exec
// command being rescheduled, so we wouldn't display our fake entry for
// "pick B".
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 ""
}
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 ""
}
}
// 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 ""
}
// 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
}
// assuming the file starts like this: // assuming the file starts like this:
// From e93d4193e6dd45ca9cf3a5a273d7ba6cd8b8fb20 Mon Sep 17 00:00:00 2001 // From e93d4193e6dd45ca9cf3a5a273d7ba6cd8b8fb20 Mon Sep 17 00:00:00 2001
// From: Lazygit Tester <test@example.com> // From: Lazygit Tester <test@example.com>

View File

@ -6,6 +6,7 @@ import (
"strings" "strings"
"testing" "testing"
"github.com/fsmiamoto/git-todo-parser/todo"
"github.com/jesseduffield/lazygit/pkg/commands/models" "github.com/jesseduffield/lazygit/pkg/commands/models"
"github.com/jesseduffield/lazygit/pkg/commands/oscommands" "github.com/jesseduffield/lazygit/pkg/commands/oscommands"
"github.com/jesseduffield/lazygit/pkg/commands/types/enums" "github.com/jesseduffield/lazygit/pkg/commands/types/enums"
@ -319,3 +320,179 @@ func TestGetCommits(t *testing.T) {
}) })
} }
} }
func TestCommitLoader_getConflictedCommitImpl(t *testing.T) {
scenarios := []struct {
testName string
todos []todo.Todo
doneTodos []todo.Todo
amendFileExists bool
expectedSha string
}{
{
testName: "no done todos",
todos: []todo.Todo{},
doneTodos: []todo.Todo{},
amendFileExists: false,
expectedSha: "",
},
{
testName: "common case (conflict)",
todos: []todo.Todo{},
doneTodos: []todo.Todo{
{
Command: todo.Pick,
Commit: "deadbeef",
},
{
Command: todo.Pick,
Commit: "fa1afe1",
},
},
amendFileExists: false,
expectedSha: "fa1afe1",
},
{
testName: "last command was 'break'",
todos: []todo.Todo{},
doneTodos: []todo.Todo{
{Command: todo.Break},
},
amendFileExists: false,
expectedSha: "",
},
{
testName: "last command was 'exec'",
todos: []todo.Todo{},
doneTodos: []todo.Todo{
{
Command: todo.Exec,
ExecCommand: "make test",
},
},
amendFileExists: false,
expectedSha: "",
},
{
testName: "last command was 'reword'",
todos: []todo.Todo{},
doneTodos: []todo.Todo{
{Command: todo.Reword},
},
amendFileExists: false,
expectedSha: "",
},
{
testName: "'pick' was rescheduled",
todos: []todo.Todo{
{
Command: todo.Pick,
Commit: "fa1afe1",
},
},
doneTodos: []todo.Todo{
{
Command: todo.Pick,
Commit: "fa1afe1",
},
},
amendFileExists: false,
expectedSha: "",
},
{
testName: "'pick' was rescheduled, buggy git version",
todos: []todo.Todo{
{
Command: todo.Pick,
Commit: "fa1afe1",
},
},
doneTodos: []todo.Todo{
{
Command: todo.Pick,
Commit: "deadbeaf",
},
{
Command: todo.Pick,
Commit: "fa1afe1",
},
{
Command: todo.Pick,
Commit: "deadbeaf",
},
},
amendFileExists: false,
expectedSha: "",
},
{
testName: "conflicting 'pick' after 'exec'",
todos: []todo.Todo{
{
Command: todo.Exec,
ExecCommand: "make test",
},
},
doneTodos: []todo.Todo{
{
Command: todo.Pick,
Commit: "deadbeaf",
},
{
Command: todo.Exec,
ExecCommand: "make test",
},
{
Command: todo.Pick,
Commit: "fa1afe1",
},
},
amendFileExists: false,
expectedSha: "fa1afe1",
},
{
testName: "'edit' with amend file",
todos: []todo.Todo{},
doneTodos: []todo.Todo{
{
Command: todo.Edit,
Commit: "fa1afe1",
},
},
amendFileExists: true,
expectedSha: "",
},
{
testName: "'edit' without amend file",
todos: []todo.Todo{},
doneTodos: []todo.Todo{
{
Command: todo.Edit,
Commit: "fa1afe1",
},
},
amendFileExists: false,
expectedSha: "fa1afe1",
},
}
for _, scenario := range scenarios {
t.Run(scenario.testName, func(t *testing.T) {
common := utils.NewDummyCommon()
builder := &CommitLoader{
Common: common,
cmd: oscommands.NewDummyCmdObjBuilder(oscommands.NewFakeRunner(t)),
getRebaseMode: func() (enums.RebaseMode, error) { return enums.REBASE_MODE_INTERACTIVE, nil },
dotGitDir: ".git",
readFile: func(filename string) ([]byte, error) {
return []byte(""), nil
},
walkFiles: func(root string, fn filepath.WalkFunc) error {
return nil
},
}
sha := builder.getConflictedCommitImpl(scenario.todos, scenario.doneTodos, scenario.amendFileExists)
assert.Equal(t, scenario.expectedSha, sha)
})
}
}

View File

@ -26,6 +26,8 @@ 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
) )
// Commit : A git commit // Commit : A git commit

View File

@ -385,6 +385,10 @@ func (self *LocalCommitsController) interactiveRebase(action todo.TodoCommand) e
// commit meaning you are trying to edit the todo file rather than actually // commit meaning you are trying to edit the todo file rather than actually
// begin a rebase. It then updates the todo file with that action // begin a rebase. It then updates the todo file with that action
func (self *LocalCommitsController) handleMidRebaseCommand(action todo.TodoCommand, commit *models.Commit) (bool, error) { func (self *LocalCommitsController) handleMidRebaseCommand(action todo.TodoCommand, commit *models.Commit) (bool, error) {
if commit.Action == models.ActionConflict {
return true, self.c.ErrorMsg(self.c.Tr.ChangingThisActionIsNotAllowed)
}
if !commit.IsTODO() { if !commit.IsTODO() {
if self.c.Git().Status.WorkingTreeState() != enums.REBASE_MODE_NONE { if self.c.Git().Status.WorkingTreeState() != enums.REBASE_MODE_NONE {
// If we are in a rebase, the only action that is allowed for // If we are in a rebase, the only action that is allowed for
@ -434,7 +438,7 @@ func (self *LocalCommitsController) moveDown(commit *models.Commit) error {
} }
if commit.IsTODO() { if commit.IsTODO() {
if !commits[index+1].IsTODO() { if !commits[index+1].IsTODO() || commits[index+1].Action == models.ActionConflict {
return nil return nil
} }

View File

@ -17,6 +17,7 @@ import (
"github.com/jesseduffield/lazygit/pkg/theme" "github.com/jesseduffield/lazygit/pkg/theme"
"github.com/jesseduffield/lazygit/pkg/utils" "github.com/jesseduffield/lazygit/pkg/utils"
"github.com/kyokomi/emoji/v2" "github.com/kyokomi/emoji/v2"
"github.com/samber/lo"
"github.com/sasha-s/go-deadlock" "github.com/sasha-s/go-deadlock"
) )
@ -103,7 +104,11 @@ func GetCommitListDisplayStrings(
for i, commit := range filteredCommits { for i, commit := range filteredCommits {
unfilteredIdx := i + startIdx unfilteredIdx := i + startIdx
bisectStatus = getBisectStatus(unfilteredIdx, commit.Sha, bisectInfo, bisectBounds) bisectStatus = getBisectStatus(unfilteredIdx, commit.Sha, bisectInfo, bisectBounds)
isYouAreHereCommit := showYouAreHereLabel && unfilteredIdx == rebaseOffset isYouAreHereCommit := false
if showYouAreHereLabel && (commit.Action == models.ActionConflict || unfilteredIdx == rebaseOffset) {
isYouAreHereCommit = true
showYouAreHereLabel = false
}
lines = append(lines, displayCommit( lines = append(lines, displayCommit(
common, common,
commit, commit,
@ -272,7 +277,7 @@ func displayCommit(
actionString := "" actionString := ""
if commit.Action != models.ActionNone { if commit.Action != models.ActionNone {
todoString := commit.Action.String() todoString := lo.Ternary(commit.Action == models.ActionConflict, "conflict", commit.Action.String())
actionString = actionColorMap(commit.Action).Sprint(todoString) + " " actionString = actionColorMap(commit.Action).Sprint(todoString) + " "
} }
@ -295,7 +300,8 @@ func displayCommit(
} }
if isYouAreHereCommit { if isYouAreHereCommit {
youAreHere := style.FgYellow.Sprintf("<-- %s ---", common.Tr.YouAreHere) color := lo.Ternary(commit.Action == models.ActionConflict, style.FgRed, style.FgYellow)
youAreHere := color.Sprintf("<-- %s ---", common.Tr.YouAreHere)
name = fmt.Sprintf("%s %s", youAreHere, name) name = fmt.Sprintf("%s %s", youAreHere, name)
} }
@ -391,6 +397,8 @@ 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

@ -53,7 +53,8 @@ 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("YOU ARE HERE.*second-change-branch unrelated change"), MatchesRegexp(`conflict.*YOU ARE HERE.*first change`),
MatchesRegexp("second-change-branch unrelated change"),
MatchesRegexp("second change"), MatchesRegexp("second change"),
MatchesRegexp("original"), MatchesRegexp("original"),
). ).
@ -62,7 +63,8 @@ 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("YOU ARE HERE.*second-change-branch unrelated change"), MatchesRegexp(`conflict.*YOU ARE HERE.*first change`),
MatchesRegexp("second-change-branch unrelated change"),
MatchesRegexp("second change"), MatchesRegexp("second change"),
MatchesRegexp("original"), MatchesRegexp("original"),
) )

View File

@ -35,8 +35,8 @@ var AmendCommitWithConflict = NewIntegrationTest(NewIntegrationTestArgs{
}). }).
Lines( Lines(
Contains("pick").Contains("three"), Contains("pick").Contains("three"),
// Would be nice to see "fixup! two" here, because that's what git is trying to apply right now Contains("conflict").Contains("<-- YOU ARE HERE --- fixup! two"),
Contains("<-- YOU ARE HERE --- two"), Contains("two"),
Contains("one"), Contains("one"),
) )
@ -66,8 +66,8 @@ var AmendCommitWithConflict = NewIntegrationTest(NewIntegrationTestArgs{
t.Views().Commits(). t.Views().Commits().
Lines( Lines(
// Would be nice to see "three" here, because that's what git is trying to apply right now Contains("<-- YOU ARE HERE --- three"),
Contains("<-- YOU ARE HERE --- two"), Contains("two"),
Contains("one"), Contains("one"),
) )
}, },

View File

@ -0,0 +1,47 @@
package interactive_rebase
import (
"github.com/jesseduffield/lazygit/pkg/config"
. "github.com/jesseduffield/lazygit/pkg/integration/components"
)
var EditTheConflCommit = NewIntegrationTest(NewIntegrationTestArgs{
Description: "Swap two commits, causing a conflict; then try to interact with the 'confl' commit, which results in an error.",
ExtraCmdArgs: []string{},
Skip: false,
SetupConfig: func(config *config.AppConfig) {},
SetupRepo: func(shell *Shell) {
shell.CreateFileAndAdd("myfile", "one")
shell.Commit("commit one")
shell.UpdateFileAndAdd("myfile", "two")
shell.Commit("commit two")
shell.UpdateFileAndAdd("myfile", "three")
shell.Commit("commit three")
},
Run: func(t *TestDriver, keys config.KeybindingConfig) {
t.Views().Commits().
Focus().
Lines(
Contains("commit three").IsSelected(),
Contains("commit two"),
Contains("commit one"),
).
Press(keys.Commits.MoveDownCommit).
Tap(func() {
t.Common().AcknowledgeConflicts()
}).
Focus().
Lines(
Contains("pick").Contains("commit two"),
Contains("conflict").Contains("<-- YOU ARE HERE --- commit three"),
Contains("commit one"),
).
NavigateToLine(Contains("<-- YOU ARE HERE --- commit three")).
Press(keys.Commits.RenameCommit)
t.ExpectPopup().Alert().
Title(Equals("Error")).
Content(Contains("Changing this kind of rebase todo entry is not allowed")).
Confirm()
},
})

View File

@ -10,8 +10,8 @@ func handleConflictsFromSwap(t *TestDriver) {
t.Views().Commits(). t.Views().Commits().
Lines( Lines(
Contains("pick").Contains("commit two"), Contains("pick").Contains("commit two"),
// Would be nice to see "three" here, it's the one that conflicted Contains("conflict").Contains("<-- YOU ARE HERE --- commit three"),
Contains("<-- YOU ARE HERE --- commit one"), Contains("commit one"),
) )
t.Views().Files(). t.Views().Files().

View File

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

View File

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

View File

@ -105,6 +105,7 @@ var tests = []*components.IntegrationTest{
interactive_rebase.DropTodoCommitWithUpdateRefShowBranchHeads, interactive_rebase.DropTodoCommitWithUpdateRefShowBranchHeads,
interactive_rebase.EditFirstCommit, interactive_rebase.EditFirstCommit,
interactive_rebase.EditNonTodoCommitDuringRebase, interactive_rebase.EditNonTodoCommitDuringRebase,
interactive_rebase.EditTheConflCommit,
interactive_rebase.FixupFirstCommit, interactive_rebase.FixupFirstCommit,
interactive_rebase.FixupSecondCommit, interactive_rebase.FixupSecondCommit,
interactive_rebase.Move, interactive_rebase.Move,