diff --git a/internal/importer/importer.go b/internal/importer/importer.go index 5d21c75f..47ecb8f0 100644 --- a/internal/importer/importer.go +++ b/internal/importer/importer.go @@ -7,6 +7,7 @@ import ( "sort" "strings" + "github.com/steveyegge/beads/internal/config" "github.com/steveyegge/beads/internal/linear" "github.com/steveyegge/beads/internal/storage" "github.com/steveyegge/beads/internal/storage/sqlite" @@ -106,6 +107,12 @@ func ImportIssues(ctx context.Context, dbPath string, store storage.Storage, iss defer func() { _ = sqliteStore.Close() }() } + // GH#686: In multi-repo mode, skip prefix validation for all issues. + // Issues from additional repos have their own prefixes which are expected and correct. + if config.GetMultiRepoConfig() != nil && !opts.SkipPrefixValidation { + opts.SkipPrefixValidation = true + } + // Clear export_hashes before import to prevent staleness (bd-160) // Import operations may add/update issues, so export_hashes entries become invalid if !opts.DryRun { @@ -208,6 +215,12 @@ func handlePrefixMismatch(ctx context.Context, sqliteStore *sqlite.SQLiteStorage result.ExpectedPrefix = configuredPrefix + // GH#686: In multi-repo mode, allow all prefixes (nil = allow all) + allowedPrefixes := buildAllowedPrefixSet(configuredPrefix) + if allowedPrefixes == nil { + return issues, nil + } + // Analyze prefixes in imported issues // Track tombstones separately - they don't count as "real" mismatches (bd-6pni) tombstoneMismatchPrefixes := make(map[string]int) @@ -219,7 +232,7 @@ func handlePrefixMismatch(ctx context.Context, sqliteStore *sqlite.SQLiteStorage for _, issue := range issues { prefix := utils.ExtractIssuePrefix(issue.ID) - if prefix != configuredPrefix { + if !allowedPrefixes[prefix] { if issue.IsTombstone() { tombstoneMismatchPrefixes[prefix]++ tombstonesToRemove = append(tombstonesToRemove, issue.ID) @@ -939,3 +952,12 @@ func validateNoDuplicateExternalRefs(issues []*types.Issue, clearDuplicates bool return nil } + +// buildAllowedPrefixSet returns allowed prefixes, or nil to allow all (GH#686). +// In multi-repo mode, additional repos have their own prefixes - allow all. +func buildAllowedPrefixSet(primaryPrefix string) map[string]bool { + if config.GetMultiRepoConfig() != nil { + return nil // Multi-repo: allow all prefixes + } + return map[string]bool{primaryPrefix: true} +} diff --git a/internal/importer/importer_test.go b/internal/importer/importer_test.go index 37540e51..e11634b0 100644 --- a/internal/importer/importer_test.go +++ b/internal/importer/importer_test.go @@ -9,6 +9,7 @@ import ( "testing" "time" + "github.com/steveyegge/beads/internal/config" "github.com/steveyegge/beads/internal/storage/sqlite" "github.com/steveyegge/beads/internal/types" ) @@ -1477,3 +1478,89 @@ func TestImportMixedPrefixMismatch(t *testing.T) { t.Errorf("Error should mention prefix mismatch, got: %v", err) } } + +// TestMultiRepoPrefixValidation tests GH#686: multi-repo allows foreign prefixes. +func TestMultiRepoPrefixValidation(t *testing.T) { + if err := config.Initialize(); err != nil { + t.Fatalf("Failed to initialize config: %v", err) + } + + ctx := context.Background() + tmpDB := t.TempDir() + "/test.db" + store, err := sqlite.New(ctx, tmpDB) + if err != nil { + t.Fatalf("Failed to create store: %v", err) + } + defer store.Close() + + if err := store.SetConfig(ctx, "issue_prefix", "primary"); err != nil { + t.Fatalf("Failed to set prefix: %v", err) + } + + t.Run("single-repo mode rejects foreign prefixes", func(t *testing.T) { + config.Set("repos.primary", "") + config.Set("repos.additional", nil) + + issues := []*types.Issue{ + { + ID: "primary-1", + Title: "Primary issue", + Status: types.StatusOpen, + Priority: 2, + IssueType: types.TypeTask, + }, + { + ID: "foreign-1", + Title: "Foreign issue", + Status: types.StatusOpen, + Priority: 2, + IssueType: types.TypeTask, + }, + } + + _, err := ImportIssues(ctx, tmpDB, store, issues, Options{}) + if err == nil { + t.Error("Expected error for foreign prefix in single-repo mode") + } + if err != nil && !strings.Contains(err.Error(), "prefix mismatch") { + t.Errorf("Expected prefix mismatch error, got: %v", err) + } + }) + + t.Run("multi-repo mode allows foreign prefixes", func(t *testing.T) { + config.Set("repos.primary", "/some/primary/path") + config.Set("repos.additional", []string{"/some/additional/path"}) + defer func() { + config.Set("repos.primary", "") + config.Set("repos.additional", nil) + }() + + issues := []*types.Issue{ + { + ID: "primary-abc1", + Title: "Primary issue", + Status: types.StatusOpen, + Priority: 2, + IssueType: types.TypeTask, + }, + { + ID: "foreign-xyz2", + Title: "Foreign issue", + Status: types.StatusOpen, + Priority: 2, + IssueType: types.TypeTask, + SourceRepo: "~/code/foreign", + }, + } + + result, err := ImportIssues(ctx, tmpDB, store, issues, Options{ + SkipPrefixValidation: false, // Verify auto-skip kicks in + }) + if err != nil { + t.Errorf("Multi-repo mode should allow foreign prefixes, got error: %v", err) + } + if result != nil && result.PrefixMismatch { + t.Error("Multi-repo mode should not report prefix mismatch") + } + }) +}