Files
gastown/internal/cmd/mq.go
Steve Yegge 91fa5e63dc Replace 'gastown' with 'greenplace' in user-facing docs/examples
Using "greenplace" (The Green Place from Mad Max: Fury Road) as the
canonical example project/rig name in documentation and help text.
This provides a clearer distinction from the actual gastown repo name.

Changes:
- docs/*.md: Updated all example paths and commands
- internal/cmd/*.go: Updated help text examples
- internal/templates/: Updated example references
- Tests: Updated to use greenplace in example session names

Note: Import paths (github.com/steveyegge/gastown) and actual code
paths referencing the gastown repo structure are unchanged.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-30 18:03:06 -08:00

413 lines
13 KiB
Go

package cmd
import (
"fmt"
"os"
"path/filepath"
"strings"
"github.com/spf13/cobra"
"github.com/steveyegge/gastown/internal/config"
"github.com/steveyegge/gastown/internal/git"
"github.com/steveyegge/gastown/internal/refinery"
"github.com/steveyegge/gastown/internal/rig"
"github.com/steveyegge/gastown/internal/style"
)
// MQ command flags
var (
// Submit flags
mqSubmitBranch string
mqSubmitIssue string
mqSubmitEpic string
mqSubmitPriority int
mqSubmitNoCleanup bool
// Retry flags
mqRetryNow bool
// Reject flags
mqRejectReason string
mqRejectNotify bool
// List command flags
mqListReady bool
mqListStatus string
mqListWorker string
mqListEpic string
mqListJSON bool
// Status command flags
mqStatusJSON bool
// Integration land flags
mqIntegrationLandForce bool
mqIntegrationLandSkipTests bool
mqIntegrationLandDryRun bool
// Integration status flags
mqIntegrationStatusJSON bool
)
var mqCmd = &cobra.Command{
Use: "mq",
GroupID: GroupWork,
Short: "Merge queue operations",
Long: `Manage the merge queue for a rig.
The merge queue tracks work branches from polecats waiting to be merged.
Use these commands to view, submit, retry, and manage merge requests.`,
}
var mqSubmitCmd = &cobra.Command{
Use: "submit",
Short: "Submit current branch to the merge queue",
Long: `Submit the current branch to the merge queue.
Creates a merge-request bead that will be processed by the Refinery.
Auto-detection:
- Branch: current git branch
- Issue: parsed from branch name (e.g., polecat/Nux/gp-xyz → gt-xyz)
- Worker: parsed from branch name
- Rig: detected from current directory
- Target: automatically determined (see below)
- Priority: inherited from source issue
Target branch auto-detection:
1. If --epic is specified: target integration/<epic>
2. If source issue has a parent epic with integration/<epic> branch: target it
3. Otherwise: target main
This ensures batch work on epics automatically flows to integration branches.
Polecat auto-cleanup:
When run from a polecat work branch (polecat/<worker>/<issue>), this command
automatically triggers polecat shutdown after submitting the MR. The polecat
sends a lifecycle request to its Witness and waits for termination.
Use --no-cleanup to disable this behavior (e.g., if you want to submit
multiple MRs or continue working).
Examples:
gt mq submit # Auto-detect everything + auto-cleanup
gt mq submit --issue gp-abc # Explicit issue
gt mq submit --epic gt-xyz # Target integration branch explicitly
gt mq submit --priority 0 # Override priority (P0)
gt mq submit --no-cleanup # Submit without auto-cleanup`,
RunE: runMqSubmit,
}
var mqRetryCmd = &cobra.Command{
Use: "retry <rig> <mr-id>",
Short: "Retry a failed merge request",
Long: `Retry a failed merge request.
Resets a failed MR so it can be processed again by the refinery.
The MR must be in a failed state (open with an error).
Examples:
gt mq retry greenplace gp-mr-abc123
gt mq retry greenplace gp-mr-abc123 --now`,
Args: cobra.ExactArgs(2),
RunE: runMQRetry,
}
var mqListCmd = &cobra.Command{
Use: "list <rig>",
Short: "Show the merge queue",
Long: `Show the merge queue for a rig.
Lists all pending merge requests waiting to be processed.
Output format:
ID STATUS PRIORITY BRANCH WORKER AGE
gt-mr-001 ready P0 polecat/Nux/gp-xyz Nux 5m
gt-mr-002 in_progress P1 polecat/Toast/gt-abc Toast 12m
gt-mr-003 blocked P1 polecat/Capable/gt-def Capable 8m
(waiting on gt-mr-001)
Examples:
gt mq list greenplace
gt mq list greenplace --ready
gt mq list greenplace --status=open
gt mq list greenplace --worker=Nux`,
Args: cobra.ExactArgs(1),
RunE: runMQList,
}
var mqRejectCmd = &cobra.Command{
Use: "reject <rig> <mr-id-or-branch>",
Short: "Reject a merge request",
Long: `Manually reject a merge request.
This closes the MR with a 'rejected' status without merging.
The source issue is NOT closed (work is not done).
Examples:
gt mq reject greenplace polecat/Nux/gp-xyz --reason "Does not meet requirements"
gt mq reject greenplace mr-Nux-12345 --reason "Superseded by other work" --notify`,
Args: cobra.ExactArgs(2),
RunE: runMQReject,
}
var mqStatusCmd = &cobra.Command{
Use: "status <id>",
Short: "Show detailed merge request status",
Long: `Display detailed information about a merge request.
Shows all MR fields, current status with timestamps, dependencies,
blockers, and processing history.
Example:
gt mq status gp-mr-abc123`,
Args: cobra.ExactArgs(1),
RunE: runMqStatus,
}
var mqIntegrationCmd = &cobra.Command{
Use: "integration",
Short: "Manage integration branches for epics",
Long: `Manage integration branches for batch work on epics.
Integration branches allow multiple MRs for an epic to target a shared
branch instead of main. After all epic work is complete, the integration
branch is landed to main as a single atomic unit.
Commands:
create Create an integration branch for an epic
land Merge integration branch to main
status Show integration branch status`,
}
var mqIntegrationCreateCmd = &cobra.Command{
Use: "create <epic-id>",
Short: "Create an integration branch for an epic",
Long: `Create an integration branch for batch work on an epic.
Creates a branch named integration/<epic-id> from main and pushes it
to origin. Future MRs for this epic's children can target this branch.
Actions:
1. Verify epic exists
2. Create branch integration/<epic-id> from main
3. Push to origin
4. Store integration branch info in epic metadata
Example:
gt mq integration create gt-auth-epic
# Creates integration/gt-auth-epic from main`,
Args: cobra.ExactArgs(1),
RunE: runMqIntegrationCreate,
}
var mqIntegrationLandCmd = &cobra.Command{
Use: "land <epic-id>",
Short: "Merge integration branch to main",
Long: `Merge an epic's integration branch to main.
Lands all work for an epic by merging its integration branch to main
as a single atomic merge commit.
Actions:
1. Verify all MRs targeting integration/<epic> are merged
2. Verify integration branch exists
3. Merge integration/<epic> to main (--no-ff)
4. Run tests on main
5. Push to origin
6. Delete integration branch
7. Update epic status
Options:
--force Land even if some MRs still open
--skip-tests Skip test run
--dry-run Preview only, make no changes
Examples:
gt mq integration land gt-auth-epic
gt mq integration land gt-auth-epic --dry-run
gt mq integration land gt-auth-epic --force --skip-tests`,
Args: cobra.ExactArgs(1),
RunE: runMqIntegrationLand,
}
var mqIntegrationStatusCmd = &cobra.Command{
Use: "status <epic-id>",
Short: "Show integration branch status for an epic",
Long: `Display the status of an integration branch.
Shows:
- Integration branch name and creation date
- Number of commits ahead of main
- Merged MRs (closed, targeting integration branch)
- Pending MRs (open, targeting integration branch)
Example:
gt mq integration status gt-auth-epic`,
Args: cobra.ExactArgs(1),
RunE: runMqIntegrationStatus,
}
func init() {
// Submit flags
mqSubmitCmd.Flags().StringVar(&mqSubmitBranch, "branch", "", "Source branch (default: current branch)")
mqSubmitCmd.Flags().StringVar(&mqSubmitIssue, "issue", "", "Source issue ID (default: parse from branch name)")
mqSubmitCmd.Flags().StringVar(&mqSubmitEpic, "epic", "", "Target epic's integration branch instead of main")
mqSubmitCmd.Flags().IntVarP(&mqSubmitPriority, "priority", "p", -1, "Override priority (0-4, default: inherit from issue)")
mqSubmitCmd.Flags().BoolVar(&mqSubmitNoCleanup, "no-cleanup", false, "Don't auto-cleanup after submit (for polecats)")
// Retry flags
mqRetryCmd.Flags().BoolVar(&mqRetryNow, "now", false, "Immediately process instead of waiting for refinery loop")
// List flags
mqListCmd.Flags().BoolVar(&mqListReady, "ready", false, "Show only ready-to-merge (no blockers)")
mqListCmd.Flags().StringVar(&mqListStatus, "status", "", "Filter by status (open, in_progress, closed)")
mqListCmd.Flags().StringVar(&mqListWorker, "worker", "", "Filter by worker name")
mqListCmd.Flags().StringVar(&mqListEpic, "epic", "", "Show MRs targeting integration/<epic>")
mqListCmd.Flags().BoolVar(&mqListJSON, "json", false, "Output as JSON")
// Reject flags
mqRejectCmd.Flags().StringVarP(&mqRejectReason, "reason", "r", "", "Reason for rejection (required)")
mqRejectCmd.Flags().BoolVar(&mqRejectNotify, "notify", false, "Send mail notification to worker")
_ = mqRejectCmd.MarkFlagRequired("reason") // cobra flags: error only at runtime if missing
// Status flags
mqStatusCmd.Flags().BoolVar(&mqStatusJSON, "json", false, "Output as JSON")
// Add subcommands
mqCmd.AddCommand(mqSubmitCmd)
mqCmd.AddCommand(mqRetryCmd)
mqCmd.AddCommand(mqListCmd)
mqCmd.AddCommand(mqRejectCmd)
mqCmd.AddCommand(mqStatusCmd)
// Integration branch subcommands
mqIntegrationCmd.AddCommand(mqIntegrationCreateCmd)
// Integration land flags
mqIntegrationLandCmd.Flags().BoolVar(&mqIntegrationLandForce, "force", false, "Land even if some MRs still open")
mqIntegrationLandCmd.Flags().BoolVar(&mqIntegrationLandSkipTests, "skip-tests", false, "Skip test run")
mqIntegrationLandCmd.Flags().BoolVar(&mqIntegrationLandDryRun, "dry-run", false, "Preview only, make no changes")
mqIntegrationCmd.AddCommand(mqIntegrationLandCmd)
// Integration status flags
mqIntegrationStatusCmd.Flags().BoolVar(&mqIntegrationStatusJSON, "json", false, "Output as JSON")
mqIntegrationCmd.AddCommand(mqIntegrationStatusCmd)
mqCmd.AddCommand(mqIntegrationCmd)
rootCmd.AddCommand(mqCmd)
}
// findCurrentRig determines the current rig from the working directory.
// Returns the rig name and rig object, or an error if not in a rig.
func findCurrentRig(townRoot string) (string, *rig.Rig, error) {
cwd, err := os.Getwd()
if err != nil {
return "", nil, fmt.Errorf("getting current directory: %w", err)
}
// Get relative path from town root to cwd
relPath, err := filepath.Rel(townRoot, cwd)
if err != nil {
return "", nil, fmt.Errorf("computing relative path: %w", err)
}
// The first component of the relative path should be the rig name
parts := strings.Split(relPath, string(filepath.Separator))
if len(parts) == 0 || parts[0] == "" || parts[0] == "." {
return "", nil, fmt.Errorf("not inside a rig directory")
}
rigName := parts[0]
// Load rig manager and get the rig
rigsConfigPath := filepath.Join(townRoot, "mayor", "rigs.json")
rigsConfig, err := config.LoadRigsConfig(rigsConfigPath)
if err != nil {
rigsConfig = &config.RigsConfig{Rigs: make(map[string]config.RigEntry)}
}
g := git.NewGit(townRoot)
rigMgr := rig.NewManager(townRoot, rigsConfig, g)
r, err := rigMgr.GetRig(rigName)
if err != nil {
return "", nil, fmt.Errorf("rig '%s' not found: %w", rigName, err)
}
return rigName, r, nil
}
func runMQRetry(cmd *cobra.Command, args []string) error {
rigName := args[0]
mrID := args[1]
mgr, _, _, err := getRefineryManager(rigName)
if err != nil {
return err
}
// Get the MR first to show info
mr, err := mgr.GetMR(mrID)
if err != nil {
if err == refinery.ErrMRNotFound {
return fmt.Errorf("merge request '%s' not found in rig '%s'", mrID, rigName)
}
return fmt.Errorf("getting merge request: %w", err)
}
// Show what we're retrying
fmt.Printf("Retrying merge request: %s\n", mrID)
fmt.Printf(" Branch: %s\n", mr.Branch)
fmt.Printf(" Worker: %s\n", mr.Worker)
if mr.Error != "" {
fmt.Printf(" Previous error: %s\n", style.Dim.Render(mr.Error))
}
// Perform the retry
if err := mgr.Retry(mrID, mqRetryNow); err != nil {
if err == refinery.ErrMRNotFailed {
return fmt.Errorf("merge request '%s' has not failed (status: %s)", mrID, mr.Status)
}
return fmt.Errorf("retrying merge request: %w", err)
}
if mqRetryNow {
fmt.Printf("%s Merge request processed\n", style.Bold.Render("✓"))
} else {
fmt.Printf("%s Merge request queued for retry\n", style.Bold.Render("✓"))
fmt.Printf(" %s\n", style.Dim.Render("Will be processed on next refinery cycle"))
}
return nil
}
func runMQReject(cmd *cobra.Command, args []string) error {
rigName := args[0]
mrIDOrBranch := args[1]
mgr, _, _, err := getRefineryManager(rigName)
if err != nil {
return err
}
result, err := mgr.RejectMR(mrIDOrBranch, mqRejectReason, mqRejectNotify)
if err != nil {
return fmt.Errorf("rejecting MR: %w", err)
}
fmt.Printf("%s Rejected: %s\n", style.Bold.Render("✗"), result.Branch)
fmt.Printf(" Worker: %s\n", result.Worker)
fmt.Printf(" Reason: %s\n", mqRejectReason)
if result.IssueID != "" {
fmt.Printf(" Issue: %s %s\n", result.IssueID, style.Dim.Render("(not closed - work not done)"))
}
if mqRejectNotify {
fmt.Printf(" %s\n", style.Dim.Render("Worker notified via mail"))
}
return nil
}