Skip to content

Commit 7f2b723

Browse files
authored
Merge pull request #7 from chojs23/allow-missing-base
Fix : Allow missing base
2 parents 966087d + 1172828 commit 7f2b723

File tree

4 files changed

+316
-19
lines changed

4 files changed

+316
-19
lines changed

internal/run/select.go

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

1313
"github.com/chojs23/ec/internal/cli"
14-
"github.com/chojs23/ec/internal/engine"
1514
"github.com/chojs23/ec/internal/gitutil"
1615
"github.com/chojs23/ec/internal/tui"
1716
)
@@ -121,7 +120,7 @@ func selectPath(paths []string) (string, error) {
121120

122121
func selectPathInteractive(ctx context.Context, repoRoot string, paths []string) (string, error) {
123122
if isInteractiveTTY() {
124-
candidates, err := buildFileCandidates(repoRoot, paths)
123+
candidates, err := buildFileCandidates(ctx, repoRoot, paths)
125124
if err != nil {
126125
return "", err
127126
}
@@ -142,22 +141,25 @@ func isTTY(file *os.File) bool {
142141
return (info.Mode() & os.ModeCharDevice) != 0
143142
}
144143

145-
func buildFileCandidates(repoRoot string, paths []string) ([]tui.FileCandidate, error) {
144+
func buildFileCandidates(ctx context.Context, repoRoot string, paths []string) ([]tui.FileCandidate, error) {
146145
candidates := make([]tui.FileCandidate, 0, len(paths))
147146
for _, path := range paths {
148-
mergedPath := path
149-
if !filepath.IsAbs(mergedPath) {
150-
mergedPath = filepath.Join(repoRoot, path)
151-
}
152-
resolved, err := engine.CheckResolvedFile(mergedPath)
153-
if err != nil {
154-
return nil, fmt.Errorf("check resolved %s: %w", path, err)
155-
}
147+
resolved := !hasUnmergedStages(ctx, repoRoot, path)
156148
candidates = append(candidates, tui.FileCandidate{Path: path, Resolved: resolved})
157149
}
158150
return candidates, nil
159151
}
160152

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+
161163
func writeTempStages(base, local, remote []byte) (string, string, string, func(), error) {
162164
baseFile, err := os.CreateTemp("", "ec-base-*")
163165
if err != nil {

internal/run/select_test.go

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

119-
candidates, err := buildFileCandidates(tmpDir, []string{"resolved.txt", "unresolved.txt"})
119+
candidates, err := buildFileCandidates(context.Background(), 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 {
127-
t.Fatalf("resolved file marked unresolved")
126+
if !candidates[0].Resolved || !candidates[1].Resolved {
127+
t.Fatalf("expected non-repo candidates to default to resolved status")
128128
}
129-
if candidates[1].Resolved {
130-
t.Fatalf("unresolved file marked resolved")
129+
}
130+
131+
func TestBuildFileCandidatesDoesNotFailOnMalformedMergedFile(t *testing.T) {
132+
if testing.Short() {
133+
t.Skip("skipping git integration test in short mode")
134+
}
135+
if _, err := exec.LookPath("git"); err != nil {
136+
t.Skip("git not found in PATH")
137+
}
138+
139+
repoDir := t.TempDir()
140+
runGit(t, repoDir, "init")
141+
runGit(t, repoDir, "config", "user.email", "test@example.com")
142+
runGit(t, repoDir, "config", "user.name", "Test User")
143+
144+
conflictPath := filepath.Join(repoDir, "conflict.txt")
145+
if err := os.WriteFile(conflictPath, []byte("base\n"), 0o644); err != nil {
146+
t.Fatalf("write base: %v", err)
147+
}
148+
runGit(t, repoDir, "add", "conflict.txt")
149+
runGit(t, repoDir, "commit", "-m", "base")
150+
151+
runGit(t, repoDir, "checkout", "-b", "feature")
152+
if err := os.WriteFile(conflictPath, []byte("theirs\n"), 0o644); err != nil {
153+
t.Fatalf("write theirs: %v", err)
154+
}
155+
runGit(t, repoDir, "add", "conflict.txt")
156+
runGit(t, repoDir, "commit", "-m", "theirs")
157+
158+
runGit(t, repoDir, "checkout", "-")
159+
if err := os.WriteFile(conflictPath, []byte("ours\n"), 0o644); err != nil {
160+
t.Fatalf("write ours: %v", err)
161+
}
162+
runGit(t, repoDir, "add", "conflict.txt")
163+
runGit(t, repoDir, "commit", "-m", "ours")
164+
165+
mergeCmd := exec.Command("git", "merge", "feature")
166+
mergeCmd.Dir = repoDir
167+
if output, err := mergeCmd.CombinedOutput(); err == nil {
168+
t.Fatalf("expected merge conflict, got success: %s", string(output))
169+
}
170+
171+
if err := os.WriteFile(conflictPath, []byte("<<<<<<< HEAD\nours\n=======\ntheirs\n>>>>\n"), 0o644); err != nil {
172+
t.Fatalf("write malformed conflict file: %v", err)
173+
}
174+
175+
candidates, err := buildFileCandidates(context.Background(), repoDir, []string{"conflict.txt"})
176+
if err != nil {
177+
t.Fatalf("buildFileCandidates error: %v", err)
178+
}
179+
if len(candidates) != 1 {
180+
t.Fatalf("candidates len = %d, want 1", len(candidates))
181+
}
182+
if candidates[0].Resolved {
183+
t.Fatalf("expected malformed merged conflict to remain unresolved based on index stages")
131184
}
132185
}
133186

internal/tui/tui.go

Lines changed: 84 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import (
1717
"github.com/chojs23/ec/internal/cli"
1818
"github.com/chojs23/ec/internal/engine"
1919
"github.com/chojs23/ec/internal/gitmerge"
20+
"github.com/chojs23/ec/internal/gitutil"
2021
"github.com/chojs23/ec/internal/markers"
2122
)
2223

@@ -222,7 +223,11 @@ func Run(ctx context.Context, opts cli.Options) error {
222223
// Validate base completeness unless explicitly allowed to proceed without it.
223224
if !opts.AllowMissingBase {
224225
if err := engine.ValidateBaseCompleteness(doc); err != nil {
225-
return fmt.Errorf("base validation failed: %w", err)
226+
if shouldAllowMissingBaseFallback(ctx, opts, err) {
227+
opts.AllowMissingBase = true
228+
} else {
229+
return fmt.Errorf("base validation failed: %w", err)
230+
}
226231
}
227232
}
228233

@@ -361,8 +366,14 @@ func (m *model) reloadFromFile() error {
361366
return fmt.Errorf("parse diff3 view: %w", err)
362367
}
363368

364-
if err := engine.ValidateBaseCompleteness(doc); err != nil {
365-
return fmt.Errorf("base validation failed: %w", err)
369+
if !m.opts.AllowMissingBase {
370+
if err := engine.ValidateBaseCompleteness(doc); err != nil {
371+
if shouldAllowMissingBaseFallback(m.ctx, m.opts, err) {
372+
m.opts.AllowMissingBase = true
373+
} else {
374+
return fmt.Errorf("base validation failed: %w", err)
375+
}
376+
}
366377
}
367378

368379
updated, manual, err := applyMergedResolutions(doc, editedBytes)
@@ -414,6 +425,76 @@ func prepareFullDiff(doc markers.Document, opts cli.Options) ([]string, []string
414425
return baseLines, oursLines, theirsLines, ranges, true
415426
}
416427

428+
func shouldAllowMissingBaseFallback(ctx context.Context, opts cli.Options, validationErr error) bool {
429+
if validationErr == nil || !strings.Contains(validationErr.Error(), "missing base chunk") {
430+
return false
431+
}
432+
if !isTrulyMissingBasePath(opts.BasePath) {
433+
return false
434+
}
435+
436+
missingStage, determined := isTrulyMissingBaseStage(ctx, opts.MergedPath)
437+
if determined {
438+
return missingStage
439+
}
440+
441+
return true
442+
}
443+
444+
func isTrulyMissingBasePath(basePath string) bool {
445+
if basePath == "" {
446+
return false
447+
}
448+
if basePath == os.DevNull {
449+
return true
450+
}
451+
452+
info, err := os.Stat(basePath)
453+
if err != nil {
454+
return false
455+
}
456+
457+
return info.Size() == 0
458+
}
459+
460+
func isTrulyMissingBaseStage(ctx context.Context, mergedPath string) (bool, bool) {
461+
if mergedPath == "" {
462+
return false, false
463+
}
464+
465+
absMergedPath, err := filepath.Abs(mergedPath)
466+
if err != nil {
467+
return false, false
468+
}
469+
470+
repoRoot, err := gitutil.RepoRoot(ctx, filepath.Dir(absMergedPath))
471+
if err != nil {
472+
return false, false
473+
}
474+
475+
relPath, err := filepath.Rel(repoRoot, absMergedPath)
476+
if err != nil {
477+
return false, false
478+
}
479+
if relPath == ".." || strings.HasPrefix(relPath, ".."+string(filepath.Separator)) {
480+
return false, false
481+
}
482+
relPath = filepath.ToSlash(relPath)
483+
484+
if _, err := gitutil.ShowStage(ctx, repoRoot, 2, relPath); err != nil {
485+
return false, false
486+
}
487+
if _, err := gitutil.ShowStage(ctx, repoRoot, 3, relPath); err != nil {
488+
return false, false
489+
}
490+
491+
if _, err := gitutil.ShowStage(ctx, repoRoot, 1, relPath); err != nil {
492+
return true, true
493+
}
494+
495+
return false, true
496+
}
497+
417498
func loadLines(path string) ([]string, error) {
418499
bytes, err := os.ReadFile(path)
419500
if err != nil {

0 commit comments

Comments
 (0)