Merge branch 'main' of github.com:steveyegge/beads
# Conflicts: # .beads/beads.jsonl
This commit is contained in:
@@ -90,7 +90,7 @@ func ImportIssues(ctx context.Context, dbPath string, store storage.Storage, iss
|
||||
}
|
||||
|
||||
// Detect and resolve collisions
|
||||
issues, err = handleCollisions(ctx, sqliteStore, issues, opts, result)
|
||||
issues, err = detectUpdates(ctx, sqliteStore, issues, opts, result)
|
||||
if err != nil {
|
||||
return result, err
|
||||
}
|
||||
@@ -193,8 +193,8 @@ func handlePrefixMismatch(ctx context.Context, sqliteStore *sqlite.SQLiteStorage
|
||||
return nil
|
||||
}
|
||||
|
||||
// handleCollisions detects and resolves ID collisions
|
||||
func handleCollisions(ctx context.Context, sqliteStore *sqlite.SQLiteStorage, issues []*types.Issue, opts Options, result *Result) ([]*types.Issue, error) {
|
||||
// detectUpdates detects same-ID scenarios (which are updates with hash IDs, not collisions)
|
||||
func detectUpdates(ctx context.Context, sqliteStore *sqlite.SQLiteStorage, issues []*types.Issue, opts Options, result *Result) ([]*types.Issue, error) {
|
||||
// Phase 1: Detect (read-only)
|
||||
collisionResult, err := sqlite.DetectCollisions(ctx, sqliteStore, issues)
|
||||
if err != nil {
|
||||
@@ -206,24 +206,12 @@ func handleCollisions(ctx context.Context, sqliteStore *sqlite.SQLiteStorage, is
|
||||
result.CollisionIDs = append(result.CollisionIDs, collision.ID)
|
||||
}
|
||||
|
||||
// Handle collisions - with hash IDs, collisions shouldn't happen
|
||||
// With hash IDs, "collisions" (same ID, different content) are actually UPDATES
|
||||
// Hash IDs are based on creation content and remain stable across updates
|
||||
// So same ID + different fields = normal update operation, not a collision
|
||||
// The collisionResult.Collisions list represents issues that will be updated
|
||||
if len(collisionResult.Collisions) > 0 {
|
||||
// Hash-based IDs make collisions extremely unlikely (same ID = same content)
|
||||
// If we get here, it's likely a bug or manual ID manipulation
|
||||
return nil, fmt.Errorf("collision detected for issues: %v (this should not happen with hash-based IDs)", result.CollisionIDs)
|
||||
|
||||
// Remove colliding issues from the list (they're already processed)
|
||||
filteredIssues := make([]*types.Issue, 0)
|
||||
collidingIDs := make(map[string]bool)
|
||||
for _, collision := range collisionResult.Collisions {
|
||||
collidingIDs[collision.ID] = true
|
||||
}
|
||||
for _, issue := range issues {
|
||||
if !collidingIDs[issue.ID] {
|
||||
filteredIssues = append(filteredIssues, issue)
|
||||
}
|
||||
}
|
||||
return filteredIssues, nil
|
||||
result.Updated = len(collisionResult.Collisions)
|
||||
}
|
||||
|
||||
// Phase 4: Renames removed - obsolete with hash IDs (bd-8e05)
|
||||
@@ -435,7 +423,7 @@ func upsertIssues(ctx context.Context, sqliteStore *sqlite.SQLiteStorage, issues
|
||||
// Phase 2: New content - check for ID collision
|
||||
if existingWithID, found := dbByID[incoming.ID]; found {
|
||||
// ID exists but different content - this is a collision
|
||||
// The collision should have been handled earlier by handleCollisions
|
||||
// The update should have been detected earlier by detectUpdates
|
||||
// If we reach here, it means collision wasn't resolved - treat as update
|
||||
if !opts.SkipUpdate {
|
||||
// Build updates map
|
||||
|
||||
@@ -616,6 +616,7 @@ func (m *MemoryStorage) SetJSONLFileHash(ctx context.Context, fileHash string) e
|
||||
// GetDependencyTree gets the dependency tree for an issue
|
||||
func (m *MemoryStorage) GetDependencyTree(ctx context.Context, issueID string, maxDepth int, showAllPaths bool, reverse bool) ([]*types.TreeNode, error) {
|
||||
// Simplified implementation - just return direct dependencies
|
||||
// Note: reverse parameter is accepted for interface compatibility but not fully implemented in memory storage
|
||||
deps, err := m.GetDependencies(ctx, issueID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
||||
@@ -431,7 +431,7 @@ func (s *SQLiteStorage) GetDependencyTree(ctx context.Context, issueID string, m
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to scan tree node: %w", err)
|
||||
}
|
||||
_ = parentID // Silence unused variable warning
|
||||
node.ParentID = parentID
|
||||
|
||||
if closedAt.Valid {
|
||||
node.ClosedAt = &closedAt.Time
|
||||
|
||||
@@ -1045,9 +1045,9 @@ func TestGetStatistics(t *testing.T) {
|
||||
// does not affect normal usage where WAL mode handles typical concurrent operations.
|
||||
// For very high concurrency needs, consider using CGO-enabled sqlite3 driver or PostgreSQL.
|
||||
|
||||
// TestParallelIssueCreation verifies that parallel issue creation doesn't cause ID collisions
|
||||
// This is a regression test for bd-89 (GH-6) where race conditions in ID generation caused
|
||||
// UNIQUE constraint failures when creating issues rapidly in parallel.
|
||||
// TestParallelIssueCreation verifies that parallel issue creation works correctly with hash IDs
|
||||
// This is a regression test for bd-89 (GH-6). With hash-based IDs, parallel creation works
|
||||
// naturally since each issue gets a unique random hash - no coordination needed.
|
||||
func TestParallelIssueCreation(t *testing.T) {
|
||||
store, cleanup := setupTestDB(t)
|
||||
defer cleanup()
|
||||
|
||||
@@ -222,8 +222,9 @@ type BlockedIssue struct {
|
||||
// TreeNode represents a node in a dependency tree
|
||||
type TreeNode struct {
|
||||
Issue
|
||||
Depth int `json:"depth"`
|
||||
Truncated bool `json:"truncated"`
|
||||
Depth int `json:"depth"`
|
||||
ParentID string `json:"parent_id"`
|
||||
Truncated bool `json:"truncated"`
|
||||
}
|
||||
|
||||
// Statistics provides aggregate metrics
|
||||
|
||||
Reference in New Issue
Block a user