rdev/cmd/sdlc/cmd_merge.go
jordan f22b220c6d feat: add SDLC branch management, merge, archive, and orchestrator APIs
Add branch lifecycle commands (branch, merge, archive) to the SDLC CLI.
Introduce orchestrator handler and service for multi-step SDLC workflows.
Expand skeleton template with 15 Claude commands covering the full feature
lifecycle. Extend classifier rules, error types, and executor port for
branch operations. Split rules.go and classifier_test.go to stay within
500-line limit.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-02 12:30:03 -07:00

132 lines
3.1 KiB
Go

package main
import (
"fmt"
"os/exec"
"time"
"github.com/orchard9/rdev/internal/sdlc"
"github.com/spf13/cobra"
)
var mergeStrategy string
var mergeCmd = &cobra.Command{
Use: "merge <slug>",
Short: "Merge a feature branch after all gates pass",
Args: cobra.ExactArgs(1),
RunE: func(_ *cobra.Command, args []string) error {
root := mustResolveRoot()
slug := args[0]
// Check merge readiness
checklist, err := sdlc.MergeChecklist(root, slug)
if err != nil {
return err
}
if len(checklist) > 0 {
if jsonOutput {
printJSON(map[string]any{
"error": "merge not ready",
"checklist": checklist,
})
}
return fmt.Errorf("%w: %v", sdlc.ErrMergeNotReady, checklist)
}
f, err := sdlc.LoadFeature(root, slug)
if err != nil {
return err
}
if f.Branch == "" {
return fmt.Errorf("feature %s has no branch", slug)
}
manifest, err := sdlc.LoadBranch(root, f.Branch)
if err != nil {
return err
}
strategy := mergeStrategy
if strategy == "" {
strategy = "squash"
}
// Checkout main branch
checkoutCmd := exec.Command("git", "checkout", manifest.BaseBranch)
checkoutCmd.Dir = root
if out, err := checkoutCmd.CombinedOutput(); err != nil {
return fmt.Errorf("git checkout %s: %s: %w", manifest.BaseBranch, string(out), err)
}
// Merge
mergeArgs := []string{"merge"}
if strategy == "squash" {
mergeArgs = append(mergeArgs, "--squash")
}
mergeArgs = append(mergeArgs, f.Branch)
gitMerge := exec.Command("git", mergeArgs...)
gitMerge.Dir = root
if out, err := gitMerge.CombinedOutput(); err != nil {
return fmt.Errorf("git merge %s: %s: %w", f.Branch, string(out), err)
}
// For squash merges, create the commit
if strategy == "squash" {
commitMsg := fmt.Sprintf("feat: %s\n\nMerged feature %s via SDLC orchestration.", f.Title, slug)
commitCmd := exec.Command("git", "commit", "-m", commitMsg)
commitCmd.Dir = root
if out, err := commitCmd.CombinedOutput(); err != nil {
return fmt.Errorf("git commit: %s: %w", string(out), err)
}
}
// Update branch manifest
now := time.Now().UTC()
manifest.MergedAt = &now
manifest.MergeStrategy = strategy
if err := sdlc.SaveBranch(root, manifest); err != nil {
return err
}
// Transition feature to released
if err := f.Transition(sdlc.PhaseReleased); err != nil {
return err
}
if err := f.Save(root); err != nil {
return err
}
// Record action
state, err := sdlc.LoadState(root)
if err != nil {
return err
}
state.RecordAction("merged", slug, "cli")
if err := state.Save(root); err != nil {
return err
}
if jsonOutput {
printJSON(map[string]string{
"feature": slug,
"branch": f.Branch,
"strategy": strategy,
"status": "merged",
})
return nil
}
fmt.Printf("Merged: %s -> %s (strategy: %s)\n", f.Branch, manifest.BaseBranch, strategy)
fmt.Printf("Feature %s transitioned to released.\n", slug)
return nil
},
}
func init() {
mergeCmd.Flags().StringVar(&mergeStrategy, "strategy", "squash", "merge strategy: squash or merge")
rootCmd.AddCommand(mergeCmd)
}