Skip to content

Commit a13fdf3

Browse files
authored
Merge pull request #9 from chojs23/allow-missing-base
Fix missing base chunk error when empty diff3 base with stage 1
2 parents 7f2b723 + 8750980 commit a13fdf3

File tree

4 files changed

+96
-19
lines changed

4 files changed

+96
-19
lines changed

internal/engine/base.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ func ValidateBaseCompleteness(doc markers.Document) error {
2323
if !ok {
2424
return fmt.Errorf("internal: conflict %d is not a ConflictSegment", i)
2525
}
26-
if len(seg.Base) == 0 {
26+
if len(seg.Base) == 0 && seg.BaseLabel == "" {
2727
return fmt.Errorf("conflict %d is missing base chunk (base display strategy requires exact base for all conflicts)", i)
2828
}
2929
}

internal/engine/base_test.go

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,27 @@ func TestValidateBaseCompleteness_MissingBase(t *testing.T) {
7070
}
7171
}
7272

73+
func TestValidateBaseCompleteness_EmptyBaseBodyWithLabel(t *testing.T) {
74+
doc := markers.Document{
75+
Segments: []markers.Segment{
76+
markers.ConflictSegment{
77+
Ours: []byte("ours\n"),
78+
Base: nil,
79+
BaseLabel: "/tmp/base.txt",
80+
Theirs: []byte("theirs\n"),
81+
},
82+
},
83+
Conflicts: []markers.ConflictRef{
84+
{SegmentIndex: 0},
85+
},
86+
}
87+
88+
err := ValidateBaseCompleteness(doc)
89+
if err != nil {
90+
t.Fatalf("expected no error for empty base body with base label, got: %v", err)
91+
}
92+
}
93+
7394
// TestBaseDisplayIntegration_RealGitConflict creates a real git conflict using
7495
// temp git repos and validates that the diff3 view has base chunks for all conflicts.
7596
func TestBaseDisplayIntegration_RealGitConflict(t *testing.T) {

internal/run/select.go

Lines changed: 12 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import (
1111
"strings"
1212

1313
"github.com/chojs23/ec/internal/cli"
14+
"github.com/chojs23/ec/internal/engine"
1415
"github.com/chojs23/ec/internal/gitutil"
1516
"github.com/chojs23/ec/internal/tui"
1617
)
@@ -120,7 +121,7 @@ func selectPath(paths []string) (string, error) {
120121

121122
func selectPathInteractive(ctx context.Context, repoRoot string, paths []string) (string, error) {
122123
if isInteractiveTTY() {
123-
candidates, err := buildFileCandidates(ctx, repoRoot, paths)
124+
candidates, err := buildFileCandidates(repoRoot, paths)
124125
if err != nil {
125126
return "", err
126127
}
@@ -141,25 +142,23 @@ func isTTY(file *os.File) bool {
141142
return (info.Mode() & os.ModeCharDevice) != 0
142143
}
143144

144-
func buildFileCandidates(ctx context.Context, repoRoot string, paths []string) ([]tui.FileCandidate, error) {
145+
func buildFileCandidates(repoRoot string, paths []string) ([]tui.FileCandidate, error) {
145146
candidates := make([]tui.FileCandidate, 0, len(paths))
146147
for _, path := range paths {
147-
resolved := !hasUnmergedStages(ctx, repoRoot, path)
148+
mergedPath := path
149+
if !filepath.IsAbs(mergedPath) {
150+
mergedPath = filepath.Join(repoRoot, path)
151+
}
152+
153+
resolved, err := engine.CheckResolvedFile(mergedPath)
154+
if err != nil {
155+
resolved = false
156+
}
148157
candidates = append(candidates, tui.FileCandidate{Path: path, Resolved: resolved})
149158
}
150159
return candidates, nil
151160
}
152161

153-
func hasUnmergedStages(ctx context.Context, repoRoot string, path string) bool {
154-
if _, err := gitutil.ShowStage(ctx, repoRoot, 2, path); err == nil {
155-
return true
156-
}
157-
if _, err := gitutil.ShowStage(ctx, repoRoot, 3, path); err == nil {
158-
return true
159-
}
160-
return false
161-
}
162-
163162
func writeTempStages(base, local, remote []byte) (string, string, string, func(), error) {
164163
baseFile, err := os.CreateTemp("", "ec-base-*")
165164
if err != nil {

internal/run/select_test.go

Lines changed: 62 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -116,15 +116,18 @@ func TestBuildFileCandidates(t *testing.T) {
116116
t.Fatalf("write unresolved: %v", err)
117117
}
118118

119-
candidates, err := buildFileCandidates(context.Background(), tmpDir, []string{"resolved.txt", "unresolved.txt"})
119+
candidates, err := buildFileCandidates(tmpDir, []string{"resolved.txt", "unresolved.txt"})
120120
if err != nil {
121121
t.Fatalf("buildFileCandidates error: %v", err)
122122
}
123123
if len(candidates) != 2 {
124124
t.Fatalf("candidates len = %d, want 2", len(candidates))
125125
}
126-
if !candidates[0].Resolved || !candidates[1].Resolved {
127-
t.Fatalf("expected non-repo candidates to default to resolved status")
126+
if !candidates[0].Resolved {
127+
t.Fatalf("expected marker-free file to be resolved")
128+
}
129+
if candidates[1].Resolved {
130+
t.Fatalf("expected marker-containing file to be unresolved")
128131
}
129132
}
130133

@@ -172,15 +175,69 @@ func TestBuildFileCandidatesDoesNotFailOnMalformedMergedFile(t *testing.T) {
172175
t.Fatalf("write malformed conflict file: %v", err)
173176
}
174177

175-
candidates, err := buildFileCandidates(context.Background(), repoDir, []string{"conflict.txt"})
178+
candidates, err := buildFileCandidates(repoDir, []string{"conflict.txt"})
176179
if err != nil {
177180
t.Fatalf("buildFileCandidates error: %v", err)
178181
}
179182
if len(candidates) != 1 {
180183
t.Fatalf("candidates len = %d, want 1", len(candidates))
181184
}
182185
if candidates[0].Resolved {
183-
t.Fatalf("expected malformed merged conflict to remain unresolved based on index stages")
186+
t.Fatalf("expected malformed merged conflict to remain unresolved")
187+
}
188+
}
189+
190+
func TestBuildFileCandidatesResolvedFromMergedContentWithoutGitAdd(t *testing.T) {
191+
if testing.Short() {
192+
t.Skip("skipping git integration test in short mode")
193+
}
194+
if _, err := exec.LookPath("git"); err != nil {
195+
t.Skip("git not found in PATH")
196+
}
197+
198+
repoDir := t.TempDir()
199+
runGit(t, repoDir, "init")
200+
runGit(t, repoDir, "config", "user.email", "test@example.com")
201+
runGit(t, repoDir, "config", "user.name", "Test User")
202+
203+
conflictPath := filepath.Join(repoDir, "conflict.txt")
204+
if err := os.WriteFile(conflictPath, []byte("a\nZ\nb\n"), 0o644); err != nil {
205+
t.Fatalf("write base: %v", err)
206+
}
207+
runGit(t, repoDir, "add", "conflict.txt")
208+
runGit(t, repoDir, "commit", "-m", "base")
209+
210+
runGit(t, repoDir, "checkout", "-b", "feature")
211+
if err := os.WriteFile(conflictPath, []byte("a\nY\nZ\nb\n"), 0o644); err != nil {
212+
t.Fatalf("write theirs: %v", err)
213+
}
214+
runGit(t, repoDir, "commit", "-am", "theirs")
215+
216+
runGit(t, repoDir, "checkout", "-")
217+
if err := os.WriteFile(conflictPath, []byte("a\nX\nZ\nb\n"), 0o644); err != nil {
218+
t.Fatalf("write ours: %v", err)
219+
}
220+
runGit(t, repoDir, "commit", "-am", "ours")
221+
222+
mergeCmd := exec.Command("git", "merge", "feature")
223+
mergeCmd.Dir = repoDir
224+
if output, err := mergeCmd.CombinedOutput(); err == nil {
225+
t.Fatalf("expected merge conflict, got success: %s", string(output))
226+
}
227+
228+
if err := os.WriteFile(conflictPath, []byte("a\nX\nZ\nb\n"), 0o644); err != nil {
229+
t.Fatalf("write resolved content: %v", err)
230+
}
231+
232+
candidates, err := buildFileCandidates(repoDir, []string{"conflict.txt"})
233+
if err != nil {
234+
t.Fatalf("buildFileCandidates error: %v", err)
235+
}
236+
if len(candidates) != 1 {
237+
t.Fatalf("candidates len = %d, want 1", len(candidates))
238+
}
239+
if !candidates[0].Resolved {
240+
t.Fatalf("expected resolved merged content to be shown as resolved without git add")
184241
}
185242
}
186243

0 commit comments

Comments
 (0)