Merge branch 'main' of github.com:steveyegge/beads
# Conflicts: # .beads/beads.jsonl
This commit is contained in:
+222
-157
File diff suppressed because one or more lines are too long
+32
-24
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -147,6 +147,12 @@ bd create "Issue title" -t bug -p 1 -l bug,critical --json
|
|||||||
# Create multiple issues from markdown file
|
# Create multiple issues from markdown file
|
||||||
bd create -f feature-plan.md --json
|
bd create -f feature-plan.md --json
|
||||||
|
|
||||||
|
# Create epic with hierarchical child tasks
|
||||||
|
bd create "Auth System" -t epic -p 1 --json # Returns: bd-a3f8e9
|
||||||
|
bd create "Login UI" -p 1 --json # Auto-assigned: bd-a3f8e9.1
|
||||||
|
bd create "Backend validation" -p 1 --json # Auto-assigned: bd-a3f8e9.2
|
||||||
|
bd create "Tests" -p 1 --json # Auto-assigned: bd-a3f8e9.3
|
||||||
|
|
||||||
# Update one or more issues
|
# Update one or more issues
|
||||||
bd update <id> [<id>...] --status in_progress --json
|
bd update <id> [<id>...] --status in_progress --json
|
||||||
bd update <id> [<id>...] --priority 1 --json
|
bd update <id> [<id>...] --priority 1 --json
|
||||||
@@ -192,10 +198,10 @@ bd rename-prefix kw- --json # Apply rename
|
|||||||
# Restore compacted issue from git history
|
# Restore compacted issue from git history
|
||||||
bd restore <id> # View full history at time of compaction
|
bd restore <id> # View full history at time of compaction
|
||||||
|
|
||||||
# Import with collision detection
|
# Import issues from JSONL
|
||||||
bd import -i .beads/issues.jsonl --dry-run # Preview only
|
bd import -i .beads/issues.jsonl --dry-run # Preview changes
|
||||||
bd import -i .beads/issues.jsonl --resolve-collisions # Auto-resolve
|
bd import -i .beads/issues.jsonl # Import and update issues
|
||||||
bd import -i .beads/issues.jsonl --resolve-collisions --dedupe-after # Auto-resolve + detect duplicates
|
bd import -i .beads/issues.jsonl --dedupe-after # Import + detect duplicates
|
||||||
|
|
||||||
# Find and merge duplicate issues
|
# Find and merge duplicate issues
|
||||||
bd duplicates # Show all duplicates
|
bd duplicates # Show all duplicates
|
||||||
@@ -335,9 +341,11 @@ bd daemons killall # Restart with default (poll) mode
|
|||||||
- `bug` - Something broken that needs fixing
|
- `bug` - Something broken that needs fixing
|
||||||
- `feature` - New functionality
|
- `feature` - New functionality
|
||||||
- `task` - Work item (tests, docs, refactoring)
|
- `task` - Work item (tests, docs, refactoring)
|
||||||
- `epic` - Large feature composed of multiple issues
|
- `epic` - Large feature composed of multiple issues (supports hierarchical children)
|
||||||
- `chore` - Maintenance work (dependencies, tooling)
|
- `chore` - Maintenance work (dependencies, tooling)
|
||||||
|
|
||||||
|
**Hierarchical children:** Epics can have child issues with dotted IDs (e.g., `bd-a3f8e9.1`, `bd-a3f8e9.2`). Children are auto-numbered sequentially. Up to 3 levels of nesting supported. The parent hash ensures unique namespace - no coordination needed between agents working on different epics.
|
||||||
|
|
||||||
### Priorities
|
### Priorities
|
||||||
|
|
||||||
- `0` - Critical (security, data loss, broken builds)
|
- `0` - Critical (security, data loss, broken builds)
|
||||||
@@ -371,8 +379,8 @@ bd duplicates --auto-merge
|
|||||||
# Preview what would be merged
|
# Preview what would be merged
|
||||||
bd duplicates --dry-run
|
bd duplicates --dry-run
|
||||||
|
|
||||||
# During import (after collision resolution)
|
# During import
|
||||||
bd import -i issues.jsonl --resolve-collisions --dedupe-after
|
bd import -i issues.jsonl --dedupe-after
|
||||||
```
|
```
|
||||||
|
|
||||||
**Detection strategies:**
|
**Detection strategies:**
|
||||||
@@ -557,42 +565,30 @@ 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 most git merges succeed cleanly.
|
||||||
|
|
||||||
**Check for collisions after merge:**
|
**When git merge conflicts occur:**
|
||||||
|
Git conflicts in `.beads/beads.jsonl` happen when the same issue is modified on both branches (different timestamps/fields). This is a **same-issue update conflict**, not an ID collision.
|
||||||
|
|
||||||
|
**Resolution:**
|
||||||
```bash
|
```bash
|
||||||
# After git merge or pull
|
# After git merge creates conflict
|
||||||
bd import -i .beads/issues.jsonl --dry-run
|
git checkout --theirs .beads/beads.jsonl # Accept remote version
|
||||||
|
# OR
|
||||||
|
git checkout --ours .beads/beads.jsonl # Keep local version
|
||||||
|
# OR manually resolve in editor
|
||||||
|
|
||||||
# Output shows:
|
# Import the resolved JSONL
|
||||||
# === Collision Detection Report ===
|
bd import -i .beads/beads.jsonl
|
||||||
# Exact matches (idempotent): 15
|
|
||||||
# New issues: 5
|
# Commit the merge
|
||||||
# COLLISIONS DETECTED: 3
|
git add .beads/beads.jsonl
|
||||||
#
|
git commit
|
||||||
# Colliding issues:
|
|
||||||
# bd-10: Fix authentication (conflicting fields: [title, priority])
|
|
||||||
# bd-12: Add feature (conflicting fields: [description, status])
|
|
||||||
```
|
```
|
||||||
|
|
||||||
**Resolve collisions automatically:**
|
**bd automatically handles updates** - same ID with different content is a normal update operation. No special flags needed.
|
||||||
```bash
|
|
||||||
# Let bd resolve collisions by remapping incoming issues to new IDs
|
|
||||||
bd import -i .beads/issues.jsonl --resolve-collisions
|
|
||||||
|
|
||||||
# bd will:
|
|
||||||
# - Keep existing issues unchanged
|
|
||||||
# - Assign new IDs to colliding issues (bd-25, bd-26, etc.)
|
|
||||||
# - Update ALL text references and dependencies automatically
|
|
||||||
# - Report the remapping with reference counts
|
|
||||||
```
|
|
||||||
|
|
||||||
**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
|
||||||
|
|
||||||
@@ -604,9 +600,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:**
|
**Beads-merge** helps with intelligent field-level merging during git merge. After resolving, just `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
|
|
||||||
|
|
||||||
## Current Project Status
|
## Current Project Status
|
||||||
|
|
||||||
@@ -709,8 +703,8 @@ rm .beads/.exclusive-lock
|
|||||||
- Use `--no-auto-flush` or `--no-auto-import` to disable automatic sync if needed
|
- Use `--no-auto-flush` or `--no-auto-import` to disable automatic sync if needed
|
||||||
- Use `bd dep tree` to understand complex dependencies
|
- Use `bd dep tree` to understand complex dependencies
|
||||||
- Priority 0-1 issues are usually more important than 2-4
|
- Priority 0-1 issues are usually more important than 2-4
|
||||||
- Use `--dry-run` to preview import collisions before resolving
|
- Use `--dry-run` to preview import changes before applying
|
||||||
- Use `--resolve-collisions` for safe automatic branch merges
|
- Hash IDs eliminate collisions - same ID with different content is a normal update
|
||||||
- Use `--id` flag with `bd create` to partition ID space for parallel workers (e.g., `worker1-100`, `worker2-500`)
|
- Use `--id` flag with `bd create` to partition ID space for parallel workers (e.g., `worker1-100`, `worker2-500`)
|
||||||
|
|
||||||
## Building and Testing
|
## Building and Testing
|
||||||
|
|||||||
@@ -83,6 +83,74 @@ Follow the repo for updates and the path to 1.0!
|
|||||||
|
|
||||||
## Usage Questions
|
## Usage Questions
|
||||||
|
|
||||||
|
### Why hash-based IDs? Why not sequential?
|
||||||
|
|
||||||
|
**Hash IDs eliminate collisions** when multiple agents or branches create issues concurrently.
|
||||||
|
|
||||||
|
**The problem with sequential IDs:**
|
||||||
|
```bash
|
||||||
|
# Branch A creates bd-10
|
||||||
|
git checkout -b feature-auth
|
||||||
|
bd create "Add OAuth" # Sequential ID: bd-10
|
||||||
|
|
||||||
|
# Branch B also creates bd-10
|
||||||
|
git checkout -b feature-payments
|
||||||
|
bd create "Add Stripe" # Collision! Same sequential ID: bd-10
|
||||||
|
|
||||||
|
# Merge conflict!
|
||||||
|
git merge feature-auth # Two different issues, same ID
|
||||||
|
```
|
||||||
|
|
||||||
|
**Hash IDs solve this:**
|
||||||
|
```bash
|
||||||
|
# Branch A
|
||||||
|
bd create "Add OAuth" # Hash ID: bd-a1b2 (from random UUID)
|
||||||
|
|
||||||
|
# Branch B
|
||||||
|
bd create "Add Stripe" # Hash ID: bd-f14c (different UUID, different hash)
|
||||||
|
|
||||||
|
# Clean merge!
|
||||||
|
git merge feature-auth # No collision, different IDs
|
||||||
|
```
|
||||||
|
|
||||||
|
**Progressive length scaling:**
|
||||||
|
- 4 chars (0-500 issues): `bd-a1b2`
|
||||||
|
- 5 chars (500-1,500 issues): `bd-f14c3`
|
||||||
|
- 6 chars (1,500+ issues): `bd-3e7a5b`
|
||||||
|
|
||||||
|
bd automatically extends hash length as your database grows to maintain low collision probability.
|
||||||
|
|
||||||
|
### What are hierarchical child IDs?
|
||||||
|
|
||||||
|
**Hierarchical IDs** (e.g., `bd-a3f8e9.1`, `bd-a3f8e9.2`) provide human-readable structure for epics and their subtasks.
|
||||||
|
|
||||||
|
**Example:**
|
||||||
|
```bash
|
||||||
|
# Create epic (generates parent hash)
|
||||||
|
bd create "Auth System" -t epic -p 1
|
||||||
|
# Returns: bd-a3f8e9
|
||||||
|
|
||||||
|
# Create children (auto-numbered .1, .2, .3)
|
||||||
|
bd create "Login UI" -p 1 # bd-a3f8e9.1
|
||||||
|
bd create "Validation" -p 1 # bd-a3f8e9.2
|
||||||
|
bd create "Tests" -p 1 # bd-a3f8e9.3
|
||||||
|
```
|
||||||
|
|
||||||
|
**Benefits:**
|
||||||
|
- Parent hash ensures unique namespace (no cross-epic collisions)
|
||||||
|
- Sequential child IDs are human-friendly
|
||||||
|
- Up to 3 levels of nesting supported
|
||||||
|
- Clear visual grouping in issue lists
|
||||||
|
|
||||||
|
**When to use:**
|
||||||
|
- Epics with multiple related tasks
|
||||||
|
- Large features with sub-features
|
||||||
|
- Work breakdown structures
|
||||||
|
|
||||||
|
**When NOT to use:**
|
||||||
|
- Simple one-off tasks (use regular hash IDs)
|
||||||
|
- Cross-cutting dependencies (use `bd dep add` instead)
|
||||||
|
|
||||||
### Should I run bd init or have my agent do it?
|
### Should I run bd init or have my agent do it?
|
||||||
|
|
||||||
**Either works!** But use the right flag:
|
**Either works!** But use the right flag:
|
||||||
@@ -218,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
|
||||||
|
|
||||||
|
|||||||
@@ -22,6 +22,36 @@ go build -o bd ./cmd/bd
|
|||||||
./bd list
|
./bd list
|
||||||
```
|
```
|
||||||
|
|
||||||
|
**Note:** Issue IDs are hash-based (e.g., `bd-a1b2`, `bd-f14c`) to prevent collisions when multiple agents/branches work concurrently.
|
||||||
|
|
||||||
|
## Hierarchical Issues (Epics)
|
||||||
|
|
||||||
|
For large features, use hierarchical IDs to organize work:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Create epic (generates parent hash ID)
|
||||||
|
./bd create "Auth System" -t epic -p 1
|
||||||
|
# Returns: bd-a3f8e9
|
||||||
|
|
||||||
|
# Create child tasks (automatically get .1, .2, .3 suffixes)
|
||||||
|
./bd create "Design login UI" -p 1 # bd-a3f8e9.1
|
||||||
|
./bd create "Backend validation" -p 1 # bd-a3f8e9.2
|
||||||
|
./bd create "Integration tests" -p 1 # bd-a3f8e9.3
|
||||||
|
|
||||||
|
# View hierarchy
|
||||||
|
./bd dep tree bd-a3f8e9
|
||||||
|
```
|
||||||
|
|
||||||
|
Output:
|
||||||
|
```
|
||||||
|
🌲 Dependency tree for bd-a3f8e9:
|
||||||
|
|
||||||
|
→ bd-a3f8e9: Auth System [epic] [P1] (open)
|
||||||
|
→ bd-a3f8e9.1: Design login UI [P1] (open)
|
||||||
|
→ bd-a3f8e9.2: Backend validation [P1] (open)
|
||||||
|
→ bd-a3f8e9.3: Integration tests [P1] (open)
|
||||||
|
```
|
||||||
|
|
||||||
## Add Dependencies
|
## Add Dependencies
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
|||||||
@@ -271,6 +271,47 @@ bd info
|
|||||||
|
|
||||||
**Note:** Hash IDs require schema version 9+. The `bd migrate` command detects old schemas and upgrades automatically.
|
**Note:** Hash IDs require schema version 9+. The `bd migrate` command detects old schemas and upgrades automatically.
|
||||||
|
|
||||||
|
### Hierarchical Child IDs
|
||||||
|
|
||||||
|
Hash IDs support **hierarchical children** for natural work breakdown structures. Child IDs use dot notation:
|
||||||
|
|
||||||
|
```
|
||||||
|
bd-a3f8e9 [epic] Auth System
|
||||||
|
bd-a3f8e9.1 [task] Design login UI
|
||||||
|
bd-a3f8e9.2 [task] Backend validation
|
||||||
|
bd-a3f8e9.3 [epic] Password Reset
|
||||||
|
bd-a3f8e9.3.1 [task] Email templates
|
||||||
|
bd-a3f8e9.3.2 [task] Reset flow tests
|
||||||
|
```
|
||||||
|
|
||||||
|
**Benefits:**
|
||||||
|
- **Collision-free**: Parent hash ensures unique namespace
|
||||||
|
- **Human-readable**: Clear parent-child relationships
|
||||||
|
- **Flexible depth**: Up to 3 levels of nesting
|
||||||
|
- **No coordination needed**: Each epic owns its child ID space
|
||||||
|
|
||||||
|
**Common patterns:**
|
||||||
|
- 1 level: Epic → tasks (most projects)
|
||||||
|
- 2 levels: Epic → features → tasks (large projects)
|
||||||
|
- 3 levels: Epic → features → stories → tasks (complex projects)
|
||||||
|
|
||||||
|
**Example workflow:**
|
||||||
|
```bash
|
||||||
|
# Create parent epic (generates hash ID automatically)
|
||||||
|
bd create "Auth System" -t epic -p 1
|
||||||
|
# Returns: bd-a3f8e9
|
||||||
|
|
||||||
|
# Create child tasks
|
||||||
|
bd create "Design login UI" -p 1 # Auto-assigned: bd-a3f8e9.1
|
||||||
|
bd create "Backend validation" -p 1 # Auto-assigned: bd-a3f8e9.2
|
||||||
|
|
||||||
|
# Create nested epic with its own children
|
||||||
|
bd create "Password Reset" -t epic -p 1 # Auto-assigned: bd-a3f8e9.3
|
||||||
|
bd create "Email templates" -p 1 # Auto-assigned: bd-a3f8e9.3.1
|
||||||
|
```
|
||||||
|
|
||||||
|
**Note:** Child IDs are automatically assigned sequentially within each parent's namespace. No need to specify parent manually - bd tracks context from git branch/working directory.
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
### Health Check
|
### Health Check
|
||||||
|
|||||||
+11
-6
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/fatih/color"
|
"github.com/fatih/color"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
@@ -273,6 +274,7 @@ var depTreeCmd = &cobra.Command{
|
|||||||
showAllPaths, _ := cmd.Flags().GetBool("show-all-paths")
|
showAllPaths, _ := cmd.Flags().GetBool("show-all-paths")
|
||||||
maxDepth, _ := cmd.Flags().GetInt("max-depth")
|
maxDepth, _ := cmd.Flags().GetInt("max-depth")
|
||||||
reverse, _ := cmd.Flags().GetBool("reverse")
|
reverse, _ := cmd.Flags().GetBool("reverse")
|
||||||
|
formatStr, _ := cmd.Flags().GetString("format")
|
||||||
|
|
||||||
if maxDepth < 1 {
|
if maxDepth < 1 {
|
||||||
fmt.Fprintf(os.Stderr, "Error: --max-depth must be >= 1\n")
|
fmt.Fprintf(os.Stderr, "Error: --max-depth must be >= 1\n")
|
||||||
@@ -285,6 +287,12 @@ var depTreeCmd = &cobra.Command{
|
|||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Handle mermaid format
|
||||||
|
if formatStr == "mermaid" {
|
||||||
|
outputMermaidTree(tree, args[0])
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if jsonOutput {
|
if jsonOutput {
|
||||||
// Always output array, even if empty
|
// Always output array, even if empty
|
||||||
if tree == nil {
|
if tree == nil {
|
||||||
@@ -383,11 +391,71 @@ var depCyclesCmd = &cobra.Command{
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// outputMermaidTree outputs a dependency tree in Mermaid.js flowchart format
|
||||||
|
func outputMermaidTree(tree []*types.TreeNode, rootID string) {
|
||||||
|
if len(tree) == 0 {
|
||||||
|
fmt.Println("flowchart TD")
|
||||||
|
fmt.Printf(" %s[\"No dependencies\"]\n", rootID)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println("flowchart TD")
|
||||||
|
|
||||||
|
// Output nodes
|
||||||
|
nodesSeen := make(map[string]bool)
|
||||||
|
for _, node := range tree {
|
||||||
|
if !nodesSeen[node.ID] {
|
||||||
|
emoji := getStatusEmoji(node.Status)
|
||||||
|
label := fmt.Sprintf("%s %s: %s", emoji, node.ID, node.Title)
|
||||||
|
// Escape quotes and backslashes in label
|
||||||
|
label = strings.ReplaceAll(label, "\\", "\\\\")
|
||||||
|
label = strings.ReplaceAll(label, "\"", "\\\"")
|
||||||
|
fmt.Printf(" %s[\"%s\"]\n", node.ID, label)
|
||||||
|
|
||||||
|
nodesSeen[node.ID] = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println()
|
||||||
|
|
||||||
|
// Output edges - use explicit parent relationships from ParentID
|
||||||
|
for _, node := range tree {
|
||||||
|
if node.ParentID != "" && node.ParentID != node.ID {
|
||||||
|
fmt.Printf(" %s --> %s\n", node.ParentID, node.ID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// getStatusEmoji returns a symbol indicator for a given status
|
||||||
|
func getStatusEmoji(status types.Status) string {
|
||||||
|
switch status {
|
||||||
|
case types.StatusOpen:
|
||||||
|
return "☐" // U+2610 Ballot Box
|
||||||
|
case types.StatusInProgress:
|
||||||
|
return "◧" // U+25E7 Square Left Half Black
|
||||||
|
case types.StatusBlocked:
|
||||||
|
return "⚠" // U+26A0 Warning Sign
|
||||||
|
case types.StatusClosed:
|
||||||
|
return "☑" // U+2611 Ballot Box with Check
|
||||||
|
default:
|
||||||
|
return "?"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
depAddCmd.Flags().StringP("type", "t", "blocks", "Dependency type (blocks|related|parent-child|discovered-from)")
|
depAddCmd.Flags().StringP("type", "t", "blocks", "Dependency type (blocks|related|parent-child|discovered-from)")
|
||||||
|
depAddCmd.Flags().Bool("json", false, "Output JSON format")
|
||||||
|
|
||||||
|
depRemoveCmd.Flags().Bool("json", false, "Output JSON format")
|
||||||
|
|
||||||
depTreeCmd.Flags().Bool("show-all-paths", false, "Show all paths to nodes (no deduplication for diamond dependencies)")
|
depTreeCmd.Flags().Bool("show-all-paths", false, "Show all paths to nodes (no deduplication for diamond dependencies)")
|
||||||
depTreeCmd.Flags().IntP("max-depth", "d", 50, "Maximum tree depth to display (safety limit)")
|
depTreeCmd.Flags().IntP("max-depth", "d", 50, "Maximum tree depth to display (safety limit)")
|
||||||
depTreeCmd.Flags().Bool("reverse", false, "Show dependent tree (what was discovered from this) instead of dependency tree (what blocks this)")
|
depTreeCmd.Flags().Bool("reverse", false, "Show dependent tree (what was discovered from this) instead of dependency tree (what blocks this)")
|
||||||
|
depTreeCmd.Flags().String("format", "", "Output format: 'mermaid' for Mermaid.js flowchart")
|
||||||
|
depTreeCmd.Flags().Bool("json", false, "Output JSON format")
|
||||||
|
|
||||||
|
depCyclesCmd.Flags().Bool("json", false, "Output JSON format")
|
||||||
|
|
||||||
depCmd.AddCommand(depAddCmd)
|
depCmd.AddCommand(depAddCmd)
|
||||||
depCmd.AddCommand(depRemoveCmd)
|
depCmd.AddCommand(depRemoveCmd)
|
||||||
depCmd.AddCommand(depTreeCmd)
|
depCmd.AddCommand(depTreeCmd)
|
||||||
|
|||||||
@@ -1,9 +1,13 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -266,3 +270,217 @@ func TestDepRemove(t *testing.T) {
|
|||||||
t.Errorf("Expected 0 dependencies after removal, got %d", len(deps))
|
t.Errorf("Expected 0 dependencies after removal, got %d", len(deps))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestDepTreeFormatFlag(t *testing.T) {
|
||||||
|
// Test that the --format flag exists on depTreeCmd
|
||||||
|
flag := depTreeCmd.Flags().Lookup("format")
|
||||||
|
if flag == nil {
|
||||||
|
t.Fatal("depTreeCmd should have --format flag")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test default value is empty string
|
||||||
|
if flag.DefValue != "" {
|
||||||
|
t.Errorf("Expected default format='', got %q", flag.DefValue)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test usage text mentions mermaid
|
||||||
|
if !strings.Contains(flag.Usage, "mermaid") {
|
||||||
|
t.Errorf("Expected flag usage to mention 'mermaid', got %q", flag.Usage)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetStatusEmoji(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
status types.Status
|
||||||
|
want string
|
||||||
|
}{
|
||||||
|
{types.StatusOpen, "☐"},
|
||||||
|
{types.StatusInProgress, "◧"},
|
||||||
|
{types.StatusBlocked, "⚠"},
|
||||||
|
{types.StatusClosed, "☑"},
|
||||||
|
{types.Status("unknown"), "?"},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(string(tt.status), func(t *testing.T) {
|
||||||
|
got := getStatusEmoji(tt.status)
|
||||||
|
if got != tt.want {
|
||||||
|
t.Errorf("getStatusEmoji(%q) = %q, want %q", tt.status, got, tt.want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestOutputMermaidTree(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
tree []*types.TreeNode
|
||||||
|
rootID string
|
||||||
|
want []string // Lines that must appear in output
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "empty tree",
|
||||||
|
tree: []*types.TreeNode{},
|
||||||
|
rootID: "test-1",
|
||||||
|
want: []string{
|
||||||
|
"flowchart TD",
|
||||||
|
`test-1["No dependencies"]`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "single dependency",
|
||||||
|
tree: []*types.TreeNode{
|
||||||
|
{
|
||||||
|
Issue: types.Issue{ID: "test-1", Title: "Task 1", Status: types.StatusInProgress},
|
||||||
|
Depth: 0,
|
||||||
|
ParentID: "",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Issue: types.Issue{ID: "test-2", Title: "Task 2", Status: types.StatusClosed},
|
||||||
|
Depth: 1,
|
||||||
|
ParentID: "test-1",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
rootID: "test-1",
|
||||||
|
want: []string{
|
||||||
|
"flowchart TD",
|
||||||
|
`test-1["◧ test-1: Task 1"]`,
|
||||||
|
`test-2["☑ test-2: Task 2"]`,
|
||||||
|
"test-1 --> test-2",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "multiple dependencies",
|
||||||
|
tree: []*types.TreeNode{
|
||||||
|
{
|
||||||
|
Issue: types.Issue{ID: "test-1", Title: "Main", Status: types.StatusOpen},
|
||||||
|
Depth: 0,
|
||||||
|
ParentID: "",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Issue: types.Issue{ID: "test-2", Title: "Sub 1", Status: types.StatusClosed},
|
||||||
|
Depth: 1,
|
||||||
|
ParentID: "test-1",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Issue: types.Issue{ID: "test-3", Title: "Sub 2", Status: types.StatusBlocked},
|
||||||
|
Depth: 1,
|
||||||
|
ParentID: "test-1",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
rootID: "test-1",
|
||||||
|
want: []string{
|
||||||
|
"flowchart TD",
|
||||||
|
`test-1["☐ test-1: Main"]`,
|
||||||
|
`test-2["☑ test-2: Sub 1"]`,
|
||||||
|
`test-3["⚠ test-3: Sub 2"]`,
|
||||||
|
"test-1 --> test-2",
|
||||||
|
"test-1 --> test-3",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
// Capture stdout
|
||||||
|
old := os.Stdout
|
||||||
|
r, w, _ := os.Pipe()
|
||||||
|
os.Stdout = w
|
||||||
|
|
||||||
|
outputMermaidTree(tt.tree, tt.rootID)
|
||||||
|
|
||||||
|
w.Close()
|
||||||
|
os.Stdout = old
|
||||||
|
|
||||||
|
var buf bytes.Buffer
|
||||||
|
io.Copy(&buf, r)
|
||||||
|
output := buf.String()
|
||||||
|
|
||||||
|
// Verify all expected lines appear
|
||||||
|
for _, line := range tt.want {
|
||||||
|
if !strings.Contains(output, line) {
|
||||||
|
t.Errorf("expected output to contain %q, got:\n%s", line, output)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestOutputMermaidTree_Siblings(t *testing.T) {
|
||||||
|
// Test case: Siblings with children (reproduces issue with wrong parent inference)
|
||||||
|
// Structure:
|
||||||
|
// BD-1 (root)
|
||||||
|
// ├── BD-2 (sibling 1)
|
||||||
|
// │ └── BD-4 (child of BD-2)
|
||||||
|
// └── BD-3 (sibling 2)
|
||||||
|
// └── BD-5 (child of BD-3)
|
||||||
|
tree := []*types.TreeNode{
|
||||||
|
{
|
||||||
|
Issue: types.Issue{ID: "BD-1", Title: "Parent", Status: types.StatusOpen},
|
||||||
|
Depth: 0,
|
||||||
|
ParentID: "",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Issue: types.Issue{ID: "BD-2", Title: "Sibling 1", Status: types.StatusOpen},
|
||||||
|
Depth: 1,
|
||||||
|
ParentID: "BD-1",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Issue: types.Issue{ID: "BD-3", Title: "Sibling 2", Status: types.StatusOpen},
|
||||||
|
Depth: 1,
|
||||||
|
ParentID: "BD-1",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Issue: types.Issue{ID: "BD-4", Title: "Child of Sibling 1", Status: types.StatusOpen},
|
||||||
|
Depth: 2,
|
||||||
|
ParentID: "BD-2",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Issue: types.Issue{ID: "BD-5", Title: "Child of Sibling 2", Status: types.StatusOpen},
|
||||||
|
Depth: 2,
|
||||||
|
ParentID: "BD-3",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// Capture stdout
|
||||||
|
old := os.Stdout
|
||||||
|
r, w, _ := os.Pipe()
|
||||||
|
os.Stdout = w
|
||||||
|
|
||||||
|
outputMermaidTree(tree, "BD-1")
|
||||||
|
|
||||||
|
w.Close()
|
||||||
|
os.Stdout = old
|
||||||
|
|
||||||
|
var buf bytes.Buffer
|
||||||
|
io.Copy(&buf, r)
|
||||||
|
output := buf.String()
|
||||||
|
|
||||||
|
// Verify correct edges exist
|
||||||
|
correctEdges := []string{
|
||||||
|
"BD-1 --> BD-2",
|
||||||
|
"BD-1 --> BD-3",
|
||||||
|
"BD-2 --> BD-4",
|
||||||
|
"BD-3 --> BD-5",
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, edge := range correctEdges {
|
||||||
|
if !strings.Contains(output, edge) {
|
||||||
|
t.Errorf("expected edge %q to be present, got:\n%s", edge, output)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify incorrect edges do NOT exist (siblings shouldn't be connected)
|
||||||
|
incorrectEdges := []string{
|
||||||
|
"BD-2 --> BD-3", // Siblings shouldn't be connected
|
||||||
|
"BD-3 --> BD-4", // BD-4's parent is BD-2, not BD-3
|
||||||
|
"BD-4 --> BD-3", // Wrong direction
|
||||||
|
"BD-4 --> BD-5", // These are cousins, not parent-child
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, edge := range incorrectEdges {
|
||||||
|
if strings.Contains(output, edge) {
|
||||||
|
t.Errorf("incorrect edge %q should NOT be present, got:\n%s", edge, output)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ Example:
|
|||||||
|
|
||||||
autoMerge, _ := cmd.Flags().GetBool("auto-merge")
|
autoMerge, _ := cmd.Flags().GetBool("auto-merge")
|
||||||
dryRun, _ := cmd.Flags().GetBool("dry-run")
|
dryRun, _ := cmd.Flags().GetBool("dry-run")
|
||||||
|
jsonOutput, _ := cmd.Flags().GetBool("json")
|
||||||
|
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
|
|
||||||
@@ -174,6 +175,7 @@ Example:
|
|||||||
func init() {
|
func init() {
|
||||||
duplicatesCmd.Flags().Bool("auto-merge", false, "Automatically merge all duplicates")
|
duplicatesCmd.Flags().Bool("auto-merge", false, "Automatically merge all duplicates")
|
||||||
duplicatesCmd.Flags().Bool("dry-run", false, "Show what would be merged without making changes")
|
duplicatesCmd.Flags().Bool("dry-run", false, "Show what would be merged without making changes")
|
||||||
|
duplicatesCmd.Flags().Bool("json", false, "Output JSON format")
|
||||||
rootCmd.AddCommand(duplicatesCmd)
|
rootCmd.AddCommand(duplicatesCmd)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+14
-5
@@ -22,7 +22,7 @@ var labelCmd = &cobra.Command{
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Helper function to process label operations for multiple issues
|
// Helper function to process label operations for multiple issues
|
||||||
func processBatchLabelOperation(issueIDs []string, label string, operation string,
|
func processBatchLabelOperation(issueIDs []string, label string, operation string, jsonOut bool,
|
||||||
daemonFunc func(string, string) error, storeFunc func(context.Context, string, string, string) error) {
|
daemonFunc func(string, string) error, storeFunc func(context.Context, string, string, string) error) {
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
results := []map[string]interface{}{}
|
results := []map[string]interface{}{}
|
||||||
@@ -40,7 +40,7 @@ func processBatchLabelOperation(issueIDs []string, label string, operation strin
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
if jsonOutput {
|
if jsonOut {
|
||||||
results = append(results, map[string]interface{}{
|
results = append(results, map[string]interface{}{
|
||||||
"status": operation,
|
"status": operation,
|
||||||
"issue_id": issueID,
|
"issue_id": issueID,
|
||||||
@@ -62,7 +62,7 @@ func processBatchLabelOperation(issueIDs []string, label string, operation strin
|
|||||||
markDirtyAndScheduleFlush()
|
markDirtyAndScheduleFlush()
|
||||||
}
|
}
|
||||||
|
|
||||||
if jsonOutput && len(results) > 0 {
|
if jsonOut && len(results) > 0 {
|
||||||
outputJSON(results)
|
outputJSON(results)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -79,6 +79,7 @@ var labelAddCmd = &cobra.Command{
|
|||||||
Short: "Add a label to one or more issues",
|
Short: "Add a label to one or more issues",
|
||||||
Args: cobra.MinimumNArgs(2),
|
Args: cobra.MinimumNArgs(2),
|
||||||
Run: func(cmd *cobra.Command, args []string) {
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
|
jsonOutput, _ := cmd.Flags().GetBool("json")
|
||||||
issueIDs, label := parseLabelArgs(args)
|
issueIDs, label := parseLabelArgs(args)
|
||||||
|
|
||||||
// Resolve partial IDs
|
// Resolve partial IDs
|
||||||
@@ -107,7 +108,7 @@ var labelAddCmd = &cobra.Command{
|
|||||||
}
|
}
|
||||||
issueIDs = resolvedIDs
|
issueIDs = resolvedIDs
|
||||||
|
|
||||||
processBatchLabelOperation(issueIDs, label, "added",
|
processBatchLabelOperation(issueIDs, label, "added", jsonOutput,
|
||||||
func(issueID, lbl string) error {
|
func(issueID, lbl string) error {
|
||||||
_, err := daemonClient.AddLabel(&rpc.LabelAddArgs{ID: issueID, Label: lbl})
|
_, err := daemonClient.AddLabel(&rpc.LabelAddArgs{ID: issueID, Label: lbl})
|
||||||
return err
|
return err
|
||||||
@@ -124,6 +125,7 @@ var labelRemoveCmd = &cobra.Command{
|
|||||||
Short: "Remove a label from one or more issues",
|
Short: "Remove a label from one or more issues",
|
||||||
Args: cobra.MinimumNArgs(2),
|
Args: cobra.MinimumNArgs(2),
|
||||||
Run: func(cmd *cobra.Command, args []string) {
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
|
jsonOutput, _ := cmd.Flags().GetBool("json")
|
||||||
issueIDs, label := parseLabelArgs(args)
|
issueIDs, label := parseLabelArgs(args)
|
||||||
|
|
||||||
// Resolve partial IDs
|
// Resolve partial IDs
|
||||||
@@ -152,7 +154,7 @@ var labelRemoveCmd = &cobra.Command{
|
|||||||
}
|
}
|
||||||
issueIDs = resolvedIDs
|
issueIDs = resolvedIDs
|
||||||
|
|
||||||
processBatchLabelOperation(issueIDs, label, "removed",
|
processBatchLabelOperation(issueIDs, label, "removed", jsonOutput,
|
||||||
func(issueID, lbl string) error {
|
func(issueID, lbl string) error {
|
||||||
_, err := daemonClient.RemoveLabel(&rpc.LabelRemoveArgs{ID: issueID, Label: lbl})
|
_, err := daemonClient.RemoveLabel(&rpc.LabelRemoveArgs{ID: issueID, Label: lbl})
|
||||||
return err
|
return err
|
||||||
@@ -168,6 +170,7 @@ var labelListCmd = &cobra.Command{
|
|||||||
Short: "List labels for an issue",
|
Short: "List labels for an issue",
|
||||||
Args: cobra.ExactArgs(1),
|
Args: cobra.ExactArgs(1),
|
||||||
Run: func(cmd *cobra.Command, args []string) {
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
|
jsonOutput, _ := cmd.Flags().GetBool("json")
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
|
|
||||||
// Resolve partial ID first
|
// Resolve partial ID first
|
||||||
@@ -242,6 +245,7 @@ var labelListAllCmd = &cobra.Command{
|
|||||||
Use: "list-all",
|
Use: "list-all",
|
||||||
Short: "List all unique labels in the database",
|
Short: "List all unique labels in the database",
|
||||||
Run: func(cmd *cobra.Command, args []string) {
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
|
jsonOutput, _ := cmd.Flags().GetBool("json")
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
|
|
||||||
var issues []*types.Issue
|
var issues []*types.Issue
|
||||||
@@ -342,6 +346,11 @@ var labelListAllCmd = &cobra.Command{
|
|||||||
}
|
}
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
|
labelAddCmd.Flags().Bool("json", false, "Output JSON format")
|
||||||
|
labelRemoveCmd.Flags().Bool("json", false, "Output JSON format")
|
||||||
|
labelListCmd.Flags().Bool("json", false, "Output JSON format")
|
||||||
|
labelListAllCmd.Flags().Bool("json", false, "Output JSON format")
|
||||||
|
|
||||||
labelCmd.AddCommand(labelAddCmd)
|
labelCmd.AddCommand(labelAddCmd)
|
||||||
labelCmd.AddCommand(labelRemoveCmd)
|
labelCmd.AddCommand(labelRemoveCmd)
|
||||||
labelCmd.AddCommand(labelListCmd)
|
labelCmd.AddCommand(labelListCmd)
|
||||||
|
|||||||
+7
-7
@@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -43,6 +43,7 @@ Example:
|
|||||||
|
|
||||||
sourceIDs := args
|
sourceIDs := args
|
||||||
dryRun, _ := cmd.Flags().GetBool("dry-run")
|
dryRun, _ := cmd.Flags().GetBool("dry-run")
|
||||||
|
jsonOutput, _ := cmd.Flags().GetBool("json")
|
||||||
|
|
||||||
// Validate merge operation
|
// Validate merge operation
|
||||||
if err := validateMerge(targetID, sourceIDs); err != nil {
|
if err := validateMerge(targetID, sourceIDs); err != nil {
|
||||||
@@ -96,6 +97,7 @@ Example:
|
|||||||
func init() {
|
func init() {
|
||||||
mergeCmd.Flags().String("into", "", "Target issue ID to merge into (required)")
|
mergeCmd.Flags().String("into", "", "Target issue ID to merge into (required)")
|
||||||
mergeCmd.Flags().Bool("dry-run", false, "Validate without making changes")
|
mergeCmd.Flags().Bool("dry-run", false, "Validate without making changes")
|
||||||
|
mergeCmd.Flags().Bool("json", false, "Output JSON format")
|
||||||
rootCmd.AddCommand(mergeCmd)
|
rootCmd.AddCommand(mergeCmd)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -206,6 +206,8 @@ var statsCmd = &cobra.Command{
|
|||||||
Use: "stats",
|
Use: "stats",
|
||||||
Short: "Show statistics",
|
Short: "Show statistics",
|
||||||
Run: func(cmd *cobra.Command, args []string) {
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
|
jsonOutput, _ := cmd.Flags().GetBool("json")
|
||||||
|
|
||||||
// If daemon is running, use RPC
|
// If daemon is running, use RPC
|
||||||
if daemonClient != nil {
|
if daemonClient != nil {
|
||||||
resp, err := daemonClient.Stats()
|
resp, err := daemonClient.Stats()
|
||||||
@@ -296,6 +298,8 @@ func init() {
|
|||||||
readyCmd.Flags().StringP("sort", "s", "hybrid", "Sort policy: hybrid (default), priority, oldest")
|
readyCmd.Flags().StringP("sort", "s", "hybrid", "Sort policy: hybrid (default), priority, oldest")
|
||||||
readyCmd.Flags().Bool("json", false, "Output JSON format")
|
readyCmd.Flags().Bool("json", false, "Output JSON format")
|
||||||
|
|
||||||
|
statsCmd.Flags().Bool("json", false, "Output JSON format")
|
||||||
|
|
||||||
rootCmd.AddCommand(readyCmd)
|
rootCmd.AddCommand(readyCmd)
|
||||||
rootCmd.AddCommand(blockedCmd)
|
rootCmd.AddCommand(blockedCmd)
|
||||||
rootCmd.AddCommand(statsCmd)
|
rootCmd.AddCommand(statsCmd)
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ This is more explicit than 'bd update --status open' and emits a Reopened event.
|
|||||||
Args: cobra.MinimumNArgs(1),
|
Args: cobra.MinimumNArgs(1),
|
||||||
Run: func(cmd *cobra.Command, args []string) {
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
reason, _ := cmd.Flags().GetString("reason")
|
reason, _ := cmd.Flags().GetString("reason")
|
||||||
|
jsonOutput, _ := cmd.Flags().GetBool("json")
|
||||||
|
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
|
|
||||||
@@ -146,5 +147,6 @@ This is more explicit than 'bd update --status open' and emits a Reopened event.
|
|||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
reopenCmd.Flags().StringP("reason", "r", "", "Reason for reopening")
|
reopenCmd.Flags().StringP("reason", "r", "", "Reason for reopening")
|
||||||
|
reopenCmd.Flags().Bool("json", false, "Output JSON format")
|
||||||
rootCmd.AddCommand(reopenCmd)
|
rootCmd.AddCommand(reopenCmd)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ var showCmd = &cobra.Command{
|
|||||||
Short: "Show issue details",
|
Short: "Show issue details",
|
||||||
Args: cobra.MinimumNArgs(1),
|
Args: cobra.MinimumNArgs(1),
|
||||||
Run: func(cmd *cobra.Command, args []string) {
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
|
jsonOutput, _ := cmd.Flags().GetBool("json")
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
|
|
||||||
// Resolve partial IDs first
|
// Resolve partial IDs first
|
||||||
@@ -329,6 +330,7 @@ var updateCmd = &cobra.Command{
|
|||||||
Short: "Update one or more issues",
|
Short: "Update one or more issues",
|
||||||
Args: cobra.MinimumNArgs(1),
|
Args: cobra.MinimumNArgs(1),
|
||||||
Run: func(cmd *cobra.Command, args []string) {
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
|
jsonOutput, _ := cmd.Flags().GetBool("json")
|
||||||
updates := make(map[string]interface{})
|
updates := make(map[string]interface{})
|
||||||
|
|
||||||
if cmd.Flags().Changed("status") {
|
if cmd.Flags().Changed("status") {
|
||||||
@@ -691,6 +693,7 @@ var closeCmd = &cobra.Command{
|
|||||||
if reason == "" {
|
if reason == "" {
|
||||||
reason = "Closed"
|
reason = "Closed"
|
||||||
}
|
}
|
||||||
|
jsonOutput, _ := cmd.Flags().GetBool("json")
|
||||||
|
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
|
|
||||||
@@ -776,6 +779,7 @@ var closeCmd = &cobra.Command{
|
|||||||
}
|
}
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
|
showCmd.Flags().Bool("json", false, "Output JSON format")
|
||||||
rootCmd.AddCommand(showCmd)
|
rootCmd.AddCommand(showCmd)
|
||||||
|
|
||||||
updateCmd.Flags().StringP("status", "s", "", "New status")
|
updateCmd.Flags().StringP("status", "s", "", "New status")
|
||||||
@@ -789,6 +793,7 @@ func init() {
|
|||||||
updateCmd.Flags().String("acceptance-criteria", "", "DEPRECATED: use --acceptance")
|
updateCmd.Flags().String("acceptance-criteria", "", "DEPRECATED: use --acceptance")
|
||||||
_ = updateCmd.Flags().MarkHidden("acceptance-criteria")
|
_ = updateCmd.Flags().MarkHidden("acceptance-criteria")
|
||||||
updateCmd.Flags().String("external-ref", "", "External reference (e.g., 'gh-9', 'jira-ABC')")
|
updateCmd.Flags().String("external-ref", "", "External reference (e.g., 'gh-9', 'jira-ABC')")
|
||||||
|
updateCmd.Flags().Bool("json", false, "Output JSON format")
|
||||||
rootCmd.AddCommand(updateCmd)
|
rootCmd.AddCommand(updateCmd)
|
||||||
|
|
||||||
editCmd.Flags().Bool("title", false, "Edit the title")
|
editCmd.Flags().Bool("title", false, "Edit the title")
|
||||||
@@ -799,5 +804,6 @@ func init() {
|
|||||||
rootCmd.AddCommand(editCmd)
|
rootCmd.AddCommand(editCmd)
|
||||||
|
|
||||||
closeCmd.Flags().StringP("reason", "r", "", "Reason for closing")
|
closeCmd.Flags().StringP("reason", "r", "", "Reason for closing")
|
||||||
|
closeCmd.Flags().Bool("json", false, "Output JSON format")
|
||||||
rootCmd.AddCommand(closeCmd)
|
rootCmd.AddCommand(closeCmd)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ Manage dependencies between beads issues.
|
|||||||
- $2: Issue ID
|
- $2: Issue ID
|
||||||
- Flags:
|
- Flags:
|
||||||
- `--reverse`: Show dependent tree (what was discovered from this) instead of dependency tree (what blocks this)
|
- `--reverse`: Show dependent tree (what was discovered from this) instead of dependency tree (what blocks this)
|
||||||
|
- `--format mermaid`: Output as Mermaid.js flowchart (renders in GitHub/GitLab markdown)
|
||||||
- `--json`: Output as JSON
|
- `--json`: Output as JSON
|
||||||
- `--max-depth N`: Limit tree depth (default: 50)
|
- `--max-depth N`: Limit tree depth (default: 50)
|
||||||
- `--show-all-paths`: Show all paths (no deduplication for diamond dependencies)
|
- `--show-all-paths`: Show all paths (no deduplication for diamond dependencies)
|
||||||
@@ -36,12 +37,45 @@ Manage dependencies between beads issues.
|
|||||||
- **parent-child**: Epic/subtask relationship
|
- **parent-child**: Epic/subtask relationship
|
||||||
- **discovered-from**: Track issues found during work
|
- **discovered-from**: Track issues found during work
|
||||||
|
|
||||||
|
## Mermaid Format
|
||||||
|
|
||||||
|
The `--format mermaid` option outputs the dependency tree as a Mermaid.js flowchart:
|
||||||
|
|
||||||
|
**Example:**
|
||||||
|
```bash
|
||||||
|
bd dep tree bd-1 --format mermaid
|
||||||
|
```
|
||||||
|
|
||||||
|
Output can be embedded in markdown:
|
||||||
|
|
||||||
|
````markdown
|
||||||
|
```mermaid
|
||||||
|
flowchart TD
|
||||||
|
bd-1["◧ bd-1: Main task"]
|
||||||
|
bd-2["☑ bd-2: Subtask"]
|
||||||
|
|
||||||
|
bd-1 --> bd-2
|
||||||
|
```
|
||||||
|
````
|
||||||
|
|
||||||
|
**Status Indicators:**
|
||||||
|
|
||||||
|
Each node includes a symbol indicator for quick visual status identification:
|
||||||
|
|
||||||
|
- ☐ **Open** - Not started yet (empty checkbox)
|
||||||
|
- ◧ **In Progress** - Currently being worked on (half-filled box)
|
||||||
|
- ⚠ **Blocked** - Waiting on something (warning sign)
|
||||||
|
- ☑ **Closed** - Completed! (checked checkbox)
|
||||||
|
|
||||||
|
The diagram colors are determined by your Mermaid theme (default, dark, forest, neutral, or base). Mermaid diagrams render natively in GitHub, GitLab, VSCode markdown preview, and can be imported to Miro.
|
||||||
|
|
||||||
## Examples
|
## Examples
|
||||||
|
|
||||||
- `bd dep add bd-10 bd-20 --type blocks`: bd-10 blocks bd-20
|
- `bd dep add bd-10 bd-20 --type blocks`: bd-10 blocks bd-20
|
||||||
- `bd dep tree bd-20`: Show what blocks bd-20 (dependency tree going UP)
|
- `bd dep tree bd-20`: Show what blocks bd-20 (dependency tree going UP)
|
||||||
- `bd dep tree bd-1 --reverse`: Show what was discovered from bd-1 (dependent tree going DOWN)
|
- `bd dep tree bd-1 --reverse`: Show what was discovered from bd-1 (dependent tree going DOWN)
|
||||||
- `bd dep tree bd-1 --reverse --max-depth 3`: Show discovery tree with depth limit
|
- `bd dep tree bd-1 --reverse --max-depth 3`: Show discovery tree with depth limit
|
||||||
|
- `bd dep tree bd-20 --format mermaid > tree.md`: Generate Mermaid diagram for documentation
|
||||||
- `bd dep cycles`: Check for circular dependencies
|
- `bd dep cycles`: Check for circular dependencies
|
||||||
|
|
||||||
## Reverse Mode: Discovery Trees
|
## Reverse Mode: Discovery Trees
|
||||||
|
|||||||
+7
-7
@@ -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 {
|
||||||
@@ -206,24 +206,12 @@ func handleCollisions(ctx context.Context, sqliteStore *sqlite.SQLiteStorage, is
|
|||||||
result.CollisionIDs = append(result.CollisionIDs, collision.ID)
|
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 {
|
if len(collisionResult.Collisions) > 0 {
|
||||||
// Hash-based IDs make collisions extremely unlikely (same ID = same content)
|
result.Updated = len(collisionResult.Collisions)
|
||||||
// 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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Phase 4: Renames removed - obsolete with hash IDs (bd-8e05)
|
// 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
|
// 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
|
||||||
|
|||||||
@@ -616,6 +616,7 @@ func (m *MemoryStorage) SetJSONLFileHash(ctx context.Context, fileHash string) e
|
|||||||
// GetDependencyTree gets the dependency tree for an issue
|
// 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) {
|
func (m *MemoryStorage) GetDependencyTree(ctx context.Context, issueID string, maxDepth int, showAllPaths bool, reverse bool) ([]*types.TreeNode, error) {
|
||||||
// Simplified implementation - just return direct dependencies
|
// 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)
|
deps, err := m.GetDependencies(ctx, issueID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|||||||
@@ -431,7 +431,7 @@ func (s *SQLiteStorage) GetDependencyTree(ctx context.Context, issueID string, m
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to scan tree node: %w", err)
|
return nil, fmt.Errorf("failed to scan tree node: %w", err)
|
||||||
}
|
}
|
||||||
_ = parentID // Silence unused variable warning
|
node.ParentID = parentID
|
||||||
|
|
||||||
if closedAt.Valid {
|
if closedAt.Valid {
|
||||||
node.ClosedAt = &closedAt.Time
|
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.
|
// 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()
|
||||||
|
|||||||
@@ -222,8 +222,9 @@ type BlockedIssue struct {
|
|||||||
// TreeNode represents a node in a dependency tree
|
// TreeNode represents a node in a dependency tree
|
||||||
type TreeNode struct {
|
type TreeNode struct {
|
||||||
Issue
|
Issue
|
||||||
Depth int `json:"depth"`
|
Depth int `json:"depth"`
|
||||||
Truncated bool `json:"truncated"`
|
ParentID string `json:"parent_id"`
|
||||||
|
Truncated bool `json:"truncated"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Statistics provides aggregate metrics
|
// Statistics provides aggregate metrics
|
||||||
|
|||||||
Reference in New Issue
Block a user