rdev/cmd/sdlc/cmd_branch.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

193 lines
4.2 KiB
Go

package main
import (
"fmt"
"os/exec"
"time"
"github.com/orchard9/rdev/internal/sdlc"
"github.com/spf13/cobra"
)
var branchCmd = &cobra.Command{
Use: "branch",
Short: "Manage feature branches",
}
var branchCreateCmd = &cobra.Command{
Use: "create <slug>",
Short: "Create a feature branch",
Args: cobra.ExactArgs(1),
RunE: func(_ *cobra.Command, args []string) error {
root := mustResolveRoot()
slug := args[0]
cfg, err := sdlc.LoadConfig(root)
if err != nil {
return err
}
// Create the branch manifest
manifest, err := sdlc.CreateBranch(root, slug, cfg)
if err != nil {
return err
}
// Create the git branch
branchName := manifest.Name
gitCmd := exec.Command("git", "checkout", "-b", branchName)
gitCmd.Dir = root
if out, err := gitCmd.CombinedOutput(); err != nil {
return fmt.Errorf("git checkout -b %s: %s: %w", branchName, string(out), err)
}
// Record the action in state
state, err := sdlc.LoadState(root)
if err != nil {
return err
}
state.RecordAction("branch_created", slug, "cli")
if err := state.Save(root); err != nil {
return err
}
if jsonOutput {
printJSON(manifest)
return nil
}
fmt.Printf("Created branch: %s (from %s)\n", branchName, manifest.BaseBranch)
return nil
},
}
var branchStatusCmd = &cobra.Command{
Use: "status <slug>",
Short: "Show branch status and merge checklist",
Args: cobra.ExactArgs(1),
RunE: func(_ *cobra.Command, args []string) error {
root := mustResolveRoot()
slug := args[0]
f, err := sdlc.LoadFeature(root, slug)
if err != nil {
return err
}
if f.Branch == "" {
if jsonOutput {
printJSON(map[string]string{"error": "no branch associated with feature"})
return nil
}
fmt.Printf("Feature %s has no branch.\n", slug)
return nil
}
manifest, err := sdlc.LoadBranch(root, f.Branch)
if err != nil {
return err
}
checklist, err := sdlc.MergeChecklist(root, slug)
if err != nil {
return err
}
if jsonOutput {
printJSON(map[string]any{
"branch": manifest,
"checklist": checklist,
"ready": len(checklist) == 0,
})
return nil
}
fmt.Printf("Branch: %s\n", manifest.Name)
fmt.Printf("Feature: %s\n", manifest.Feature)
fmt.Printf("Base: %s\n", manifest.BaseBranch)
fmt.Printf("Created: %s\n", manifest.CreatedAt.Format(time.RFC3339))
if manifest.LastSyncAt != nil {
fmt.Printf("Last sync: %s\n", manifest.LastSyncAt.Format(time.RFC3339))
}
fmt.Println()
if len(checklist) == 0 {
fmt.Println("Merge status: READY")
} else {
fmt.Println("Merge status: NOT READY")
fmt.Println("Unmet gates:")
for _, gate := range checklist {
fmt.Printf(" - %s\n", gate)
}
}
return nil
},
}
var branchSyncCmd = &cobra.Command{
Use: "sync <slug>",
Short: "Sync feature branch with base branch",
Args: cobra.ExactArgs(1),
RunE: func(_ *cobra.Command, args []string) error {
root := mustResolveRoot()
slug := args[0]
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
}
// Fetch and rebase
fetchCmd := exec.Command("git", "fetch", "origin")
fetchCmd.Dir = root
if out, err := fetchCmd.CombinedOutput(); err != nil {
return fmt.Errorf("git fetch: %s: %w", string(out), err)
}
rebaseCmd := exec.Command("git", "rebase", "origin/"+manifest.BaseBranch)
rebaseCmd.Dir = root
if out, err := rebaseCmd.CombinedOutput(); err != nil {
return fmt.Errorf("git rebase origin/%s: %s: %w", manifest.BaseBranch, string(out), err)
}
// Update last sync time
now := time.Now().UTC()
manifest.LastSyncAt = &now
if err := sdlc.SaveBranch(root, manifest); err != nil {
return err
}
if jsonOutput {
printJSON(map[string]string{
"feature": slug,
"branch": manifest.Name,
"synced": "true",
"synced_at": now.Format(time.RFC3339),
})
return nil
}
fmt.Printf("Synced %s with origin/%s\n", manifest.Name, manifest.BaseBranch)
return nil
},
}
func init() {
branchCmd.AddCommand(
branchCreateCmd,
branchStatusCmd,
branchSyncCmd,
)
rootCmd.AddCommand(branchCmd)
}