feat(mq): add integration status command (gt-h5n.6)

Implement 'gt mq integration status <epic>' command that displays:
- Integration branch name and creation date
- Commits ahead of main
- Merged MRs (closed, targeting integration branch)
- Pending MRs (open, targeting integration branch)

Also adds git helpers for BranchCreatedDate and CommitsAhead.

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Steve Yegge
2025-12-19 12:53:04 -08:00
parent 8de4010d2c
commit f6d7da3284
2 changed files with 255 additions and 2 deletions

View File

@@ -49,6 +49,9 @@ var (
mqIntegrationLandForce bool mqIntegrationLandForce bool
mqIntegrationLandSkipTests bool mqIntegrationLandSkipTests bool
mqIntegrationLandDryRun bool mqIntegrationLandDryRun bool
// Integration status flags
mqIntegrationStatusJSON bool
) )
var mqCmd = &cobra.Command{ var mqCmd = &cobra.Command{
@@ -169,7 +172,7 @@ branch is landed to main as a single atomic unit.
Commands: Commands:
create Create an integration branch for an epic create Create an integration branch for an epic
land Merge integration branch to main land Merge integration branch to main
status Show integration branch status (not yet implemented)`, status Show integration branch status`,
} }
var mqIntegrationCreateCmd = &cobra.Command{ var mqIntegrationCreateCmd = &cobra.Command{
@@ -223,6 +226,23 @@ Examples:
RunE: runMqIntegrationLand, 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() { func init() {
// Submit flags // Submit flags
mqSubmitCmd.Flags().StringVar(&mqSubmitBranch, "branch", "", "Source branch (default: current branch)") mqSubmitCmd.Flags().StringVar(&mqSubmitBranch, "branch", "", "Source branch (default: current branch)")
@@ -258,12 +278,16 @@ func init() {
// Integration branch subcommands // Integration branch subcommands
mqIntegrationCmd.AddCommand(mqIntegrationCreateCmd) mqIntegrationCmd.AddCommand(mqIntegrationCreateCmd)
// Integration land flags // Integration land flags
mqIntegrationLandCmd.Flags().BoolVar(&mqIntegrationLandForce, "force", false, "Land even if some MRs still open") 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(&mqIntegrationLandSkipTests, "skip-tests", false, "Skip test run")
mqIntegrationLandCmd.Flags().BoolVar(&mqIntegrationLandDryRun, "dry-run", false, "Preview only, make no changes") mqIntegrationLandCmd.Flags().BoolVar(&mqIntegrationLandDryRun, "dry-run", false, "Preview only, make no changes")
mqIntegrationCmd.AddCommand(mqIntegrationLandCmd) mqIntegrationCmd.AddCommand(mqIntegrationLandCmd)
// Integration status flags
mqIntegrationStatusCmd.Flags().BoolVar(&mqIntegrationStatusJSON, "json", false, "Output as JSON")
mqIntegrationCmd.AddCommand(mqIntegrationStatusCmd)
mqCmd.AddCommand(mqIntegrationCmd) mqCmd.AddCommand(mqIntegrationCmd)
rootCmd.AddCommand(mqCmd) rootCmd.AddCommand(mqCmd)
@@ -1536,3 +1560,177 @@ func detectIntegrationBranch(bd *beads.Beads, g *git.Git, issueID string) (strin
return "", nil // No integration branch found return "", nil // No integration branch found
} }
// IntegrationStatusOutput is the JSON output structure for integration status.
type IntegrationStatusOutput struct {
Epic string `json:"epic"`
Branch string `json:"branch"`
Created string `json:"created,omitempty"`
AheadOfMain int `json:"ahead_of_main"`
MergedMRs []IntegrationStatusMRSummary `json:"merged_mrs"`
PendingMRs []IntegrationStatusMRSummary `json:"pending_mrs"`
}
// IntegrationStatusMRSummary represents a merge request in the integration status output.
type IntegrationStatusMRSummary struct {
ID string `json:"id"`
Title string `json:"title"`
Status string `json:"status,omitempty"`
}
// runMqIntegrationStatus shows the status of an integration branch for an epic.
func runMqIntegrationStatus(cmd *cobra.Command, args []string) error {
epicID := args[0]
// Find workspace
townRoot, err := workspace.FindFromCwdOrError()
if err != nil {
return fmt.Errorf("not in a Gas Town workspace: %w", err)
}
// Find current rig
_, r, err := findCurrentRig(townRoot)
if err != nil {
return err
}
// Initialize beads for the rig
bd := beads.New(r.Path)
// Build integration branch name
branchName := "integration/" + epicID
// Initialize git for the rig
g := git.NewGit(r.Path)
// Fetch from origin to ensure we have latest refs
if err := g.Fetch("origin"); err != nil {
// Non-fatal, continue with local data
}
// Check if integration branch exists (locally or remotely)
localExists, _ := g.BranchExists(branchName)
remoteExists, _ := g.RemoteBranchExists("origin", branchName)
if !localExists && !remoteExists {
return fmt.Errorf("integration branch '%s' does not exist", branchName)
}
// Determine which ref to use for comparison
ref := branchName
if !localExists && remoteExists {
ref = "origin/" + branchName
}
// Get branch creation date
createdDate, err := g.BranchCreatedDate(ref)
if err != nil {
createdDate = "" // Non-fatal
}
// Get commits ahead of main
aheadCount, err := g.CommitsAhead("main", ref)
if err != nil {
aheadCount = 0 // Non-fatal
}
// Query for MRs targeting this integration branch
targetBranch := "integration/" + epicID
// Get all merge-request issues
allMRs, err := bd.List(beads.ListOptions{
Type: "merge-request",
Status: "", // all statuses
})
if err != nil {
return fmt.Errorf("querying merge requests: %w", err)
}
// Filter by target branch and separate into merged/pending
var mergedMRs, pendingMRs []*beads.Issue
for _, mr := range allMRs {
fields := beads.ParseMRFields(mr)
if fields == nil || fields.Target != targetBranch {
continue
}
if mr.Status == "closed" {
mergedMRs = append(mergedMRs, mr)
} else {
pendingMRs = append(pendingMRs, mr)
}
}
// Build output structure
output := IntegrationStatusOutput{
Epic: epicID,
Branch: branchName,
Created: createdDate,
AheadOfMain: aheadCount,
MergedMRs: make([]IntegrationStatusMRSummary, 0, len(mergedMRs)),
PendingMRs: make([]IntegrationStatusMRSummary, 0, len(pendingMRs)),
}
for _, mr := range mergedMRs {
// Extract the title without "Merge: " prefix for cleaner display
title := strings.TrimPrefix(mr.Title, "Merge: ")
output.MergedMRs = append(output.MergedMRs, IntegrationStatusMRSummary{
ID: mr.ID,
Title: title,
})
}
for _, mr := range pendingMRs {
title := strings.TrimPrefix(mr.Title, "Merge: ")
output.PendingMRs = append(output.PendingMRs, IntegrationStatusMRSummary{
ID: mr.ID,
Title: title,
Status: mr.Status,
})
}
// JSON output
if mqIntegrationStatusJSON {
enc := json.NewEncoder(os.Stdout)
enc.SetIndent("", " ")
return enc.Encode(output)
}
// Human-readable output
return printIntegrationStatus(&output)
}
// printIntegrationStatus prints the integration status in human-readable format.
func printIntegrationStatus(output *IntegrationStatusOutput) error {
fmt.Printf("Integration: %s\n", style.Bold.Render(output.Branch))
if output.Created != "" {
fmt.Printf("Created: %s\n", output.Created)
}
fmt.Printf("Ahead of main: %d commits\n", output.AheadOfMain)
// Merged MRs
fmt.Printf("\nMerged MRs (%d):\n", len(output.MergedMRs))
if len(output.MergedMRs) == 0 {
fmt.Printf(" %s\n", style.Dim.Render("(none)"))
} else {
for _, mr := range output.MergedMRs {
fmt.Printf(" %-12s %s\n", mr.ID, mr.Title)
}
}
// Pending MRs
fmt.Printf("\nPending MRs (%d):\n", len(output.PendingMRs))
if len(output.PendingMRs) == 0 {
fmt.Printf(" %s\n", style.Dim.Render("(none)"))
} else {
for _, mr := range output.PendingMRs {
statusInfo := ""
if mr.Status != "" && mr.Status != "open" {
statusInfo = fmt.Sprintf(" (%s)", mr.Status)
}
fmt.Printf(" %-12s %s%s\n", mr.ID, mr.Title, style.Dim.Render(statusInfo))
}
}
return nil
}

View File

@@ -470,3 +470,58 @@ func (g *Git) WorktreeList() ([]Worktree, error) {
return worktrees, nil return worktrees, nil
} }
// BranchCreatedDate returns the date when a branch was created.
// This uses the committer date of the first commit on the branch.
// Returns date in YYYY-MM-DD format.
func (g *Git) BranchCreatedDate(branch string) (string, error) {
// Get the date of the first commit on the branch that's not on main
// Use merge-base to find where the branch diverged from main
mergeBase, err := g.run("merge-base", "main", branch)
if err != nil {
// If merge-base fails, fall back to the branch tip's date
out, err := g.run("log", "-1", "--format=%cs", branch)
if err != nil {
return "", err
}
return out, nil
}
// Get the first commit after the merge base on this branch
out, err := g.run("log", "--format=%cs", "--reverse", mergeBase+".."+branch)
if err != nil {
return "", err
}
// Get the first line (first commit's date)
lines := strings.Split(out, "\n")
if len(lines) > 0 && lines[0] != "" {
return lines[0], nil
}
// If no commits after merge-base, the branch points to merge-base
// Return the merge-base commit date
out, err = g.run("log", "-1", "--format=%cs", mergeBase)
if err != nil {
return "", err
}
return out, nil
}
// CommitsAhead returns the number of commits that branch has ahead of base.
// For example, CommitsAhead("main", "feature") returns how many commits
// are on feature that are not on main.
func (g *Git) CommitsAhead(base, branch string) (int, error) {
out, err := g.run("rev-list", "--count", base+".."+branch)
if err != nil {
return 0, err
}
var count int
_, err = fmt.Sscanf(out, "%d", &count)
if err != nil {
return 0, fmt.Errorf("parsing commit count: %w", err)
}
return count, nil
}