Remove collision-era language from docs and code
- Updated FAQ.md, ADVANCED.md, TROUBLESHOOTING.md to explain hash IDs eliminate collisions - Removed --resolve-collisions references from all documentation and examples - Renamed handleCollisions() to detectUpdates() to reflect update semantics - Updated test names: TestAutoImportWithCollision → TestAutoImportWithUpdate - Clarified: with hash IDs, same-ID = update operation, not collision Closes: bd-50a7, bd-b84f, bd-bda8, bd-650c, bd-3ef2, bd-c083, bd-85a6
This commit is contained in:
56
ADVANCED.md
56
ADVANCED.md
@@ -197,43 +197,53 @@ bd automatically detects when you're in a worktree and shows a prominent warning
|
|||||||
**Why It Matters:**
|
**Why It Matters:**
|
||||||
The daemon maintains its own view of the current working directory and git state. When multiple worktrees share the same `.beads` database, the daemon may commit changes intended for one branch to a different branch, leading to confusion and incorrect git history.
|
The daemon maintains its own view of the current working directory and git state. When multiple worktrees share the same `.beads` database, the daemon may commit changes intended for one branch to a different branch, leading to confusion and incorrect git history.
|
||||||
|
|
||||||
## Handling Import Collisions
|
## Handling Git Merge Conflicts
|
||||||
|
|
||||||
When merging branches or pulling changes, you may encounter ID collisions (same ID, different content). bd detects and safely handles these:
|
**With hash-based IDs (v0.20.1+), ID collisions are eliminated.** Different issues get different hash IDs, so concurrent creation doesn't cause conflicts.
|
||||||
|
|
||||||
**Check for collisions after merge:**
|
### Understanding Same-ID Scenarios
|
||||||
|
|
||||||
|
When you encounter the same ID during import, it's an **update operation**, not a collision:
|
||||||
|
|
||||||
|
- Hash IDs are content-based and remain stable across updates
|
||||||
|
- Same ID + different fields = normal update to existing issue
|
||||||
|
- bd automatically applies updates when importing
|
||||||
|
|
||||||
|
**Preview changes before importing:**
|
||||||
```bash
|
```bash
|
||||||
# After git merge or pull
|
# After git merge or pull
|
||||||
bd import -i .beads/issues.jsonl --dry-run
|
bd import -i .beads/issues.jsonl --dry-run
|
||||||
|
|
||||||
# Output shows:
|
# Output shows:
|
||||||
# === Collision Detection Report ===
|
|
||||||
# Exact matches (idempotent): 15
|
# Exact matches (idempotent): 15
|
||||||
# New issues: 5
|
# New issues: 5
|
||||||
# COLLISIONS DETECTED: 3
|
# Updates: 3
|
||||||
#
|
#
|
||||||
# Colliding issues:
|
# Issues to be updated:
|
||||||
# bd-10: Fix authentication (conflicting fields: [title, priority])
|
# bd-a3f2: Fix authentication (changed: priority, status)
|
||||||
# bd-12: Add feature (conflicting fields: [description, status])
|
# bd-b8e1: Add feature (changed: description)
|
||||||
```
|
```
|
||||||
|
|
||||||
**Resolve collisions automatically:**
|
### Git Merge Conflicts
|
||||||
|
|
||||||
|
The conflicts you'll encounter are **git merge conflicts** in the JSONL file when the same issue was modified on both branches (different timestamps/fields). This is not an ID collision.
|
||||||
|
|
||||||
|
**Resolution:**
|
||||||
```bash
|
```bash
|
||||||
# Let bd resolve collisions by remapping incoming issues to new IDs
|
# After git merge creates conflict
|
||||||
bd import -i .beads/issues.jsonl --resolve-collisions
|
git checkout --theirs .beads/beads.jsonl # Accept remote version
|
||||||
|
# OR
|
||||||
|
git checkout --ours .beads/beads.jsonl # Keep local version
|
||||||
|
# OR manually resolve in editor (keep line with newer updated_at)
|
||||||
|
|
||||||
# bd will:
|
# Import the resolved JSONL
|
||||||
# - Keep existing issues unchanged
|
bd import -i .beads/beads.jsonl
|
||||||
# - Assign new IDs to colliding issues (bd-25, bd-26, etc.)
|
|
||||||
# - Update ALL text references and dependencies automatically
|
# Commit the merge
|
||||||
# - Report the remapping with reference counts
|
git add .beads/beads.jsonl
|
||||||
|
git commit
|
||||||
```
|
```
|
||||||
|
|
||||||
**Important**: The `--resolve-collisions` flag is safe and recommended for branch merges. It preserves the existing database and only renumbers the incoming colliding issues. All text mentions like "see bd-10" and dependency links are automatically updated to use the new IDs.
|
|
||||||
|
|
||||||
**Manual resolution** (alternative):
|
|
||||||
If you prefer manual control, resolve the Git conflict in `.beads/issues.jsonl` directly, then import normally without `--resolve-collisions`.
|
|
||||||
|
|
||||||
### Advanced: Intelligent Merge Tools
|
### Advanced: Intelligent Merge Tools
|
||||||
|
|
||||||
For Git merge conflicts in `.beads/issues.jsonl`, consider using **[beads-merge](https://github.com/neongreen/mono/tree/main/beads-merge)** - a specialized merge tool by @neongreen that:
|
For Git merge conflicts in `.beads/issues.jsonl`, consider using **[beads-merge](https://github.com/neongreen/mono/tree/main/beads-merge)** - a specialized merge tool by @neongreen that:
|
||||||
@@ -244,9 +254,7 @@ For Git merge conflicts in `.beads/issues.jsonl`, consider using **[beads-merge]
|
|||||||
- Leaves remaining conflicts for manual resolution
|
- Leaves remaining conflicts for manual resolution
|
||||||
- Works as a Git/jujutsu merge driver
|
- Works as a Git/jujutsu merge driver
|
||||||
|
|
||||||
**Two types of conflicts, two tools:**
|
After using beads-merge to resolve the git conflict, just run `bd import` to update your database.
|
||||||
- **Git merge conflicts** (same issue modified in two branches) → Use beads-merge during git merge
|
|
||||||
- **ID collisions** (different issues with same ID) → Use `bd import --resolve-collisions` after merge
|
|
||||||
|
|
||||||
## Custom Git Hooks
|
## Custom Git Hooks
|
||||||
|
|
||||||
|
|||||||
9
FAQ.md
9
FAQ.md
@@ -286,12 +286,11 @@ When two developers create new issues:
|
|||||||
|
|
||||||
Git may show a conflict, but resolution is simple: **keep both lines** (both changes are compatible).
|
Git may show a conflict, but resolution is simple: **keep both lines** (both changes are compatible).
|
||||||
|
|
||||||
For ID collisions (same ID, different content):
|
**With hash-based IDs (v0.20.1+), same-ID scenarios are updates, not collisions:**
|
||||||
```bash
|
|
||||||
bd import -i .beads/issues.jsonl --resolve-collisions
|
|
||||||
```
|
|
||||||
|
|
||||||
See [ADVANCED.md#handling-import-collisions](ADVANCED.md#handling-import-collisions) for details.
|
If you import an issue with the same ID but different fields, bd treats it as an update to the existing issue. This is normal behavior - hash IDs remain stable, so same ID = same issue being updated.
|
||||||
|
|
||||||
|
For git conflicts where the same issue was modified on both branches, manually resolve the JSONL conflict (usually keeping the newer `updated_at` timestamp), then `bd import` will apply the update.
|
||||||
|
|
||||||
## Migration Questions
|
## Migration Questions
|
||||||
|
|
||||||
|
|||||||
@@ -187,19 +187,24 @@ bd import -i .beads/issues.jsonl # Sync to SQLite
|
|||||||
|
|
||||||
See [ADVANCED.md](ADVANCED.md) for detailed merge strategies.
|
See [ADVANCED.md](ADVANCED.md) for detailed merge strategies.
|
||||||
|
|
||||||
### ID collisions after branch merge
|
### Git merge conflicts in JSONL
|
||||||
|
|
||||||
When merging branches where different issues were created with the same ID:
|
**With hash-based IDs (v0.20.1+), ID collisions don't occur.** Different issues get different hash IDs.
|
||||||
|
|
||||||
|
If git shows a conflict in `.beads/issues.jsonl`, it's because the same issue was modified on both branches:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Check for collisions
|
# Preview what will be updated
|
||||||
bd import -i .beads/issues.jsonl --dry-run
|
bd import -i .beads/issues.jsonl --dry-run
|
||||||
|
|
||||||
# Automatically resolve collisions
|
# Resolve git conflict (keep newer version or manually merge)
|
||||||
bd import -i .beads/issues.jsonl --resolve-collisions
|
git checkout --theirs .beads/issues.jsonl # Or --ours, or edit manually
|
||||||
|
|
||||||
|
# Import updates the database
|
||||||
|
bd import -i .beads/issues.jsonl
|
||||||
```
|
```
|
||||||
|
|
||||||
See [ADVANCED.md#handling-import-collisions](ADVANCED.md#handling-import-collisions) for details.
|
See [ADVANCED.md#handling-git-merge-conflicts](ADVANCED.md#handling-git-merge-conflicts) for details.
|
||||||
|
|
||||||
### Permission denied on git hooks
|
### Permission denied on git hooks
|
||||||
|
|
||||||
|
|||||||
@@ -836,9 +836,9 @@ func TestAutoImportDisabled(t *testing.T) {
|
|||||||
storeMutex.Unlock()
|
storeMutex.Unlock()
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestAutoImportWithCollision tests that auto-import detects collisions and preserves local changes
|
// TestAutoImportWithUpdate tests that auto-import detects same-ID updates and applies them
|
||||||
func TestAutoImportWithCollision(t *testing.T) {
|
func TestAutoImportWithUpdate(t *testing.T) {
|
||||||
tmpDir, err := os.MkdirTemp("", "bd-test-collision-*")
|
tmpDir, err := os.MkdirTemp("", "bd-test-update-*")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Failed to create temp dir: %v", err)
|
t.Fatalf("Failed to create temp dir: %v", err)
|
||||||
}
|
}
|
||||||
@@ -877,7 +877,7 @@ func TestAutoImportWithCollision(t *testing.T) {
|
|||||||
t.Fatalf("Failed to create issue: %v", err)
|
t.Fatalf("Failed to create issue: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create JSONL with same ID but status=open (conflict)
|
// Create JSONL with same ID but status=open (update scenario)
|
||||||
jsonlIssue := &types.Issue{
|
jsonlIssue := &types.Issue{
|
||||||
ID: "test-col-1",
|
ID: "test-col-1",
|
||||||
Title: "Remote version",
|
Title: "Remote version",
|
||||||
@@ -911,9 +911,9 @@ func TestAutoImportWithCollision(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestAutoImportNoCollision tests happy path with no conflicts
|
// TestAutoImportNoUpdate tests happy path with no updates needed
|
||||||
func TestAutoImportNoCollision(t *testing.T) {
|
func TestAutoImportNoUpdate(t *testing.T) {
|
||||||
tmpDir, err := os.MkdirTemp("", "bd-test-nocoll-*")
|
tmpDir, err := os.MkdirTemp("", "bd-test-noupdate-*")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Failed to create temp dir: %v", err)
|
t.Fatalf("Failed to create temp dir: %v", err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,21 +10,21 @@ Import issues from JSON Lines format (one JSON object per line).
|
|||||||
- **From stdin**: `bd import` (reads from stdin)
|
- **From stdin**: `bd import` (reads from stdin)
|
||||||
- **From file**: `bd import -i issues.jsonl`
|
- **From file**: `bd import -i issues.jsonl`
|
||||||
- **Preview**: `bd import -i issues.jsonl --dry-run`
|
- **Preview**: `bd import -i issues.jsonl --dry-run`
|
||||||
- **Resolve collisions**: `bd import -i issues.jsonl --resolve-collisions`
|
|
||||||
|
|
||||||
## Behavior
|
## Behavior
|
||||||
|
|
||||||
- **Existing issues** (same ID): Updated with new data
|
- **Existing issues** (same ID): Updated with new data
|
||||||
- **New issues**: Created
|
- **New issues**: Created
|
||||||
- **Collisions** (same ID, different content): Detected and reported
|
- **Same-ID scenarios**: With hash-based IDs (v0.20.1+), same ID = same issue being updated (not a collision)
|
||||||
|
|
||||||
## Collision Handling
|
## Preview Changes
|
||||||
|
|
||||||
When merging branches or pulling changes, ID collisions can occur:
|
Use `--dry-run` to see what will change before importing:
|
||||||
|
|
||||||
- **--dry-run**: Preview collisions without making changes
|
```bash
|
||||||
- **--resolve-collisions**: Automatically remap colliding issues to new IDs
|
bd import -i issues.jsonl --dry-run
|
||||||
- All text references and dependencies are automatically updated
|
# Shows: new issues, updates, exact matches
|
||||||
|
```
|
||||||
|
|
||||||
## Automatic Import
|
## Automatic Import
|
||||||
|
|
||||||
|
|||||||
@@ -263,32 +263,25 @@ Import issues from JSONL format.
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
bd import < issues.jsonl
|
bd import < issues.jsonl
|
||||||
bd import --resolve-collisions < issues.jsonl
|
bd import -i issues.jsonl --dry-run # Preview changes
|
||||||
```
|
```
|
||||||
|
|
||||||
**Flags:**
|
**Behavior with hash-based IDs (v0.20.1+):**
|
||||||
- `--resolve-collisions` - Automatically remap conflicting issue IDs
|
- Same ID = update operation (hash IDs remain stable)
|
||||||
|
- Different issues get different hash IDs (no collisions)
|
||||||
|
- Import automatically applies updates to existing issues
|
||||||
|
|
||||||
**Use cases for --resolve-collisions:**
|
**Use `--dry-run` to preview:**
|
||||||
- **Reimporting** after manual JSONL edits - if you closed an issue in the JSONL that's still open in DB
|
|
||||||
- **Merging databases** - importing issues from another database with overlapping IDs
|
|
||||||
- **Restoring from backup** - when database state has diverged from JSONL
|
|
||||||
|
|
||||||
**What --resolve-collisions does:**
|
|
||||||
1. Detects ID conflicts (same ID, different status/content)
|
|
||||||
2. Remaps conflicting imports to new IDs
|
|
||||||
3. Updates all references and dependencies to use new IDs
|
|
||||||
4. Reports remapping (e.g., "mit-1 → bd-4")
|
|
||||||
|
|
||||||
**Without --resolve-collisions**: Import fails on first conflict.
|
|
||||||
|
|
||||||
**Example scenario:**
|
|
||||||
```bash
|
```bash
|
||||||
# You have: mit-1 (open) in database
|
bd import -i issues.jsonl --dry-run
|
||||||
# Importing: mit-1 (closed) from JSONL
|
# Shows: new issues, updates, exact matches
|
||||||
# Result: Import creates bd-4 with closed status, preserves existing mit-1
|
|
||||||
```
|
```
|
||||||
|
|
||||||
|
**Use cases:**
|
||||||
|
- **Syncing after git pull** - daemon auto-imports, manual rarely needed
|
||||||
|
- **Merging databases** - import issues from another database
|
||||||
|
- **Restoring from backup** - reimport JSONL to restore state
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Setup Commands
|
## Setup Commands
|
||||||
|
|||||||
@@ -63,14 +63,16 @@ The hook is silent on success, fast (no git operations), and safe (fails commit
|
|||||||
After a git pull or merge, the hook runs:
|
After a git pull or merge, the hook runs:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
bd import -i .beads/issues.jsonl --resolve-collisions
|
bd import -i .beads/issues.jsonl
|
||||||
```
|
```
|
||||||
|
|
||||||
This ensures your local database reflects the merged state. The hook:
|
This ensures your local database reflects the merged state. The hook:
|
||||||
- Only runs if `.beads/issues.jsonl` exists
|
- Only runs if `.beads/issues.jsonl` exists
|
||||||
- Automatically resolves ID collisions from branch merges
|
- Imports any new issues or updates from the merge
|
||||||
- Warns on failure but doesn't block the merge
|
- Warns on failure but doesn't block the merge
|
||||||
|
|
||||||
|
**Note:** With hash-based IDs (v0.20.1+), ID collisions don't occur - different issues get different hash IDs.
|
||||||
|
|
||||||
## Compatibility
|
## Compatibility
|
||||||
|
|
||||||
- **Auto-sync**: Works alongside bd's automatic 5-second debounce
|
- **Auto-sync**: Works alongside bd's automatic 5-second debounce
|
||||||
|
|||||||
@@ -176,11 +176,11 @@ bd ready # See what's ready to work on
|
|||||||
# Import all issues (open and closed)
|
# Import all issues (open and closed)
|
||||||
python gh2jsonl.py --repo mycompany/myapp > all-issues.jsonl
|
python gh2jsonl.py --repo mycompany/myapp > all-issues.jsonl
|
||||||
|
|
||||||
# Preview import (check for collisions)
|
# Preview import (check for new issues and updates)
|
||||||
bd import -i all-issues.jsonl --dry-run
|
bd import -i all-issues.jsonl --dry-run
|
||||||
|
|
||||||
# Import with collision resolution if needed
|
# Import issues
|
||||||
bd import -i all-issues.jsonl --resolve-collisions
|
bd import -i all-issues.jsonl
|
||||||
|
|
||||||
# View stats
|
# View stats
|
||||||
bd stats
|
bd stats
|
||||||
|
|||||||
@@ -90,7 +90,7 @@ func ImportIssues(ctx context.Context, dbPath string, store storage.Storage, iss
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Detect and resolve collisions
|
// Detect and resolve collisions
|
||||||
issues, err = handleCollisions(ctx, sqliteStore, issues, opts, result)
|
issues, err = detectUpdates(ctx, sqliteStore, issues, opts, result)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return result, err
|
return result, err
|
||||||
}
|
}
|
||||||
@@ -193,8 +193,8 @@ func handlePrefixMismatch(ctx context.Context, sqliteStore *sqlite.SQLiteStorage
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// handleCollisions detects and resolves ID collisions
|
// detectUpdates detects same-ID scenarios (which are updates with hash IDs, not collisions)
|
||||||
func handleCollisions(ctx context.Context, sqliteStore *sqlite.SQLiteStorage, issues []*types.Issue, opts Options, result *Result) ([]*types.Issue, error) {
|
func detectUpdates(ctx context.Context, sqliteStore *sqlite.SQLiteStorage, issues []*types.Issue, opts Options, result *Result) ([]*types.Issue, error) {
|
||||||
// Phase 1: Detect (read-only)
|
// Phase 1: Detect (read-only)
|
||||||
collisionResult, err := sqlite.DetectCollisions(ctx, sqliteStore, issues)
|
collisionResult, err := sqlite.DetectCollisions(ctx, sqliteStore, issues)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -423,7 +423,7 @@ func upsertIssues(ctx context.Context, sqliteStore *sqlite.SQLiteStorage, issues
|
|||||||
// Phase 2: New content - check for ID collision
|
// Phase 2: New content - check for ID collision
|
||||||
if existingWithID, found := dbByID[incoming.ID]; found {
|
if existingWithID, found := dbByID[incoming.ID]; found {
|
||||||
// ID exists but different content - this is a collision
|
// 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 we reach here, it means collision wasn't resolved - treat as update
|
||||||
if !opts.SkipUpdate {
|
if !opts.SkipUpdate {
|
||||||
// Build updates map
|
// Build updates map
|
||||||
|
|||||||
@@ -1045,9 +1045,9 @@ func TestGetStatistics(t *testing.T) {
|
|||||||
// does not affect normal usage where WAL mode handles typical concurrent operations.
|
// 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.
|
// For very high concurrency needs, consider using CGO-enabled sqlite3 driver or PostgreSQL.
|
||||||
|
|
||||||
// TestParallelIssueCreation verifies that parallel issue creation doesn't cause ID collisions
|
// TestParallelIssueCreation verifies that parallel issue creation works correctly with hash IDs
|
||||||
// This is a regression test for bd-89 (GH-6) where race conditions in ID generation caused
|
// This is a regression test for bd-89 (GH-6). With hash-based IDs, parallel creation works
|
||||||
// UNIQUE constraint failures when creating issues rapidly in parallel.
|
// naturally since each issue gets a unique random hash - no coordination needed.
|
||||||
func TestParallelIssueCreation(t *testing.T) {
|
func TestParallelIssueCreation(t *testing.T) {
|
||||||
store, cleanup := setupTestDB(t)
|
store, cleanup := setupTestDB(t)
|
||||||
defer cleanup()
|
defer cleanup()
|
||||||
|
|||||||
Reference in New Issue
Block a user