Merge branch 'main' of github.com:steveyegge/beads

This commit is contained in:
Steve Yegge
2025-11-06 19:16:48 -08:00
10 changed files with 251 additions and 387 deletions

24
.beads/.gitignore vendored Normal file
View File

@@ -0,0 +1,24 @@
# SQLite databases
*.db
*.db-journal
*.db-wal
*.db-shm
# Daemon runtime files
daemon.lock
daemon.log
daemon.pid
bd.sock
# Legacy database files
db.sqlite
bd.db
# Keep JSONL exports and config (source of truth for git)
!*.jsonl
!metadata.json
!config.json
# Exclude merge artifacts (temporary files from 3-way merge)
beads.base.jsonl
beads.left.jsonl

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -95,10 +95,18 @@ Output to stdout by default, or use -o flag for file output.`,
os.Exit(1) os.Exit(1)
} }
// Export command doesn't work with daemon - need direct access // Export command requires direct database access for consistent snapshot
// If daemon is connected, close it and open direct connection
if daemonClient != nil {
if os.Getenv("BD_DEBUG") != "" {
fmt.Fprintf(os.Stderr, "Debug: export command forcing direct mode (closes daemon connection)\n")
}
_ = daemonClient.Close()
daemonClient = nil
}
// Ensure we have a direct store connection // Ensure we have a direct store connection
if store == nil { if store == nil {
// Initialize store directly even if daemon is running
var err error var err error
if dbPath == "" { if dbPath == "" {
fmt.Fprintf(os.Stderr, "Error: no database path found\n") fmt.Fprintf(os.Stderr, "Error: no database path found\n")

View File

@@ -10,6 +10,7 @@ import (
"strings" "strings"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"github.com/steveyegge/beads/internal/storage/sqlite"
"github.com/steveyegge/beads/internal/types" "github.com/steveyegge/beads/internal/types"
) )
@@ -25,8 +26,30 @@ Behavior:
- New issues are created - New issues are created
- Collisions (same ID, different content) are detected and reported - Collisions (same ID, different content) are detected and reported
- Use --dedupe-after to find and merge content duplicates after import - Use --dedupe-after to find and merge content duplicates after import
- Use --dry-run to preview changes without applying them`, - Use --dry-run to preview changes without applying them
NOTE: Import requires direct database access and does not work with daemon mode.
The command automatically uses --no-daemon when executed.`,
Run: func(cmd *cobra.Command, args []string) { Run: func(cmd *cobra.Command, args []string) {
// Import requires direct database access due to complex transaction handling
// and collision detection. Force direct mode regardless of daemon state.
if daemonClient != nil {
if os.Getenv("BD_DEBUG") != "" {
fmt.Fprintf(os.Stderr, "Debug: import command forcing direct mode (closes daemon connection)\n")
}
_ = daemonClient.Close()
daemonClient = nil
// Now initialize direct store
var err error
store, err = sqlite.New(dbPath)
if err != nil {
fmt.Fprintf(os.Stderr, "Error: failed to open database: %v\n", err)
os.Exit(1)
}
defer func() { _ = store.Close() }()
}
input, _ := cmd.Flags().GetString("input") input, _ := cmd.Flags().GetString("input")
skipUpdate, _ := cmd.Flags().GetBool("skip-existing") skipUpdate, _ := cmd.Flags().GetBool("skip-existing")
strict, _ := cmd.Flags().GetBool("strict") strict, _ := cmd.Flags().GetBool("strict")

View File

@@ -326,21 +326,29 @@ func replaceIDReferences(text string, mapping map[string]string) string {
// isHashID checks if an ID is hash-based (not sequential) // isHashID checks if an ID is hash-based (not sequential)
func isHashID(id string) bool { func isHashID(id string) bool {
// Hash IDs contain hex characters after the prefix // Hash IDs contain hex letters (a-f), sequential IDs are only digits
// Sequential IDs are only digits // May have hierarchical suffix like .1 or .1.2
parts := strings.SplitN(id, "-", 2) parts := strings.SplitN(id, "-", 2)
if len(parts) != 2 { if len(parts) != 2 {
return false return false
} }
// Check if the suffix starts with a hex digit (a-f)
suffix := parts[1] suffix := parts[1]
if len(suffix) == 0 { // Strip hierarchical suffix like .1 or .1.2
baseSuffix := strings.Split(suffix, ".")[0]
if len(baseSuffix) == 0 {
return false return false
} }
// If it contains any letter a-f, it's a hash ID // Must be valid hex
return regexp.MustCompile(`[a-f]`).MatchString(suffix) if !regexp.MustCompile(`^[0-9a-f]+$`).MatchString(baseSuffix) {
return false
}
// If it contains letters a-f, it's a hash ID
// Sequential IDs are only digits 0-9
return regexp.MustCompile(`[a-f]`).MatchString(baseSuffix)
} }
// saveMappingFile saves the ID mapping to a JSON file // saveMappingFile saves the ID mapping to a JSON file

View File

@@ -1,21 +1,29 @@
# bd Git Hooks # bd Git Hooks
This directory contains git hooks that integrate bd (beads) with your git workflow, solving the race condition between daemon auto-flush and git commits. This directory contains git hooks that integrate bd (beads) with your git workflow, preventing stale JSONL from being pushed to remote.
## The Problem ## The Problem
When using bd in daemon mode, operations trigger a 5-second debounced auto-flush to JSONL. This creates a race condition: Two race conditions can occur:
1. User closes issue via MCP → daemon schedules flush (5 sec delay) 1. **Between operations and commits**: Daemon auto-flush (5s debounce) may fire after commit
2. User commits code changes → JSONL appears clean - User closes issue via MCP → daemon schedules flush (5 sec delay)
3. Daemon flush fires → JSONL modified after commit - User commits code changes → JSONL appears clean
4. Result: dirty working tree showing JSONL changes - Daemon flush fires → JSONL modified after commit
- Result: dirty working tree showing JSONL changes
2. **Between commits and pushes**: Changes made after commit but before push (bd-my64)
- User commits → pre-commit hook flushes JSONL
- User adds comments or updates issues
- User pushes → outdated JSONL is pushed
- Result: remote has stale JSONL
## The Solution ## The Solution
These git hooks ensure bd changes are always synchronized with your commits: These git hooks ensure bd changes are always synchronized with your commits and pushes:
- **pre-commit** - Flushes pending bd changes to JSONL before commit - **pre-commit** - Flushes pending bd changes to JSONL before commit and stages it
- **pre-push** - Blocks push if JSONL has uncommitted changes (bd-my64)
- **post-merge** - Imports updated JSONL after git pull/merge - **post-merge** - Imports updated JSONL after git pull/merge
## Installation ## Installation
@@ -37,8 +45,9 @@ This will:
```bash ```bash
cp examples/git-hooks/pre-commit .git/hooks/pre-commit cp examples/git-hooks/pre-commit .git/hooks/pre-commit
cp examples/git-hooks/pre-push .git/hooks/pre-push
cp examples/git-hooks/post-merge .git/hooks/post-merge cp examples/git-hooks/post-merge .git/hooks/post-merge
chmod +x .git/hooks/pre-commit .git/hooks/post-merge chmod +x .git/hooks/pre-commit .git/hooks/pre-push .git/hooks/post-merge
``` ```
## How It Works ## How It Works
@@ -58,16 +67,33 @@ This:
The hook is silent on success, fast (no git operations), and safe (fails commit if flush fails). The hook is silent on success, fast (no git operations), and safe (fails commit if flush fails).
### pre-push
Before each push, the hook:
```bash
bd sync --flush-only # Flush pending changes (if bd available)
git status --porcelain .beads/*.jsonl # Check for uncommitted changes
```
This prevents pushing stale JSONL by:
1. Flushing pending in-memory changes from daemon's 5s debounce
2. Checking for uncommitted changes (staged, unstaged, untracked, deleted)
3. Failing the push with clear error message if changes exist
4. Instructing user to commit JSONL before pushing again
This solves bd-my64: changes made between commit and push (or pending debounced flushes) are caught before reaching remote.
### post-merge ### post-merge
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 bd import -i .beads/beads.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/beads.jsonl` exists (also checks `issues.jsonl` for backward compat)
- Imports any new issues or updates from the merge - 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
@@ -92,7 +118,7 @@ This ensures your local database reflects the merged state. The hook:
Remove the hooks: Remove the hooks:
```bash ```bash
rm .git/hooks/pre-commit .git/hooks/post-merge rm .git/hooks/pre-commit .git/hooks/pre-push .git/hooks/post-merge
``` ```
Your backed-up hooks (if any) are in `.git/hooks/*.backup-*`. Your backed-up hooks (if any) are in `.git/hooks/*.backup-*`.

View File

@@ -59,7 +59,7 @@ echo "✓ Git hooks installed successfully"
echo "" echo ""
echo "Hooks installed:" echo "Hooks installed:"
echo " pre-commit - Flushes pending bd changes to JSONL before commit" echo " pre-commit - Flushes pending bd changes to JSONL before commit"
echo " pre-push - Blocks push if JSONL has uncommitted changes (bd-my64)"
echo " post-merge - Imports updated JSONL after git pull/merge" echo " post-merge - Imports updated JSONL after git pull/merge"
echo " pre-push - Exports database to JSONL before push (prevents stale JSONL)"
echo "" echo ""
echo "To uninstall, remove the hooks from .git/hooks/" echo "To uninstall, remove the hooks from .git/hooks/"

View File

@@ -4,7 +4,7 @@
# bd (beads) pre-commit hook # bd (beads) pre-commit hook
# #
# This hook ensures that any pending bd issue changes are flushed to # This hook ensures that any pending bd issue changes are flushed to
# .beads/issues.jsonl before the commit is created, preventing the # .beads/beads.jsonl before the commit is created, preventing the
# race condition where daemon auto-flush fires after the commit. # race condition where daemon auto-flush fires after the commit.
# #
# Installation: # Installation:
@@ -35,9 +35,10 @@ if ! bd sync --flush-only >/dev/null 2>&1; then
exit 1 exit 1
fi fi
# If the JSONL file was modified, stage it # Stage both possible JSONL files (backward compatibility)
if [ -f .beads/issues.jsonl ]; then # git add is harmless if file doesn't exist
git add .beads/issues.jsonl 2>/dev/null || true for f in .beads/beads.jsonl .beads/issues.jsonl; do
fi [ -f "$f" ] && git add "$f" 2>/dev/null || true
done
exit 0 exit 0

View File

@@ -3,9 +3,14 @@
# #
# bd (beads) pre-push hook # bd (beads) pre-push hook
# #
# This hook ensures that the database is exported to JSONL before pushing, # This hook prevents pushing stale JSONL by:
# preventing the problem where database changes are committed without # 1. Flushing any pending in-memory changes to JSONL (if bd available)
# corresponding JSONL updates. # 2. Checking for uncommitted changes (staged, unstaged, untracked, deleted)
# 3. Failing the push with clear instructions if changes found
#
# The pre-commit hook already exports changes, but this catches:
# - Changes made between commit and push
# - Pending debounced flushes (5s daemon delay)
# #
# Installation: # Installation:
# cp examples/git-hooks/pre-push .git/hooks/pre-push # cp examples/git-hooks/pre-push .git/hooks/pre-push
@@ -14,38 +19,43 @@
# Or use the install script: # Or use the install script:
# examples/git-hooks/install.sh # examples/git-hooks/install.sh
# Check if bd is available
if ! command -v bd >/dev/null 2>&1; then
echo "Warning: bd command not found, skipping pre-push export check" >&2
exit 0
fi
# Check if we're in a bd workspace # Check if we're in a bd workspace
if [ ! -d .beads ]; then if [ ! -d .beads ]; then
# Not a bd workspace, nothing to do # Not a bd workspace, nothing to do
exit 0 exit 0
fi fi
# Check if database is newer than JSONL # Optionally flush pending bd changes so they surface in JSONL
DB_FILE=".beads/beads.db" # This prevents the race where a debounced flush lands after the check
JSONL_FILE=".beads/beads.jsonl" if command -v bd >/dev/null 2>&1; then
bd sync --flush-only >/dev/null 2>&1 || true
fi
if [ -f "$DB_FILE" ] && [ -f "$JSONL_FILE" ]; then # Collect all tracked or existing JSONL files (supports both old and new names)
# Get modification times FILES=""
if [ "$DB_FILE" -nt "$JSONL_FILE" ]; then for f in .beads/beads.jsonl .beads/issues.jsonl; do
echo "⚠️ Database is newer than JSONL - exporting before push..." >&2 # Include file if it exists in working tree OR is tracked by git (even if deleted)
if git ls-files --error-unmatch "$f" >/dev/null 2>&1 || [ -f "$f" ]; then
# Force export to ensure JSONL is up to date FILES="$FILES $f"
if ! BEADS_NO_DAEMON=1 bd export --output "$JSONL_FILE" >/dev/null 2>&1; then fi
echo "Error: Failed to export database to JSONL" >&2 done
echo "Run 'bd export' manually to diagnose" >&2
exit 1 # Check for any uncommitted changes using porcelain status
fi # This catches: staged, unstaged, untracked, deleted, renamed, and conflicts
if [ -n "$FILES" ]; then
# Stage the updated JSONL # shellcheck disable=SC2086
git add "$JSONL_FILE" 2>/dev/null || true if [ -n "$(git status --porcelain -- $FILES 2>/dev/null)" ]; then
echo "❌ Error: Beads JSONL has uncommitted changes" >&2
echo "✓ Exported database to JSONL" >&2 echo "" >&2
echo "You made changes to bd issues between your last commit and this push." >&2
echo "Please commit the updated JSONL before pushing:" >&2
echo "" >&2
# shellcheck disable=SC2086
echo " git add $FILES" >&2
echo ' git commit -m "Update bd JSONL"' >&2
echo " git push" >&2
echo "" >&2
exit 1
fi fi
fi fi