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:
@@ -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
|
||||
|
||||
@@ -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-*")
|
||||
|
||||
Reference in New Issue
Block a user