Fix bd-270: Detect and handle Git merge conflicts in JSONL auto-import

- Add pre-parse merge conflict marker detection before JSON parsing
- Show clear error message when conflicts are detected
- Provide resolution instructions (Git client or bd export)
- Add TestAutoImportMergeConflict test case
- Prevents silent auto-import failures from cryptic parse errors

Amp-Thread-ID: https://ampcode.com/threads/T-d83011c0-7dfc-49c9-96c1-05c94ec2a3d3
Co-authored-by: Amp <amp@ampcode.com>
This commit is contained in:
Steve Yegge
2025-10-16 00:49:41 -07:00
parent 392379efad
commit 5e420e8ee0
3 changed files with 123 additions and 300 deletions

View File

@@ -2,6 +2,7 @@ package main
import (
"bufio"
"bytes"
"context"
"crypto/sha256"
"encoding/hex"
@@ -208,6 +209,22 @@ func autoImportIfNewer() {
fmt.Fprintf(os.Stderr, "Debug: auto-import triggered (hash changed)\n")
}
// Check for Git merge conflict markers (bd-270)
conflictMarkers := []string{"<<<<<<< ", "=======", ">>>>>>> "}
for _, marker := range conflictMarkers {
if bytes.Contains(jsonlData, []byte(marker)) {
fmt.Fprintf(os.Stderr, "\n❌ Git merge conflict detected in %s\n\n", jsonlPath)
fmt.Fprintf(os.Stderr, "The JSONL file contains unresolved merge conflict markers.\n")
fmt.Fprintf(os.Stderr, "This prevents auto-import from loading your issues.\n\n")
fmt.Fprintf(os.Stderr, "To resolve:\n")
fmt.Fprintf(os.Stderr, " 1. Resolve the merge conflict in your Git client, OR\n")
fmt.Fprintf(os.Stderr, " 2. Export from database to regenerate clean JSONL:\n")
fmt.Fprintf(os.Stderr, " bd export -o %s\n\n", jsonlPath)
fmt.Fprintf(os.Stderr, "After resolving, commit the fixed JSONL file.\n")
return
}
}
// Content changed - parse all issues
scanner := bufio.NewScanner(strings.NewReader(string(jsonlData)))
scanner.Buffer(make([]byte, 0, 1024), 2*1024*1024) // 2MB buffer for large JSON lines

View File

@@ -2,10 +2,13 @@ package main
import (
"bufio"
"bytes"
"context"
"encoding/json"
"io"
"os"
"path/filepath"
"strings"
"sync"
"testing"
"time"
@@ -978,6 +981,90 @@ func TestAutoImportNoCollision(t *testing.T) {
}
}
// TestAutoImportMergeConflict tests that auto-import detects Git merge conflicts (bd-270)
func TestAutoImportMergeConflict(t *testing.T) {
tmpDir, err := os.MkdirTemp("", "bd-test-conflict-*")
if err != nil {
t.Fatalf("Failed to create temp dir: %v", err)
}
defer os.RemoveAll(tmpDir)
dbPath = filepath.Join(tmpDir, "test.db")
jsonlPath := filepath.Join(tmpDir, "issues.jsonl")
testStore, err := sqlite.New(dbPath)
if err != nil {
t.Fatalf("Failed to create storage: %v", err)
}
defer testStore.Close()
store = testStore
storeMutex.Lock()
storeActive = true
storeMutex.Unlock()
defer func() {
storeMutex.Lock()
storeActive = false
storeMutex.Unlock()
}()
ctx := context.Background()
// Create an initial issue in database
dbIssue := &types.Issue{
ID: "test-conflict-1",
Title: "Original issue",
Status: types.StatusOpen,
Priority: 1,
IssueType: types.TypeTask,
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
}
if err := testStore.CreateIssue(ctx, dbIssue, "test"); err != nil {
t.Fatalf("Failed to create issue: %v", err)
}
// Create JSONL with merge conflict markers
conflictContent := `<<<<<<< HEAD
{"id":"test-conflict-1","title":"HEAD version","status":"open","priority":1,"issue_type":"task","created_at":"2025-10-16T00:00:00Z","updated_at":"2025-10-16T00:00:00Z"}
=======
{"id":"test-conflict-1","title":"Incoming version","status":"in_progress","priority":2,"issue_type":"bug","created_at":"2025-10-16T00:00:00Z","updated_at":"2025-10-16T00:00:00Z"}
>>>>>>> incoming-branch
`
if err := os.WriteFile(jsonlPath, []byte(conflictContent), 0644); err != nil {
t.Fatalf("Failed to create conflicted JSONL: %v", err)
}
// Capture stderr to check for merge conflict message
oldStderr := os.Stderr
r, w, _ := os.Pipe()
os.Stderr = w
// Run auto-import - should detect conflict and abort
autoImportIfNewer()
w.Close()
os.Stderr = oldStderr
var buf bytes.Buffer
io.Copy(&buf, r)
stderrOutput := buf.String()
// Verify merge conflict was detected
if !strings.Contains(stderrOutput, "Git merge conflict detected") {
t.Errorf("Expected 'Git merge conflict detected' in stderr, got: %s", stderrOutput)
}
// Verify the database was not modified (original issue unchanged)
result, err := testStore.GetIssue(ctx, "test-conflict-1")
if err != nil {
t.Fatalf("Failed to get issue: %v", err)
}
if result.Title != "Original issue" {
t.Errorf("Expected title 'Original issue' (unchanged), got '%s'", result.Title)
}
}
// TestAutoImportClosedAtInvariant tests that auto-import enforces status/closed_at invariant
func TestAutoImportClosedAtInvariant(t *testing.T) {
tmpDir, err := os.MkdirTemp("", "bd-test-invariant-*")