Fix bd-51: Add git hooks to eliminate auto-flush race condition
- Added --flush-only flag to bd sync command - Created pre-commit hook to flush pending changes before commit - Created post-merge hook to import changes after pull/merge - Added install script for easy setup - Updated AGENTS.md with git hooks workflow - Resolves race condition where daemon auto-flush fires after commit Amp-Thread-ID: https://ampcode.com/threads/T-00b80d3a-4194-4c75-a60e-25a318cf9f91 Co-authored-by: Amp <amp@ampcode.com>
This commit is contained in:
@@ -1,200 +1,102 @@
|
||||
# Git Hooks for Beads
|
||||
# bd Git Hooks
|
||||
|
||||
Optional git hooks for immediate export/import of beads issues.
|
||||
This directory contains git hooks that integrate bd (beads) with your git workflow, solving the race condition between daemon auto-flush and git commits.
|
||||
|
||||
**NOTE**: As of bd v0.9+, **auto-sync is enabled by default!** These hooks are optional and provide:
|
||||
- **Immediate export** (no 5-second debounce wait)
|
||||
- **Guaranteed import** after every git operation
|
||||
- **Extra safety** for critical workflows
|
||||
## The Problem
|
||||
|
||||
## What These Hooks Do
|
||||
When using bd in daemon mode, operations trigger a 5-second debounced auto-flush to JSONL. This creates a race condition:
|
||||
|
||||
- **pre-commit**: Exports SQLite → JSONL before every commit (immediate, no debounce)
|
||||
- **post-merge**: Imports JSONL → SQLite after git pull/merge (guaranteed)
|
||||
- **post-checkout**: Imports JSONL → SQLite after branch switching (guaranteed)
|
||||
1. User closes issue via MCP → daemon schedules flush (5 sec delay)
|
||||
2. User commits code changes → JSONL appears clean
|
||||
3. Daemon flush fires → JSONL modified after commit
|
||||
4. Result: dirty working tree showing JSONL changes
|
||||
|
||||
This keeps your `.beads/issues.jsonl` (committed to git) in sync with your local SQLite database (gitignored).
|
||||
## The Solution
|
||||
|
||||
## Do You Need These Hooks?
|
||||
These git hooks ensure bd changes are always synchronized with your commits:
|
||||
|
||||
**Most users don't need hooks anymore!** bd automatically:
|
||||
- Exports after CRUD operations (5-second debounce)
|
||||
- Imports when JSONL is newer than DB
|
||||
|
||||
**Install hooks if you:**
|
||||
- Want immediate export (no waiting 5 seconds)
|
||||
- Want guaranteed import after every git operation
|
||||
- Need extra certainty for team workflows
|
||||
- Prefer explicit automation over automatic behavior
|
||||
- **pre-commit** - Flushes pending bd changes to JSONL before commit
|
||||
- **post-merge** - Imports updated JSONL after git pull/merge
|
||||
|
||||
## Installation
|
||||
|
||||
### Quick Install
|
||||
|
||||
From your repository root:
|
||||
|
||||
```bash
|
||||
cd /path/to/your/project
|
||||
./examples/git-hooks/install.sh
|
||||
```
|
||||
|
||||
The installer will prompt before overwriting existing hooks.
|
||||
This will:
|
||||
- Copy hooks to `.git/hooks/`
|
||||
- Make them executable
|
||||
- Back up any existing hooks
|
||||
|
||||
### Manual Install
|
||||
|
||||
```bash
|
||||
# Copy hooks to .git/hooks/
|
||||
cp examples/git-hooks/pre-commit .git/hooks/
|
||||
cp examples/git-hooks/post-merge .git/hooks/
|
||||
cp examples/git-hooks/post-checkout .git/hooks/
|
||||
|
||||
# Make them executable
|
||||
chmod +x .git/hooks/pre-commit
|
||||
chmod +x .git/hooks/post-merge
|
||||
chmod +x .git/hooks/post-checkout
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
Once installed, the hooks run automatically:
|
||||
|
||||
```bash
|
||||
# Creating/updating issues
|
||||
bd create "New feature" -p 1
|
||||
bd update bd-1 --status in_progress
|
||||
|
||||
# Committing changes - hook exports automatically
|
||||
git add .
|
||||
git commit -m "Update feature"
|
||||
# 🔗 Exporting beads issues to JSONL...
|
||||
# ✓ Beads issues exported and staged
|
||||
|
||||
# Pulling changes - hook imports automatically
|
||||
git pull
|
||||
# 🔗 Importing beads issues from JSONL...
|
||||
# ✓ Beads issues imported successfully
|
||||
|
||||
# Switching branches - hook imports automatically
|
||||
git checkout feature-branch
|
||||
# 🔗 Importing beads issues from JSONL...
|
||||
# ✓ Beads issues imported successfully
|
||||
cp examples/git-hooks/pre-commit .git/hooks/pre-commit
|
||||
cp examples/git-hooks/post-merge .git/hooks/post-merge
|
||||
chmod +x .git/hooks/pre-commit .git/hooks/post-merge
|
||||
```
|
||||
|
||||
## How It Works
|
||||
|
||||
### The Workflow
|
||||
### pre-commit
|
||||
|
||||
1. You work with bd commands (`create`, `update`, `close`)
|
||||
2. Changes are stored in SQLite (`.beads/*.db`) - fast local queries
|
||||
3. Before commit, hook exports to JSONL (`.beads/issues.jsonl`) - git-friendly
|
||||
4. JSONL is committed to git (source of truth)
|
||||
5. After pull/merge/checkout, hook imports JSONL back to SQLite
|
||||
6. Your local SQLite cache is now in sync with git
|
||||
|
||||
### Why This Design?
|
||||
|
||||
**SQLite for speed**:
|
||||
- Fast queries (dependency trees, ready work)
|
||||
- Rich SQL capabilities
|
||||
- Sub-100ms response times
|
||||
|
||||
**JSONL for git**:
|
||||
- Clean diffs (one issue per line)
|
||||
- Mergeable (independent lines)
|
||||
- Human-readable
|
||||
- AI-resolvable conflicts
|
||||
|
||||
Best of both worlds!
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Hook not running
|
||||
Before each commit, the hook runs:
|
||||
|
||||
```bash
|
||||
# Check if hook is executable
|
||||
ls -l .git/hooks/pre-commit
|
||||
# Should show -rwxr-xr-x
|
||||
|
||||
# Make it executable if needed
|
||||
chmod +x .git/hooks/pre-commit
|
||||
bd sync --flush-only
|
||||
```
|
||||
|
||||
### Export/import fails
|
||||
This:
|
||||
1. Exports any pending database changes to `.beads/issues.jsonl`
|
||||
2. Stages the JSONL file if modified
|
||||
3. Allows the commit to proceed with clean state
|
||||
|
||||
The hook is silent on success, fast (no git operations), and safe (fails commit if flush fails).
|
||||
|
||||
### post-merge
|
||||
|
||||
After a git pull or merge, the hook runs:
|
||||
|
||||
```bash
|
||||
# Check if bd is in PATH
|
||||
which bd
|
||||
|
||||
# Check if you're in a beads-initialized directory
|
||||
bd list
|
||||
bd import -i .beads/issues.jsonl --resolve-collisions
|
||||
```
|
||||
|
||||
### Merge conflicts in 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
|
||||
- Warns on failure but doesn't block the merge
|
||||
|
||||
If you get merge conflicts in `.beads/issues.jsonl`:
|
||||
## Compatibility
|
||||
|
||||
1. Most conflicts are safe to resolve by keeping both sides
|
||||
2. Each line is an independent issue
|
||||
3. Look for `<<<<<<< HEAD` markers
|
||||
4. Keep all lines that don't conflict
|
||||
5. For actual conflicts on the same issue, choose the newest
|
||||
- **Auto-sync**: Works alongside bd's automatic 5-second debounce
|
||||
- **Direct mode**: Hooks work in both daemon and `--no-daemon` mode
|
||||
- **Worktrees**: Safe to use with git worktrees
|
||||
|
||||
Example conflict:
|
||||
## Benefits
|
||||
|
||||
```
|
||||
<<<<<<< HEAD
|
||||
{"id":"bd-3","title":"Updated title","status":"closed","updated_at":"2025-10-12T10:00:00Z"}
|
||||
=======
|
||||
{"id":"bd-3","title":"Updated title","status":"in_progress","updated_at":"2025-10-12T09:00:00Z"}
|
||||
>>>>>>> feature-branch
|
||||
```
|
||||
✅ No more dirty working tree after commits
|
||||
✅ Database always in sync with git
|
||||
✅ Automatic collision resolution on merge
|
||||
✅ Fast and silent operation
|
||||
✅ Optional - manual `bd sync` still works
|
||||
|
||||
Resolution: Keep the HEAD version (newer timestamp).
|
||||
## Uninstall
|
||||
|
||||
After resolving:
|
||||
```bash
|
||||
git add .beads/issues.jsonl
|
||||
git commit
|
||||
bd import -i .beads/issues.jsonl # Sync to SQLite
|
||||
```
|
||||
|
||||
## Uninstalling
|
||||
Remove the hooks:
|
||||
|
||||
```bash
|
||||
rm .git/hooks/pre-commit
|
||||
rm .git/hooks/post-merge
|
||||
rm .git/hooks/post-checkout
|
||||
rm .git/hooks/pre-commit .git/hooks/post-merge
|
||||
```
|
||||
|
||||
## Customization
|
||||
Your backed-up hooks (if any) are in `.git/hooks/*.backup-*`.
|
||||
|
||||
### Skip hook for one commit
|
||||
## Related
|
||||
|
||||
```bash
|
||||
git commit --no-verify -m "Skip hooks"
|
||||
```
|
||||
|
||||
### Add to existing hooks
|
||||
|
||||
If you already have git hooks, you can append to them:
|
||||
|
||||
```bash
|
||||
# Append to existing pre-commit
|
||||
cat examples/git-hooks/pre-commit >> .git/hooks/pre-commit
|
||||
```
|
||||
|
||||
### Filter exports
|
||||
|
||||
Export only specific issues:
|
||||
|
||||
```bash
|
||||
# Edit pre-commit hook, change:
|
||||
bd export --format=jsonl -o .beads/issues.jsonl
|
||||
|
||||
# To:
|
||||
bd export --format=jsonl --status=open -o .beads/issues.jsonl
|
||||
```
|
||||
|
||||
## See Also
|
||||
|
||||
- [Git hooks documentation](https://git-scm.com/book/en/v2/Customizing-Git-Git-Hooks)
|
||||
- [../../TEXT_FORMATS.md](../../TEXT_FORMATS.md) - JSONL merge strategies
|
||||
- [../../GIT_WORKFLOW.md](../../GIT_WORKFLOW.md) - Design rationale
|
||||
- See [bd-51](../../.beads/bd-51) for the race condition bug report
|
||||
- See [AGENTS.md](../../AGENTS.md) for the full git workflow
|
||||
- See [examples/](../) for other integrations
|
||||
|
||||
@@ -1,83 +1,64 @@
|
||||
#!/usr/bin/env bash
|
||||
#!/bin/bash
|
||||
#
|
||||
# Install Beads git hooks
|
||||
# Install bd git hooks
|
||||
#
|
||||
# This script copies the hooks to .git/hooks/ and makes them executable
|
||||
# This script copies the bd git hooks to your .git/hooks directory
|
||||
# and makes them executable.
|
||||
#
|
||||
# Usage:
|
||||
# ./examples/git-hooks/install.sh
|
||||
|
||||
set -e
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
HOOKS_DIR="$(git rev-parse --git-dir)/hooks"
|
||||
|
||||
# Check if we're in a git repository
|
||||
if ! git rev-parse --git-dir &> /dev/null; then
|
||||
echo "Error: Not in a git repository"
|
||||
if [ ! -d .git ]; then
|
||||
echo "Error: Not in a git repository root" >&2
|
||||
echo "Run this script from the root of your git repository" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Installing Beads git hooks to $HOOKS_DIR"
|
||||
echo ""
|
||||
|
||||
# Install pre-commit hook
|
||||
if [[ -f "$HOOKS_DIR/pre-commit" ]]; then
|
||||
echo "⚠ $HOOKS_DIR/pre-commit already exists"
|
||||
read -p "Overwrite? (y/n) " -n 1 -r
|
||||
echo
|
||||
if [[ ! $REPLY =~ ^[Yy]$ ]]; then
|
||||
echo "Skipping pre-commit"
|
||||
else
|
||||
cp "$SCRIPT_DIR/pre-commit" "$HOOKS_DIR/pre-commit"
|
||||
chmod +x "$HOOKS_DIR/pre-commit"
|
||||
echo "✓ Installed pre-commit hook"
|
||||
fi
|
||||
else
|
||||
cp "$SCRIPT_DIR/pre-commit" "$HOOKS_DIR/pre-commit"
|
||||
chmod +x "$HOOKS_DIR/pre-commit"
|
||||
echo "✓ Installed pre-commit hook"
|
||||
# Check if we're in a bd workspace
|
||||
if [ ! -d .beads ]; then
|
||||
echo "Error: Not in a bd workspace" >&2
|
||||
echo "Run 'bd init' first" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Install post-merge hook
|
||||
if [[ -f "$HOOKS_DIR/post-merge" ]]; then
|
||||
echo "⚠ $HOOKS_DIR/post-merge already exists"
|
||||
read -p "Overwrite? (y/n) " -n 1 -r
|
||||
echo
|
||||
if [[ ! $REPLY =~ ^[Yy]$ ]]; then
|
||||
echo "Skipping post-merge"
|
||||
else
|
||||
cp "$SCRIPT_DIR/post-merge" "$HOOKS_DIR/post-merge"
|
||||
chmod +x "$HOOKS_DIR/post-merge"
|
||||
echo "✓ Installed post-merge hook"
|
||||
fi
|
||||
else
|
||||
cp "$SCRIPT_DIR/post-merge" "$HOOKS_DIR/post-merge"
|
||||
chmod +x "$HOOKS_DIR/post-merge"
|
||||
echo "✓ Installed post-merge hook"
|
||||
fi
|
||||
# Find the script directory (handles being called from anywhere)
|
||||
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
|
||||
|
||||
# Install post-checkout hook
|
||||
if [[ -f "$HOOKS_DIR/post-checkout" ]]; then
|
||||
echo "⚠ $HOOKS_DIR/post-checkout already exists"
|
||||
read -p "Overwrite? (y/n) " -n 1 -r
|
||||
echo
|
||||
if [[ ! $REPLY =~ ^[Yy]$ ]]; then
|
||||
echo "Skipping post-checkout"
|
||||
else
|
||||
cp "$SCRIPT_DIR/post-checkout" "$HOOKS_DIR/post-checkout"
|
||||
chmod +x "$HOOKS_DIR/post-checkout"
|
||||
echo "✓ Installed post-checkout hook"
|
||||
# Hooks to install
|
||||
HOOKS="pre-commit post-merge"
|
||||
|
||||
echo "Installing bd git hooks..."
|
||||
|
||||
for hook in $HOOKS; do
|
||||
src="$SCRIPT_DIR/$hook"
|
||||
dst=".git/hooks/$hook"
|
||||
|
||||
if [ ! -f "$src" ]; then
|
||||
echo "Warning: Hook $hook not found at $src" >&2
|
||||
continue
|
||||
fi
|
||||
else
|
||||
cp "$SCRIPT_DIR/post-checkout" "$HOOKS_DIR/post-checkout"
|
||||
chmod +x "$HOOKS_DIR/post-checkout"
|
||||
echo "✓ Installed post-checkout hook"
|
||||
fi
|
||||
|
||||
# Backup existing hook if present
|
||||
if [ -f "$dst" ]; then
|
||||
backup="$dst.backup-$(date +%Y%m%d-%H%M%S)"
|
||||
echo " Backing up existing $hook to $backup"
|
||||
mv "$dst" "$backup"
|
||||
fi
|
||||
|
||||
# Copy and make executable
|
||||
cp "$src" "$dst"
|
||||
chmod +x "$dst"
|
||||
echo " Installed $hook"
|
||||
done
|
||||
|
||||
echo ""
|
||||
echo "✓ Beads git hooks installed successfully!"
|
||||
echo "✓ Git hooks installed successfully"
|
||||
echo ""
|
||||
echo "These hooks will:"
|
||||
echo " • Export issues to JSONL before every commit"
|
||||
echo " • Import issues from JSONL after merges"
|
||||
echo " • Import issues from JSONL after branch checkouts"
|
||||
echo "Hooks installed:"
|
||||
echo " pre-commit - Flushes pending bd changes to JSONL before commit"
|
||||
echo " post-merge - Imports updated JSONL after git pull/merge"
|
||||
echo ""
|
||||
echo "To uninstall, simply delete the hooks from $HOOKS_DIR"
|
||||
echo "To uninstall, remove .git/hooks/pre-commit and .git/hooks/post-merge"
|
||||
|
||||
@@ -1,33 +1,41 @@
|
||||
#!/usr/bin/env bash
|
||||
#!/bin/sh
|
||||
#
|
||||
# Beads post-merge hook
|
||||
# Automatically imports JSONL to SQLite database after pulling/merging
|
||||
# bd (beads) post-merge hook
|
||||
#
|
||||
# Install: cp examples/git-hooks/post-merge .git/hooks/post-merge && chmod +x .git/hooks/post-merge
|
||||
# This hook imports updated issues from .beads/issues.jsonl after a
|
||||
# git pull or merge, ensuring the database stays in sync with git.
|
||||
#
|
||||
# Installation:
|
||||
# cp examples/git-hooks/post-merge .git/hooks/post-merge
|
||||
# chmod +x .git/hooks/post-merge
|
||||
#
|
||||
# Or use the install script:
|
||||
# examples/git-hooks/install.sh
|
||||
|
||||
set -e
|
||||
|
||||
# Check if bd is installed
|
||||
if ! command -v bd &> /dev/null; then
|
||||
echo "Warning: bd not found in PATH, skipping import"
|
||||
# Check if bd is available
|
||||
if ! command -v bd >/dev/null 2>&1; then
|
||||
echo "Warning: bd command not found, skipping post-merge import" >&2
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Check if issues.jsonl exists
|
||||
if [[ ! -f .beads/issues.jsonl ]]; then
|
||||
# No JSONL file, nothing to import
|
||||
# Check if we're in a bd workspace
|
||||
if [ ! -d .beads ]; then
|
||||
# Not a bd workspace, nothing to do
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Import issues from JSONL
|
||||
echo "🔗 Importing beads issues from JSONL..."
|
||||
# Check if issues.jsonl exists and was updated
|
||||
if [ ! -f .beads/issues.jsonl ]; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if bd import -i .beads/issues.jsonl 2>/dev/null; then
|
||||
echo "✓ Beads issues imported successfully"
|
||||
else
|
||||
echo "Warning: bd import failed"
|
||||
echo "You may need to resolve conflicts manually"
|
||||
exit 1
|
||||
# Import the updated JSONL
|
||||
# The auto-import feature should handle this, but we force it here
|
||||
# to ensure immediate sync after merge
|
||||
if ! bd import -i .beads/issues.jsonl --resolve-collisions >/dev/null 2>&1; then
|
||||
echo "Warning: Failed to import bd changes after merge" >&2
|
||||
echo "Run 'bd import -i .beads/issues.jsonl --resolve-collisions' manually" >&2
|
||||
# Don't fail the merge, just warn
|
||||
fi
|
||||
|
||||
exit 0
|
||||
|
||||
@@ -1,33 +1,42 @@
|
||||
#!/usr/bin/env bash
|
||||
#!/bin/sh
|
||||
#
|
||||
# Beads pre-commit hook
|
||||
# Automatically exports SQLite database to JSONL before committing
|
||||
# bd (beads) pre-commit hook
|
||||
#
|
||||
# Install: cp examples/git-hooks/pre-commit .git/hooks/pre-commit && chmod +x .git/hooks/pre-commit
|
||||
# This hook ensures that any pending bd issue changes are flushed to
|
||||
# .beads/issues.jsonl before the commit is created, preventing the
|
||||
# race condition where daemon auto-flush fires after the commit.
|
||||
#
|
||||
# Installation:
|
||||
# cp examples/git-hooks/pre-commit .git/hooks/pre-commit
|
||||
# chmod +x .git/hooks/pre-commit
|
||||
#
|
||||
# Or use the install script:
|
||||
# examples/git-hooks/install.sh
|
||||
|
||||
set -e
|
||||
|
||||
# Check if bd is installed
|
||||
if ! command -v bd &> /dev/null; then
|
||||
echo "Warning: bd not found in PATH, skipping export"
|
||||
# Check if bd is available
|
||||
if ! command -v bd >/dev/null 2>&1; then
|
||||
echo "Warning: bd command not found, skipping pre-commit flush" >&2
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Check if .beads directory exists
|
||||
if [[ ! -d .beads ]]; then
|
||||
# No beads database, nothing to do
|
||||
# Check if we're in a bd workspace
|
||||
if [ ! -d .beads ]; then
|
||||
# Not a bd workspace, nothing to do
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Export issues to JSONL
|
||||
echo "🔗 Exporting beads issues to JSONL..."
|
||||
# Flush pending changes to JSONL
|
||||
# Use --flush-only to skip git operations (we're already in a git hook)
|
||||
# Suppress output unless there's an error
|
||||
if ! bd sync --flush-only >/dev/null 2>&1; then
|
||||
echo "Error: Failed to flush bd changes to JSONL" >&2
|
||||
echo "Run 'bd sync --flush-only' manually to diagnose" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if bd export --format=jsonl -o .beads/issues.jsonl 2>/dev/null; then
|
||||
# Add the JSONL file to the commit
|
||||
git add .beads/issues.jsonl
|
||||
echo "✓ Beads issues exported and staged"
|
||||
else
|
||||
echo "Warning: bd export failed, continuing anyway"
|
||||
# If the JSONL file was modified, stage it
|
||||
if [ -f .beads/issues.jsonl ]; then
|
||||
git add .beads/issues.jsonl 2>/dev/null || true
|
||||
fi
|
||||
|
||||
exit 0
|
||||
|
||||
Reference in New Issue
Block a user