Merge branch 'main' of github.com:steveyegge/beads
# Conflicts: # .beads/beads.jsonl
This commit is contained in:
File diff suppressed because one or more lines are too long
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:**
|
||||
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
|
||||
# After git merge or pull
|
||||
bd import -i .beads/issues.jsonl --dry-run
|
||||
|
||||
# Output shows:
|
||||
# === Collision Detection Report ===
|
||||
# Exact matches (idempotent): 15
|
||||
# New issues: 5
|
||||
# COLLISIONS DETECTED: 3
|
||||
# Updates: 3
|
||||
#
|
||||
# Colliding issues:
|
||||
# bd-10: Fix authentication (conflicting fields: [title, priority])
|
||||
# bd-12: Add feature (conflicting fields: [description, status])
|
||||
# Issues to be updated:
|
||||
# bd-a3f2: Fix authentication (changed: priority, 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
|
||||
# Let bd resolve collisions by remapping incoming issues to new IDs
|
||||
bd import -i .beads/issues.jsonl --resolve-collisions
|
||||
# After git merge creates conflict
|
||||
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:
|
||||
# - 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
|
||||
# Import the resolved JSONL
|
||||
bd import -i .beads/beads.jsonl
|
||||
|
||||
# Commit the merge
|
||||
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
|
||||
|
||||
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
|
||||
- Works as a Git/jujutsu merge driver
|
||||
|
||||
**Two types of conflicts, two tools:**
|
||||
- **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
|
||||
After using beads-merge to resolve the git conflict, just run `bd import` to update your database.
|
||||
|
||||
## Custom Git Hooks
|
||||
|
||||
|
||||
78
AGENTS.md
78
AGENTS.md
@@ -147,6 +147,12 @@ bd create "Issue title" -t bug -p 1 -l bug,critical --json
|
||||
# Create multiple issues from markdown file
|
||||
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
|
||||
bd update <id> [<id>...] --status in_progress --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
|
||||
bd restore <id> # View full history at time of compaction
|
||||
|
||||
# Import with collision detection
|
||||
bd import -i .beads/issues.jsonl --dry-run # Preview only
|
||||
bd import -i .beads/issues.jsonl --resolve-collisions # Auto-resolve
|
||||
bd import -i .beads/issues.jsonl --resolve-collisions --dedupe-after # Auto-resolve + detect duplicates
|
||||
# Import issues from JSONL
|
||||
bd import -i .beads/issues.jsonl --dry-run # Preview changes
|
||||
bd import -i .beads/issues.jsonl # Import and update issues
|
||||
bd import -i .beads/issues.jsonl --dedupe-after # Import + detect duplicates
|
||||
|
||||
# Find and merge duplicate issues
|
||||
bd duplicates # Show all duplicates
|
||||
@@ -335,9 +341,11 @@ bd daemons killall # Restart with default (poll) mode
|
||||
- `bug` - Something broken that needs fixing
|
||||
- `feature` - New functionality
|
||||
- `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)
|
||||
|
||||
**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
|
||||
|
||||
- `0` - Critical (security, data loss, broken builds)
|
||||
@@ -371,8 +379,8 @@ bd duplicates --auto-merge
|
||||
# Preview what would be merged
|
||||
bd duplicates --dry-run
|
||||
|
||||
# During import (after collision resolution)
|
||||
bd import -i issues.jsonl --resolve-collisions --dedupe-after
|
||||
# During import
|
||||
bd import -i issues.jsonl --dedupe-after
|
||||
```
|
||||
|
||||
**Detection strategies:**
|
||||
@@ -557,42 +565,30 @@ bd automatically detects when you're in a worktree and shows a prominent warning
|
||||
**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.
|
||||
|
||||
### 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
|
||||
# After git merge or pull
|
||||
bd import -i .beads/issues.jsonl --dry-run
|
||||
# After git merge creates conflict
|
||||
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:
|
||||
# === Collision Detection Report ===
|
||||
# Exact matches (idempotent): 15
|
||||
# New issues: 5
|
||||
# COLLISIONS DETECTED: 3
|
||||
#
|
||||
# Colliding issues:
|
||||
# bd-10: Fix authentication (conflicting fields: [title, priority])
|
||||
# bd-12: Add feature (conflicting fields: [description, status])
|
||||
# Import the resolved JSONL
|
||||
bd import -i .beads/beads.jsonl
|
||||
|
||||
# Commit the merge
|
||||
git add .beads/beads.jsonl
|
||||
git commit
|
||||
```
|
||||
|
||||
**Resolve collisions automatically:**
|
||||
```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`.
|
||||
**bd automatically handles updates** - same ID with different content is a normal update operation. No special flags needed.
|
||||
|
||||
### 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
|
||||
- Works as a Git/jujutsu merge driver
|
||||
|
||||
**Two types of conflicts, two tools:**
|
||||
- **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
|
||||
**Beads-merge** helps with intelligent field-level merging during git merge. After resolving, just `bd import` to update your database.
|
||||
|
||||
## 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 `bd dep tree` to understand complex dependencies
|
||||
- Priority 0-1 issues are usually more important than 2-4
|
||||
- Use `--dry-run` to preview import collisions before resolving
|
||||
- Use `--resolve-collisions` for safe automatic branch merges
|
||||
- Use `--dry-run` to preview import changes before applying
|
||||
- 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`)
|
||||
|
||||
## Building and Testing
|
||||
|
||||
77
FAQ.md
77
FAQ.md
@@ -83,6 +83,74 @@ Follow the repo for updates and the path to 1.0!
|
||||
|
||||
## 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?
|
||||
|
||||
**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).
|
||||
|
||||
For ID collisions (same ID, different content):
|
||||
```bash
|
||||
bd import -i .beads/issues.jsonl --resolve-collisions
|
||||
```
|
||||
**With hash-based IDs (v0.20.1+), same-ID scenarios are updates, not 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
|
||||
|
||||
|
||||
@@ -22,6 +22,36 @@ go build -o bd ./cmd/bd
|
||||
./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
|
||||
|
||||
```bash
|
||||
|
||||
41
README.md
41
README.md
@@ -271,6 +271,47 @@ bd info
|
||||
|
||||
**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
|
||||
|
||||
### Health Check
|
||||
|
||||
@@ -187,19 +187,24 @@ bd import -i .beads/issues.jsonl # Sync to SQLite
|
||||
|
||||
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
|
||||
# Check for collisions
|
||||
# Preview what will be updated
|
||||
bd import -i .beads/issues.jsonl --dry-run
|
||||
|
||||
# Automatically resolve collisions
|
||||
bd import -i .beads/issues.jsonl --resolve-collisions
|
||||
# Resolve git conflict (keep newer version or manually merge)
|
||||
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
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/fatih/color"
|
||||
"github.com/spf13/cobra"
|
||||
@@ -273,6 +274,7 @@ var depTreeCmd = &cobra.Command{
|
||||
showAllPaths, _ := cmd.Flags().GetBool("show-all-paths")
|
||||
maxDepth, _ := cmd.Flags().GetInt("max-depth")
|
||||
reverse, _ := cmd.Flags().GetBool("reverse")
|
||||
formatStr, _ := cmd.Flags().GetString("format")
|
||||
|
||||
if maxDepth < 1 {
|
||||
fmt.Fprintf(os.Stderr, "Error: --max-depth must be >= 1\n")
|
||||
@@ -285,6 +287,12 @@ var depTreeCmd = &cobra.Command{
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// Handle mermaid format
|
||||
if formatStr == "mermaid" {
|
||||
outputMermaidTree(tree, args[0])
|
||||
return
|
||||
}
|
||||
|
||||
if jsonOutput {
|
||||
// Always output array, even if empty
|
||||
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() {
|
||||
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().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().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(depRemoveCmd)
|
||||
depCmd.AddCommand(depTreeCmd)
|
||||
|
||||
@@ -1,9 +1,13 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
@@ -266,3 +270,217 @@ func TestDepRemove(t *testing.T) {
|
||||
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")
|
||||
dryRun, _ := cmd.Flags().GetBool("dry-run")
|
||||
jsonOutput, _ := cmd.Flags().GetBool("json")
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
@@ -174,6 +175,7 @@ Example:
|
||||
func init() {
|
||||
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("json", false, "Output JSON format")
|
||||
rootCmd.AddCommand(duplicatesCmd)
|
||||
}
|
||||
|
||||
|
||||
@@ -22,7 +22,7 @@ var labelCmd = &cobra.Command{
|
||||
}
|
||||
|
||||
// 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) {
|
||||
ctx := context.Background()
|
||||
results := []map[string]interface{}{}
|
||||
@@ -40,7 +40,7 @@ func processBatchLabelOperation(issueIDs []string, label string, operation strin
|
||||
continue
|
||||
}
|
||||
|
||||
if jsonOutput {
|
||||
if jsonOut {
|
||||
results = append(results, map[string]interface{}{
|
||||
"status": operation,
|
||||
"issue_id": issueID,
|
||||
@@ -62,7 +62,7 @@ func processBatchLabelOperation(issueIDs []string, label string, operation strin
|
||||
markDirtyAndScheduleFlush()
|
||||
}
|
||||
|
||||
if jsonOutput && len(results) > 0 {
|
||||
if jsonOut && len(results) > 0 {
|
||||
outputJSON(results)
|
||||
}
|
||||
}
|
||||
@@ -79,6 +79,7 @@ var labelAddCmd = &cobra.Command{
|
||||
Short: "Add a label to one or more issues",
|
||||
Args: cobra.MinimumNArgs(2),
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
jsonOutput, _ := cmd.Flags().GetBool("json")
|
||||
issueIDs, label := parseLabelArgs(args)
|
||||
|
||||
// Resolve partial IDs
|
||||
@@ -107,7 +108,7 @@ var labelAddCmd = &cobra.Command{
|
||||
}
|
||||
issueIDs = resolvedIDs
|
||||
|
||||
processBatchLabelOperation(issueIDs, label, "added",
|
||||
processBatchLabelOperation(issueIDs, label, "added", jsonOutput,
|
||||
func(issueID, lbl string) error {
|
||||
_, err := daemonClient.AddLabel(&rpc.LabelAddArgs{ID: issueID, Label: lbl})
|
||||
return err
|
||||
@@ -124,6 +125,7 @@ var labelRemoveCmd = &cobra.Command{
|
||||
Short: "Remove a label from one or more issues",
|
||||
Args: cobra.MinimumNArgs(2),
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
jsonOutput, _ := cmd.Flags().GetBool("json")
|
||||
issueIDs, label := parseLabelArgs(args)
|
||||
|
||||
// Resolve partial IDs
|
||||
@@ -152,7 +154,7 @@ var labelRemoveCmd = &cobra.Command{
|
||||
}
|
||||
issueIDs = resolvedIDs
|
||||
|
||||
processBatchLabelOperation(issueIDs, label, "removed",
|
||||
processBatchLabelOperation(issueIDs, label, "removed", jsonOutput,
|
||||
func(issueID, lbl string) error {
|
||||
_, err := daemonClient.RemoveLabel(&rpc.LabelRemoveArgs{ID: issueID, Label: lbl})
|
||||
return err
|
||||
@@ -168,6 +170,7 @@ var labelListCmd = &cobra.Command{
|
||||
Short: "List labels for an issue",
|
||||
Args: cobra.ExactArgs(1),
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
jsonOutput, _ := cmd.Flags().GetBool("json")
|
||||
ctx := context.Background()
|
||||
|
||||
// Resolve partial ID first
|
||||
@@ -242,6 +245,7 @@ var labelListAllCmd = &cobra.Command{
|
||||
Use: "list-all",
|
||||
Short: "List all unique labels in the database",
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
jsonOutput, _ := cmd.Flags().GetBool("json")
|
||||
ctx := context.Background()
|
||||
|
||||
var issues []*types.Issue
|
||||
@@ -342,6 +346,11 @@ var labelListAllCmd = &cobra.Command{
|
||||
}
|
||||
|
||||
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(labelRemoveCmd)
|
||||
labelCmd.AddCommand(labelListCmd)
|
||||
|
||||
@@ -836,9 +836,9 @@ func TestAutoImportDisabled(t *testing.T) {
|
||||
storeMutex.Unlock()
|
||||
}
|
||||
|
||||
// TestAutoImportWithCollision tests that auto-import detects collisions and preserves local changes
|
||||
func TestAutoImportWithCollision(t *testing.T) {
|
||||
tmpDir, err := os.MkdirTemp("", "bd-test-collision-*")
|
||||
// TestAutoImportWithUpdate tests that auto-import detects same-ID updates and applies them
|
||||
func TestAutoImportWithUpdate(t *testing.T) {
|
||||
tmpDir, err := os.MkdirTemp("", "bd-test-update-*")
|
||||
if err != nil {
|
||||
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)
|
||||
}
|
||||
|
||||
// Create JSONL with same ID but status=open (conflict)
|
||||
// Create JSONL with same ID but status=open (update scenario)
|
||||
jsonlIssue := &types.Issue{
|
||||
ID: "test-col-1",
|
||||
Title: "Remote version",
|
||||
@@ -911,9 +911,9 @@ func TestAutoImportWithCollision(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestAutoImportNoCollision tests happy path with no conflicts
|
||||
func TestAutoImportNoCollision(t *testing.T) {
|
||||
tmpDir, err := os.MkdirTemp("", "bd-test-nocoll-*")
|
||||
// TestAutoImportNoUpdate tests happy path with no updates needed
|
||||
func TestAutoImportNoUpdate(t *testing.T) {
|
||||
tmpDir, err := os.MkdirTemp("", "bd-test-noupdate-*")
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create temp dir: %v", err)
|
||||
}
|
||||
|
||||
@@ -43,6 +43,7 @@ Example:
|
||||
|
||||
sourceIDs := args
|
||||
dryRun, _ := cmd.Flags().GetBool("dry-run")
|
||||
jsonOutput, _ := cmd.Flags().GetBool("json")
|
||||
|
||||
// Validate merge operation
|
||||
if err := validateMerge(targetID, sourceIDs); err != nil {
|
||||
@@ -96,6 +97,7 @@ Example:
|
||||
func init() {
|
||||
mergeCmd.Flags().String("into", "", "Target issue ID to merge into (required)")
|
||||
mergeCmd.Flags().Bool("dry-run", false, "Validate without making changes")
|
||||
mergeCmd.Flags().Bool("json", false, "Output JSON format")
|
||||
rootCmd.AddCommand(mergeCmd)
|
||||
}
|
||||
|
||||
|
||||
@@ -206,6 +206,8 @@ var statsCmd = &cobra.Command{
|
||||
Use: "stats",
|
||||
Short: "Show statistics",
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
jsonOutput, _ := cmd.Flags().GetBool("json")
|
||||
|
||||
// If daemon is running, use RPC
|
||||
if daemonClient != nil {
|
||||
resp, err := daemonClient.Stats()
|
||||
@@ -296,6 +298,8 @@ func init() {
|
||||
readyCmd.Flags().StringP("sort", "s", "hybrid", "Sort policy: hybrid (default), priority, oldest")
|
||||
readyCmd.Flags().Bool("json", false, "Output JSON format")
|
||||
|
||||
statsCmd.Flags().Bool("json", false, "Output JSON format")
|
||||
|
||||
rootCmd.AddCommand(readyCmd)
|
||||
rootCmd.AddCommand(blockedCmd)
|
||||
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),
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
reason, _ := cmd.Flags().GetString("reason")
|
||||
jsonOutput, _ := cmd.Flags().GetBool("json")
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
@@ -146,5 +147,6 @@ This is more explicit than 'bd update --status open' and emits a Reopened event.
|
||||
|
||||
func init() {
|
||||
reopenCmd.Flags().StringP("reason", "r", "", "Reason for reopening")
|
||||
reopenCmd.Flags().Bool("json", false, "Output JSON format")
|
||||
rootCmd.AddCommand(reopenCmd)
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@ var showCmd = &cobra.Command{
|
||||
Short: "Show issue details",
|
||||
Args: cobra.MinimumNArgs(1),
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
jsonOutput, _ := cmd.Flags().GetBool("json")
|
||||
ctx := context.Background()
|
||||
|
||||
// Resolve partial IDs first
|
||||
@@ -329,6 +330,7 @@ var updateCmd = &cobra.Command{
|
||||
Short: "Update one or more issues",
|
||||
Args: cobra.MinimumNArgs(1),
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
jsonOutput, _ := cmd.Flags().GetBool("json")
|
||||
updates := make(map[string]interface{})
|
||||
|
||||
if cmd.Flags().Changed("status") {
|
||||
@@ -691,6 +693,7 @@ var closeCmd = &cobra.Command{
|
||||
if reason == "" {
|
||||
reason = "Closed"
|
||||
}
|
||||
jsonOutput, _ := cmd.Flags().GetBool("json")
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
@@ -776,6 +779,7 @@ var closeCmd = &cobra.Command{
|
||||
}
|
||||
|
||||
func init() {
|
||||
showCmd.Flags().Bool("json", false, "Output JSON format")
|
||||
rootCmd.AddCommand(showCmd)
|
||||
|
||||
updateCmd.Flags().StringP("status", "s", "", "New status")
|
||||
@@ -789,6 +793,7 @@ func init() {
|
||||
updateCmd.Flags().String("acceptance-criteria", "", "DEPRECATED: use --acceptance")
|
||||
_ = updateCmd.Flags().MarkHidden("acceptance-criteria")
|
||||
updateCmd.Flags().String("external-ref", "", "External reference (e.g., 'gh-9', 'jira-ABC')")
|
||||
updateCmd.Flags().Bool("json", false, "Output JSON format")
|
||||
rootCmd.AddCommand(updateCmd)
|
||||
|
||||
editCmd.Flags().Bool("title", false, "Edit the title")
|
||||
@@ -799,5 +804,6 @@ func init() {
|
||||
rootCmd.AddCommand(editCmd)
|
||||
|
||||
closeCmd.Flags().StringP("reason", "r", "", "Reason for closing")
|
||||
closeCmd.Flags().Bool("json", false, "Output JSON format")
|
||||
rootCmd.AddCommand(closeCmd)
|
||||
}
|
||||
|
||||
@@ -23,6 +23,7 @@ Manage dependencies between beads issues.
|
||||
- $2: Issue ID
|
||||
- Flags:
|
||||
- `--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
|
||||
- `--max-depth N`: Limit tree depth (default: 50)
|
||||
- `--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
|
||||
- **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
|
||||
|
||||
- `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-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-20 --format mermaid > tree.md`: Generate Mermaid diagram for documentation
|
||||
- `bd dep cycles`: Check for circular dependencies
|
||||
|
||||
## Reverse Mode: Discovery Trees
|
||||
|
||||
@@ -10,21 +10,21 @@ Import issues from JSON Lines format (one JSON object per line).
|
||||
- **From stdin**: `bd import` (reads from stdin)
|
||||
- **From file**: `bd import -i issues.jsonl`
|
||||
- **Preview**: `bd import -i issues.jsonl --dry-run`
|
||||
- **Resolve collisions**: `bd import -i issues.jsonl --resolve-collisions`
|
||||
|
||||
## Behavior
|
||||
|
||||
- **Existing issues** (same ID): Updated with new data
|
||||
- **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
|
||||
- **--resolve-collisions**: Automatically remap colliding issues to new IDs
|
||||
- All text references and dependencies are automatically updated
|
||||
```bash
|
||||
bd import -i issues.jsonl --dry-run
|
||||
# Shows: new issues, updates, exact matches
|
||||
```
|
||||
|
||||
## Automatic Import
|
||||
|
||||
|
||||
@@ -263,32 +263,25 @@ Import issues from JSONL format.
|
||||
|
||||
```bash
|
||||
bd import < issues.jsonl
|
||||
bd import --resolve-collisions < issues.jsonl
|
||||
bd import -i issues.jsonl --dry-run # Preview changes
|
||||
```
|
||||
|
||||
**Flags:**
|
||||
- `--resolve-collisions` - Automatically remap conflicting issue IDs
|
||||
**Behavior with hash-based IDs (v0.20.1+):**
|
||||
- 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:**
|
||||
- **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:**
|
||||
**Use `--dry-run` to preview:**
|
||||
```bash
|
||||
# You have: mit-1 (open) in database
|
||||
# Importing: mit-1 (closed) from JSONL
|
||||
# Result: Import creates bd-4 with closed status, preserves existing mit-1
|
||||
bd import -i issues.jsonl --dry-run
|
||||
# Shows: new issues, updates, exact matches
|
||||
```
|
||||
|
||||
**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
|
||||
|
||||
@@ -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:
|
||||
|
||||
```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:
|
||||
- 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
|
||||
|
||||
**Note:** With hash-based IDs (v0.20.1+), ID collisions don't occur - different issues get different hash IDs.
|
||||
|
||||
## Compatibility
|
||||
|
||||
- **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)
|
||||
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
|
||||
|
||||
# Import with collision resolution if needed
|
||||
bd import -i all-issues.jsonl --resolve-collisions
|
||||
# Import issues
|
||||
bd import -i all-issues.jsonl
|
||||
|
||||
# View stats
|
||||
bd stats
|
||||
|
||||
@@ -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