fix: skip prefix validation in multi-repo mode (GH#686) (#688)
Issues from additional repos have their own prefixes, which is expected. Skip validation when multi-repo config is present.
This commit is contained in:
@@ -7,6 +7,7 @@ import (
|
|||||||
"sort"
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"github.com/steveyegge/beads/internal/config"
|
||||||
"github.com/steveyegge/beads/internal/linear"
|
"github.com/steveyegge/beads/internal/linear"
|
||||||
"github.com/steveyegge/beads/internal/storage"
|
"github.com/steveyegge/beads/internal/storage"
|
||||||
"github.com/steveyegge/beads/internal/storage/sqlite"
|
"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() }()
|
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)
|
// Clear export_hashes before import to prevent staleness (bd-160)
|
||||||
// Import operations may add/update issues, so export_hashes entries become invalid
|
// Import operations may add/update issues, so export_hashes entries become invalid
|
||||||
if !opts.DryRun {
|
if !opts.DryRun {
|
||||||
@@ -208,6 +215,12 @@ func handlePrefixMismatch(ctx context.Context, sqliteStore *sqlite.SQLiteStorage
|
|||||||
|
|
||||||
result.ExpectedPrefix = configuredPrefix
|
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
|
// Analyze prefixes in imported issues
|
||||||
// Track tombstones separately - they don't count as "real" mismatches (bd-6pni)
|
// Track tombstones separately - they don't count as "real" mismatches (bd-6pni)
|
||||||
tombstoneMismatchPrefixes := make(map[string]int)
|
tombstoneMismatchPrefixes := make(map[string]int)
|
||||||
@@ -219,7 +232,7 @@ func handlePrefixMismatch(ctx context.Context, sqliteStore *sqlite.SQLiteStorage
|
|||||||
|
|
||||||
for _, issue := range issues {
|
for _, issue := range issues {
|
||||||
prefix := utils.ExtractIssuePrefix(issue.ID)
|
prefix := utils.ExtractIssuePrefix(issue.ID)
|
||||||
if prefix != configuredPrefix {
|
if !allowedPrefixes[prefix] {
|
||||||
if issue.IsTombstone() {
|
if issue.IsTombstone() {
|
||||||
tombstoneMismatchPrefixes[prefix]++
|
tombstoneMismatchPrefixes[prefix]++
|
||||||
tombstonesToRemove = append(tombstonesToRemove, issue.ID)
|
tombstonesToRemove = append(tombstonesToRemove, issue.ID)
|
||||||
@@ -939,3 +952,12 @@ func validateNoDuplicateExternalRefs(issues []*types.Issue, clearDuplicates bool
|
|||||||
|
|
||||||
return nil
|
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}
|
||||||
|
}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/steveyegge/beads/internal/config"
|
||||||
"github.com/steveyegge/beads/internal/storage/sqlite"
|
"github.com/steveyegge/beads/internal/storage/sqlite"
|
||||||
"github.com/steveyegge/beads/internal/types"
|
"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)
|
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")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user