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:
File diff suppressed because one or more lines are too long
@@ -2,6 +2,7 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bufio"
|
"bufio"
|
||||||
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
"crypto/sha256"
|
"crypto/sha256"
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
@@ -208,6 +209,22 @@ func autoImportIfNewer() {
|
|||||||
fmt.Fprintf(os.Stderr, "Debug: auto-import triggered (hash changed)\n")
|
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
|
// Content changed - parse all issues
|
||||||
scanner := bufio.NewScanner(strings.NewReader(string(jsonlData)))
|
scanner := bufio.NewScanner(strings.NewReader(string(jsonlData)))
|
||||||
scanner.Buffer(make([]byte, 0, 1024), 2*1024*1024) // 2MB buffer for large JSON lines
|
scanner.Buffer(make([]byte, 0, 1024), 2*1024*1024) // 2MB buffer for large JSON lines
|
||||||
|
|||||||
@@ -2,10 +2,13 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bufio"
|
"bufio"
|
||||||
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"io"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"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
|
// TestAutoImportClosedAtInvariant tests that auto-import enforces status/closed_at invariant
|
||||||
func TestAutoImportClosedAtInvariant(t *testing.T) {
|
func TestAutoImportClosedAtInvariant(t *testing.T) {
|
||||||
tmpDir, err := os.MkdirTemp("", "bd-test-invariant-*")
|
tmpDir, err := os.MkdirTemp("", "bd-test-invariant-*")
|
||||||
|
|||||||
Reference in New Issue
Block a user