Files
beads/cmd/bd/merge.go
Steve Yegge 52c505956f feat: Add bd merge command for git 3-way JSONL merging (bd-omx1)
- Implemented bd merge command with dual-mode operation:
  1. Git 3-way merge: bd merge <output> <base> <left> <right>
  2. Duplicate issue merge: bd merge <sources...> --into <target> (placeholder)
- Added MergeFiles wrapper to internal/merge package
- Command works without database when used as git merge driver
- Supports --debug flag for verbose output
- Exit code 0 for clean merge, 1 for conflicts
- Handles deletions intelligently (delete-modify conflicts)
- Added proper MIT license attribution for @neongreen's beads-merge code
- Tests pass for git merge functionality

This enables git merge driver setup for .beads/beads.jsonl files.
2025-11-05 19:16:50 -08:00

115 lines
3.2 KiB
Go

package main
import (
"fmt"
"os"
"github.com/spf13/cobra"
"github.com/steveyegge/beads/internal/merge"
)
var (
mergeDebug bool
mergeInto string
mergeDryRun bool
)
var mergeCmd = &cobra.Command{
Use: "merge <source-ids...> --into <target-id> | merge <output> <base> <left> <right>",
Short: "Merge duplicate issues or perform 3-way JSONL merge",
Long: `Two modes of operation:
1. Duplicate issue merge (--into flag):
bd merge <source-id...> --into <target-id>
Consolidates duplicate issues into a single target issue.
2. Git 3-way merge (4 positional args, no --into):
bd merge <output> <base> <left> <right>
Performs intelligent field-level JSONL merging for git merge driver.
Git merge mode implements:
- Dependencies merged with union + dedup
- Timestamps use max(left, right)
- Status/priority use 3-way comparison
- Detects deleted-vs-modified conflicts
Git merge driver setup:
git config merge.beads.driver "bd merge %A %O %L %R"
Exit codes:
0 - Clean merge (no conflicts)
1 - Conflicts found (conflict markers written to output)
Other - Error occurred`,
Args: cobra.MinimumNArgs(1),
// Skip database initialization check for git merge mode
PersistentPreRun: func(cmd *cobra.Command, args []string) {
// If this is git merge mode (4 args, no --into), skip normal DB init
if mergeInto == "" && len(args) == 4 {
return
}
// Otherwise, run the normal PersistentPreRun
if rootCmd.PersistentPreRun != nil {
rootCmd.PersistentPreRun(cmd, args)
}
},
RunE: runMerge,
}
func init() {
mergeCmd.Flags().BoolVar(&mergeDebug, "debug", false, "Enable debug output")
mergeCmd.Flags().StringVar(&mergeInto, "into", "", "Target issue ID for duplicate merge")
mergeCmd.Flags().BoolVar(&mergeDryRun, "dry-run", false, "Preview merge without applying changes")
rootCmd.AddCommand(mergeCmd)
}
func runMerge(cmd *cobra.Command, args []string) error {
// Determine mode based on arguments
if mergeInto != "" {
// Duplicate issue merge mode
return runDuplicateMerge(cmd, args)
} else if len(args) == 4 {
// Git 3-way merge mode
return runGitMerge(cmd, args)
} else {
return fmt.Errorf("invalid arguments: use either '<source-ids...> --into <target-id>' or '<output> <base> <left> <right>'")
}
}
func runGitMerge(_ *cobra.Command, args []string) error {
outputPath := args[0]
basePath := args[1]
leftPath := args[2]
rightPath := args[3]
if mergeDebug {
fmt.Fprintf(os.Stderr, "Merging:\n")
fmt.Fprintf(os.Stderr, " Base: %s\n", basePath)
fmt.Fprintf(os.Stderr, " Left: %s\n", leftPath)
fmt.Fprintf(os.Stderr, " Right: %s\n", rightPath)
fmt.Fprintf(os.Stderr, " Output: %s\n", outputPath)
}
// Perform the merge
hasConflicts, err := merge.MergeFiles(outputPath, basePath, leftPath, rightPath, mergeDebug)
if err != nil {
return fmt.Errorf("merge failed: %w", err)
}
if hasConflicts {
if mergeDebug {
fmt.Fprintf(os.Stderr, "Merge completed with conflicts\n")
}
os.Exit(1)
}
if mergeDebug {
fmt.Fprintf(os.Stderr, "Merge completed successfully\n")
}
return nil
}
func runDuplicateMerge(cmd *cobra.Command, sourceIDs []string) error {
// This will be implemented later or moved from duplicates.go
return fmt.Errorf("duplicate issue merge not yet implemented - use 'bd duplicates --auto-merge' for now")
}