Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions internal/engine/state.go
Original file line number Diff line number Diff line change
Expand Up @@ -433,6 +433,9 @@ func (c *conflictState) applyClassification(resolution markers.Resolution, unres
c.manual = manual
c.labelKnown = known
if known {
if labels.BaseLabel == "" {
labels.BaseLabel = c.canonical.BaseLabel
}
c.labels = labels
} else {
c.labels = ConflictLabels{
Expand Down
42 changes: 42 additions & 0 deletions internal/engine/state_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -484,6 +484,48 @@ func TestImportMergedPreservesTextBetweenAdjacentConflictsAfterResolve(t *testin
}
}

func TestImportMergedPreservesCanonicalBaseLabelForTwoWayConflict(t *testing.T) {
input := []byte("intro\n<<<<<<< HEAD\nours line\n||||||| base-commit\n=======\ntheirs line\n>>>>>>> feature\noutro\n")
doc, err := markers.Parse(input)
if err != nil {
t.Fatalf("Parse failed: %v", err)
}
state, err := NewState(doc)
if err != nil {
t.Fatalf("NewState failed: %v", err)
}
merged := []byte("intro\n<<<<<<< ours-label\nours line\n=======\ntheirs line\n>>>>>>> theirs-label\noutro\n")
if err := state.ImportMerged(merged); err != nil {
t.Fatalf("ImportMerged failed: %v", err)
}

updated := state.Document()
seg, ok := updated.Segments[updated.Conflicts[0].SegmentIndex].(markers.ConflictSegment)
if !ok {
t.Fatalf("segment is %T, want ConflictSegment", updated.Segments[updated.Conflicts[0].SegmentIndex])
}
if len(seg.Base) != 0 {
t.Fatalf("Base = %q, want empty", string(seg.Base))
}
if seg.BaseLabel != "base-commit" {
t.Fatalf("BaseLabel = %q, want %q", seg.BaseLabel, "base-commit")
}
if err := ValidateBaseCompleteness(updated); err != nil {
t.Fatalf("ValidateBaseCompleteness failed: %v", err)
}

labels, known := state.MergedLabels()
if !known[0] {
t.Fatalf("MergedLabels known = false, want true")
}
if labels[0].OursLabel != "ours-label" || labels[0].TheirsLabel != "theirs-label" {
t.Fatalf("MergedLabels = %+v", labels[0])
}
if labels[0].BaseLabel != "base-commit" {
t.Fatalf("MergedLabels BaseLabel = %q, want %q", labels[0].BaseLabel, "base-commit")
}
}

func TestImportMergedRejectsReorderedSeparatedConflicts(t *testing.T) {
input := []byte("<<<<<<< left-one\nours1\n=======\ntheirs1\n>>>>>>> right-one\n<<<<<<< left-two\nours2\n=======\ntheirs2\n>>>>>>> right-two\n")
doc, err := markers.Parse(input)
Expand Down
1 change: 1 addition & 0 deletions internal/tui/tui.go
Original file line number Diff line number Diff line change
Expand Up @@ -412,6 +412,7 @@ func shouldAllowMissingBaseFallback(ctx context.Context, opts cli.Options, valid
if validationErr == nil || !strings.Contains(validationErr.Error(), "missing base chunk") {
return false
}

if !isTrulyMissingBasePath(opts.BasePath) {
return false
}
Expand Down
61 changes: 61 additions & 0 deletions internal/tui/tui_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -845,6 +845,67 @@ func TestReloadFromFileKeepsExistingUndoHistory(t *testing.T) {
}
}

func TestReloadFromFileAllowsTwoWayMergedConflictWhenCanonicalBaseLabelExists(t *testing.T) {
ctx := context.Background()
tmpDir := t.TempDir()

basePath := filepath.Join(tmpDir, "base.txt")
localPath := filepath.Join(tmpDir, "local.txt")
remotePath := filepath.Join(tmpDir, "remote.txt")
mergedPath := filepath.Join(tmpDir, "merged.txt")

if err := os.WriteFile(basePath, []byte("intro\noutro\n"), 0o644); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(localPath, []byte("intro\nours line\noutro\n"), 0o644); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(remotePath, []byte("intro\ntheirs line\noutro\n"), 0o644); err != nil {
t.Fatal(err)
}
mergedContent := "intro\n<<<<<<< ours-label\nours line\n=======\ntheirs line\n>>>>>>> theirs-label\noutro\n"
if err := os.WriteFile(mergedPath, []byte(mergedContent), 0o644); err != nil {
t.Fatal(err)
}

diff3Bytes, err := gitmerge.MergeFileDiff3(ctx, localPath, basePath, remotePath)
if err != nil {
t.Fatalf("MergeFileDiff3 failed: %v", err)
}
doc, err := markers.Parse(diff3Bytes)
if err != nil {
t.Fatalf("Parse error = %v", err)
}
state, err := engine.NewState(doc)
if err != nil {
t.Fatalf("NewState error = %v", err)
}

m := model{
ctx: ctx,
opts: cli.Options{BasePath: basePath, LocalPath: localPath, RemotePath: remotePath, MergedPath: mergedPath},
state: state,
doc: doc,
}

if err := m.reloadFromFile(); err != nil {
t.Fatalf("reloadFromFile error = %v", err)
}
seg := conflictSegment(t, m.doc, 0)
if len(seg.Base) != 0 {
t.Fatalf("seg.Base = %q, want empty", string(seg.Base))
}
if seg.BaseLabel == "" {
t.Fatal("seg.BaseLabel = empty, want preserved canonical base label")
}
if !m.mergedLabelKnown[0] {
t.Fatalf("mergedLabelKnown[0] = false, want true")
}
if m.mergedLabels[0].OursLabel != "ours-label" || m.mergedLabels[0].TheirsLabel != "theirs-label" {
t.Fatalf("mergedLabels[0] = %+v", m.mergedLabels[0])
}
}

func TestModelInitReturnsNil(t *testing.T) {
if cmd := (model{}).Init(); cmd != nil {
t.Fatalf("Init() = %v, want nil", cmd)
Expand Down
Loading