Skip to content

Commit 6860fb8

Browse files
authored
Merge pull request #28 from chojs23/fix/merge-style-conflict
Preserve canonical base label when importing merge-style conflicts
2 parents 314fe57 + a66c3f2 commit 6860fb8

4 files changed

Lines changed: 107 additions & 0 deletions

File tree

internal/engine/state.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -433,6 +433,9 @@ func (c *conflictState) applyClassification(resolution markers.Resolution, unres
433433
c.manual = manual
434434
c.labelKnown = known
435435
if known {
436+
if labels.BaseLabel == "" {
437+
labels.BaseLabel = c.canonical.BaseLabel
438+
}
436439
c.labels = labels
437440
} else {
438441
c.labels = ConflictLabels{

internal/engine/state_test.go

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -484,6 +484,48 @@ func TestImportMergedPreservesTextBetweenAdjacentConflictsAfterResolve(t *testin
484484
}
485485
}
486486

487+
func TestImportMergedPreservesCanonicalBaseLabelForTwoWayConflict(t *testing.T) {
488+
input := []byte("intro\n<<<<<<< HEAD\nours line\n||||||| base-commit\n=======\ntheirs line\n>>>>>>> feature\noutro\n")
489+
doc, err := markers.Parse(input)
490+
if err != nil {
491+
t.Fatalf("Parse failed: %v", err)
492+
}
493+
state, err := NewState(doc)
494+
if err != nil {
495+
t.Fatalf("NewState failed: %v", err)
496+
}
497+
merged := []byte("intro\n<<<<<<< ours-label\nours line\n=======\ntheirs line\n>>>>>>> theirs-label\noutro\n")
498+
if err := state.ImportMerged(merged); err != nil {
499+
t.Fatalf("ImportMerged failed: %v", err)
500+
}
501+
502+
updated := state.Document()
503+
seg, ok := updated.Segments[updated.Conflicts[0].SegmentIndex].(markers.ConflictSegment)
504+
if !ok {
505+
t.Fatalf("segment is %T, want ConflictSegment", updated.Segments[updated.Conflicts[0].SegmentIndex])
506+
}
507+
if len(seg.Base) != 0 {
508+
t.Fatalf("Base = %q, want empty", string(seg.Base))
509+
}
510+
if seg.BaseLabel != "base-commit" {
511+
t.Fatalf("BaseLabel = %q, want %q", seg.BaseLabel, "base-commit")
512+
}
513+
if err := ValidateBaseCompleteness(updated); err != nil {
514+
t.Fatalf("ValidateBaseCompleteness failed: %v", err)
515+
}
516+
517+
labels, known := state.MergedLabels()
518+
if !known[0] {
519+
t.Fatalf("MergedLabels known = false, want true")
520+
}
521+
if labels[0].OursLabel != "ours-label" || labels[0].TheirsLabel != "theirs-label" {
522+
t.Fatalf("MergedLabels = %+v", labels[0])
523+
}
524+
if labels[0].BaseLabel != "base-commit" {
525+
t.Fatalf("MergedLabels BaseLabel = %q, want %q", labels[0].BaseLabel, "base-commit")
526+
}
527+
}
528+
487529
func TestImportMergedRejectsReorderedSeparatedConflicts(t *testing.T) {
488530
input := []byte("<<<<<<< left-one\nours1\n=======\ntheirs1\n>>>>>>> right-one\n<<<<<<< left-two\nours2\n=======\ntheirs2\n>>>>>>> right-two\n")
489531
doc, err := markers.Parse(input)

internal/tui/tui.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -412,6 +412,7 @@ func shouldAllowMissingBaseFallback(ctx context.Context, opts cli.Options, valid
412412
if validationErr == nil || !strings.Contains(validationErr.Error(), "missing base chunk") {
413413
return false
414414
}
415+
415416
if !isTrulyMissingBasePath(opts.BasePath) {
416417
return false
417418
}

internal/tui/tui_test.go

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -845,6 +845,67 @@ func TestReloadFromFileKeepsExistingUndoHistory(t *testing.T) {
845845
}
846846
}
847847

848+
func TestReloadFromFileAllowsTwoWayMergedConflictWhenCanonicalBaseLabelExists(t *testing.T) {
849+
ctx := context.Background()
850+
tmpDir := t.TempDir()
851+
852+
basePath := filepath.Join(tmpDir, "base.txt")
853+
localPath := filepath.Join(tmpDir, "local.txt")
854+
remotePath := filepath.Join(tmpDir, "remote.txt")
855+
mergedPath := filepath.Join(tmpDir, "merged.txt")
856+
857+
if err := os.WriteFile(basePath, []byte("intro\noutro\n"), 0o644); err != nil {
858+
t.Fatal(err)
859+
}
860+
if err := os.WriteFile(localPath, []byte("intro\nours line\noutro\n"), 0o644); err != nil {
861+
t.Fatal(err)
862+
}
863+
if err := os.WriteFile(remotePath, []byte("intro\ntheirs line\noutro\n"), 0o644); err != nil {
864+
t.Fatal(err)
865+
}
866+
mergedContent := "intro\n<<<<<<< ours-label\nours line\n=======\ntheirs line\n>>>>>>> theirs-label\noutro\n"
867+
if err := os.WriteFile(mergedPath, []byte(mergedContent), 0o644); err != nil {
868+
t.Fatal(err)
869+
}
870+
871+
diff3Bytes, err := gitmerge.MergeFileDiff3(ctx, localPath, basePath, remotePath)
872+
if err != nil {
873+
t.Fatalf("MergeFileDiff3 failed: %v", err)
874+
}
875+
doc, err := markers.Parse(diff3Bytes)
876+
if err != nil {
877+
t.Fatalf("Parse error = %v", err)
878+
}
879+
state, err := engine.NewState(doc)
880+
if err != nil {
881+
t.Fatalf("NewState error = %v", err)
882+
}
883+
884+
m := model{
885+
ctx: ctx,
886+
opts: cli.Options{BasePath: basePath, LocalPath: localPath, RemotePath: remotePath, MergedPath: mergedPath},
887+
state: state,
888+
doc: doc,
889+
}
890+
891+
if err := m.reloadFromFile(); err != nil {
892+
t.Fatalf("reloadFromFile error = %v", err)
893+
}
894+
seg := conflictSegment(t, m.doc, 0)
895+
if len(seg.Base) != 0 {
896+
t.Fatalf("seg.Base = %q, want empty", string(seg.Base))
897+
}
898+
if seg.BaseLabel == "" {
899+
t.Fatal("seg.BaseLabel = empty, want preserved canonical base label")
900+
}
901+
if !m.mergedLabelKnown[0] {
902+
t.Fatalf("mergedLabelKnown[0] = false, want true")
903+
}
904+
if m.mergedLabels[0].OursLabel != "ours-label" || m.mergedLabels[0].TheirsLabel != "theirs-label" {
905+
t.Fatalf("mergedLabels[0] = %+v", m.mergedLabels[0])
906+
}
907+
}
908+
848909
func TestModelInitReturnsNil(t *testing.T) {
849910
if cmd := (model{}).Init(); cmd != nil {
850911
t.Fatalf("Init() = %v, want nil", cmd)

0 commit comments

Comments
 (0)