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>
132 lines
3.1 KiB
Go
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)
|
|
}
|