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>
193 lines
4.2 KiB
Go
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)
|
|
}
|