Improvements to shell completions from PR #935: 1. Add IDPrefix field to IssueFilter for efficient database-level filtering - Queries are now filtered at SQL level instead of fetching all issues - Updated sqlite, transaction, and memory stores to support IDPrefix 2. Add ValidArgsFunction to additional commands: - dep (add, remove, list, tree) - comments, comment (add) - delete - graph - label (add, remove, list) - duplicate, supersede - audit - move - relate, unrelate - refile - gate (show, resolve, add-waiter) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> Executed-By: beads/crew/dave Rig: beads Role: crew
166 lines
5.1 KiB
Go
166 lines
5.1 KiB
Go
package main
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
|
|
"github.com/spf13/cobra"
|
|
"github.com/steveyegge/beads/internal/routing"
|
|
"github.com/steveyegge/beads/internal/storage/sqlite"
|
|
"github.com/steveyegge/beads/internal/types"
|
|
"github.com/steveyegge/beads/internal/ui"
|
|
)
|
|
|
|
var refileCmd = &cobra.Command{
|
|
Use: "refile <source-id> <target-rig>",
|
|
GroupID: "issues",
|
|
Short: "Move an issue to a different rig",
|
|
Long: `Move an issue from one rig to another.
|
|
|
|
This creates a new issue in the target rig with the same content,
|
|
then closes the source issue with a reference to the new location.
|
|
|
|
The target rig can be specified as:
|
|
- A rig name: beads, gastown
|
|
- A prefix: bd-, gt-
|
|
- A prefix without hyphen: bd, gt
|
|
|
|
Examples:
|
|
bd refile bd-8hea gastown # Move to gastown by rig name
|
|
bd refile bd-8hea gt- # Move to gastown by prefix
|
|
bd refile bd-8hea gt # Move to gastown (prefix without hyphen)`,
|
|
Args: cobra.ExactArgs(2),
|
|
Run: func(cmd *cobra.Command, args []string) {
|
|
CheckReadonly("refile")
|
|
|
|
sourceID := args[0]
|
|
targetRig := args[1]
|
|
|
|
keepOpen, _ := cmd.Flags().GetBool("keep-open")
|
|
|
|
ctx := rootCtx
|
|
|
|
// Step 1: Get the source issue (via routing if needed)
|
|
result, err := resolveAndGetIssueWithRouting(ctx, store, sourceID)
|
|
if err != nil {
|
|
FatalError("failed to find source issue: %v", err)
|
|
}
|
|
if result == nil || result.Issue == nil {
|
|
FatalError("source issue %s not found", sourceID)
|
|
}
|
|
defer result.Close()
|
|
|
|
sourceIssue := result.Issue
|
|
resolvedSourceID := result.ResolvedID
|
|
|
|
// Warn if source issue is already closed
|
|
if sourceIssue.Status == types.StatusClosed {
|
|
fmt.Fprintf(os.Stderr, "%s Source issue %s is already closed\n", ui.RenderWarn("⚠"), resolvedSourceID)
|
|
}
|
|
|
|
// Step 2: Find the town-level beads directory
|
|
townBeadsDir, err := findTownBeadsDir()
|
|
if err != nil {
|
|
FatalError("cannot refile: %v", err)
|
|
}
|
|
|
|
// Step 3: Resolve the target rig's beads directory
|
|
targetBeadsDir, targetPrefix, err := routing.ResolveBeadsDirForRig(targetRig, townBeadsDir)
|
|
if err != nil {
|
|
FatalError("%v", err)
|
|
}
|
|
|
|
// Check we're not refiling to the same rig
|
|
sourcePrefix := routing.ExtractPrefix(resolvedSourceID)
|
|
if sourcePrefix == targetPrefix {
|
|
FatalError("source issue %s is already in rig %q", resolvedSourceID, targetRig)
|
|
}
|
|
|
|
// Step 4: Open storage for the target rig
|
|
targetDBPath := filepath.Join(targetBeadsDir, "beads.db")
|
|
targetStore, err := sqlite.New(ctx, targetDBPath)
|
|
if err != nil {
|
|
FatalError("failed to open target rig database: %v", err)
|
|
}
|
|
defer func() {
|
|
if err := targetStore.Close(); err != nil {
|
|
fmt.Fprintf(os.Stderr, "warning: failed to close target rig database: %v\n", err)
|
|
}
|
|
}()
|
|
|
|
// Step 5: Create the new issue in target rig (copy all fields)
|
|
newIssue := &types.Issue{
|
|
// Don't copy ID - let target rig generate new one
|
|
Title: sourceIssue.Title,
|
|
Description: sourceIssue.Description,
|
|
Design: sourceIssue.Design,
|
|
AcceptanceCriteria: sourceIssue.AcceptanceCriteria,
|
|
Status: types.StatusOpen, // Always start as open
|
|
Priority: sourceIssue.Priority,
|
|
IssueType: sourceIssue.IssueType,
|
|
Assignee: sourceIssue.Assignee,
|
|
ExternalRef: sourceIssue.ExternalRef,
|
|
EstimatedMinutes: sourceIssue.EstimatedMinutes,
|
|
SourceRepo: sourceIssue.SourceRepo,
|
|
Ephemeral: sourceIssue.Ephemeral,
|
|
MolType: sourceIssue.MolType,
|
|
RoleType: sourceIssue.RoleType,
|
|
Rig: sourceIssue.Rig,
|
|
CreatedBy: actor,
|
|
}
|
|
|
|
// Append refiled note to description
|
|
if newIssue.Description != "" {
|
|
newIssue.Description += "\n\n"
|
|
}
|
|
newIssue.Description += fmt.Sprintf("(Refiled from %s)", resolvedSourceID)
|
|
|
|
if err := targetStore.CreateIssue(ctx, newIssue, actor); err != nil {
|
|
FatalError("failed to create issue in target rig: %v", err)
|
|
}
|
|
|
|
// Step 6: Copy labels if any
|
|
labels, err := result.Store.GetLabels(ctx, resolvedSourceID)
|
|
if err == nil && len(labels) > 0 {
|
|
for _, label := range labels {
|
|
if err := targetStore.AddLabel(ctx, newIssue.ID, label, actor); err != nil {
|
|
WarnError("failed to copy label %s: %v", label, err)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Step 7: Close the source issue (unless --keep-open)
|
|
if !keepOpen {
|
|
closeReason := fmt.Sprintf("Refiled to %s", newIssue.ID)
|
|
if err := result.Store.CloseIssue(ctx, resolvedSourceID, closeReason, actor, ""); err != nil {
|
|
WarnError("failed to close source issue: %v", err)
|
|
}
|
|
// Schedule auto-flush if source was local store
|
|
if !result.Routed {
|
|
markDirtyAndScheduleFlush()
|
|
}
|
|
}
|
|
|
|
// Output
|
|
if jsonOutput {
|
|
outputJSON(map[string]interface{}{
|
|
"source": resolvedSourceID,
|
|
"target": newIssue.ID,
|
|
"closed": !keepOpen,
|
|
})
|
|
} else {
|
|
fmt.Printf("%s Refiled %s → %s\n", ui.RenderPass("✓"), resolvedSourceID, newIssue.ID)
|
|
if !keepOpen {
|
|
fmt.Printf(" Source issue closed\n")
|
|
}
|
|
}
|
|
},
|
|
}
|
|
|
|
func init() {
|
|
refileCmd.Flags().Bool("keep-open", false, "Keep the source issue open (don't close it)")
|
|
refileCmd.ValidArgsFunction = issueIDCompletion
|
|
rootCmd.AddCommand(refileCmd)
|
|
}
|