diff --git a/internal/beads/beads.go b/internal/beads/beads.go index 139f8ce9..3384d611 100644 --- a/internal/beads/beads.go +++ b/internal/beads/beads.go @@ -1118,3 +1118,28 @@ func (b *Beads) GetRoleConfig(roleBeadID string) (*RoleConfig, error) { return ParseRoleConfig(issue.Description), nil } + +// FindMRForBranch searches for an existing merge-request bead for the given branch. +// Returns the MR bead if found, nil if not found. +// This enables idempotent `gt done` - if an MR already exists, we skip creation. +func (b *Beads) FindMRForBranch(branch string) (*Issue, error) { + // List all merge-request beads (open status only - closed MRs are already processed) + issues, err := b.List(ListOptions{ + Status: "open", + Type: "merge-request", + }) + if err != nil { + return nil, err + } + + // Search for one matching this branch + // MR description format: "branch: \ntarget: ..." + branchPrefix := "branch: " + branch + "\n" + for _, issue := range issues { + if strings.HasPrefix(issue.Description, branchPrefix) { + return issue, nil + } + } + + return nil, nil +} diff --git a/internal/cmd/done.go b/internal/cmd/done.go index 2724fa0d..2767f857 100644 --- a/internal/cmd/done.go +++ b/internal/cmd/done.go @@ -153,29 +153,43 @@ func runDone(cmd *cobra.Command, args []string) error { } } - // Build MR bead title and description - title := fmt.Sprintf("Merge: %s", issueID) - description := fmt.Sprintf("branch: %s\ntarget: %s\nsource_issue: %s\nrig: %s", - branch, target, issueID, rigName) - if worker != "" { - description += fmt.Sprintf("\nworker: %s", worker) - } - - // Create MR bead (ephemeral wisp - will be cleaned up after merge) - mrIssue, err := bd.Create(beads.CreateOptions{ - Title: title, - Type: "merge-request", - Priority: priority, - Description: description, - }) + // Check if MR bead already exists for this branch (idempotency) + existingMR, err := bd.FindMRForBranch(branch) if err != nil { - return fmt.Errorf("creating merge request bead: %w", err) + style.PrintWarning("could not check for existing MR: %v", err) + // Continue with creation attempt - Create will fail if duplicate } - mrID = mrIssue.ID - // Success output - fmt.Printf("%s Work submitted to merge queue\n", style.Bold.Render("✓")) - fmt.Printf(" MR ID: %s\n", style.Bold.Render(mrID)) + if existingMR != nil { + // MR already exists - use it instead of creating a new one + mrID = existingMR.ID + fmt.Printf("%s MR already exists (idempotent)\n", style.Bold.Render("✓")) + fmt.Printf(" MR ID: %s\n", style.Bold.Render(mrID)) + } else { + // Build MR bead title and description + title := fmt.Sprintf("Merge: %s", issueID) + description := fmt.Sprintf("branch: %s\ntarget: %s\nsource_issue: %s\nrig: %s", + branch, target, issueID, rigName) + if worker != "" { + description += fmt.Sprintf("\nworker: %s", worker) + } + + // Create MR bead (ephemeral wisp - will be cleaned up after merge) + mrIssue, err := bd.Create(beads.CreateOptions{ + Title: title, + Type: "merge-request", + Priority: priority, + Description: description, + }) + if err != nil { + return fmt.Errorf("creating merge request bead: %w", err) + } + mrID = mrIssue.ID + + // Success output + fmt.Printf("%s Work submitted to merge queue\n", style.Bold.Render("✓")) + fmt.Printf(" MR ID: %s\n", style.Bold.Render(mrID)) + } fmt.Printf(" Source: %s\n", branch) fmt.Printf(" Target: %s\n", target) fmt.Printf(" Issue: %s\n", issueID)