Merge remote-tracking branch 'origin/main' into improve-sync-jsonl-msg
This commit is contained in:
File diff suppressed because one or more lines are too long
@@ -193,7 +193,6 @@ bd init --stealth
|
|||||||
```
|
```
|
||||||
|
|
||||||
**Stealth mode configures:**
|
**Stealth mode configures:**
|
||||||
- **Global gitattributes** (`~/.config/git/attributes`) - Enables beads merge for `**/.beads/issues.jsonl` files across all repos
|
|
||||||
- **Global gitignore** (`~/.config/git/ignore`) - Ignores `**/.beads/` and `**/.claude/settings.local.json` globally
|
- **Global gitignore** (`~/.config/git/ignore`) - Ignores `**/.beads/` and `**/.claude/settings.local.json` globally
|
||||||
- **Claude Code settings** (`.claude/settings.local.json`) - Adds `bd onboard` instruction for AI agents
|
- **Claude Code settings** (`.claude/settings.local.json`) - Adds `bd onboard` instruction for AI agents
|
||||||
|
|
||||||
|
|||||||
100
cmd/bd/init.go
100
cmd/bd/init.go
@@ -31,7 +31,6 @@ and database file. Optionally specify a custom issue prefix.
|
|||||||
With --no-db: creates .beads/ directory and issues.jsonl file instead of SQLite database.
|
With --no-db: creates .beads/ directory and issues.jsonl file instead of SQLite database.
|
||||||
|
|
||||||
With --stealth: configures global git settings for invisible beads usage:
|
With --stealth: configures global git settings for invisible beads usage:
|
||||||
• Global gitattributes for beads merge support across all repos
|
|
||||||
• Global gitignore to prevent beads files from being committed
|
• Global gitignore to prevent beads files from being committed
|
||||||
• Claude Code settings with bd onboard instruction
|
• Claude Code settings with bd onboard instruction
|
||||||
Perfect for personal use without affecting repo collaborators.`,
|
Perfect for personal use without affecting repo collaborators.`,
|
||||||
@@ -1166,11 +1165,6 @@ func setupStealthMode(verbose bool) error {
|
|||||||
return fmt.Errorf("failed to get user home directory: %w", err)
|
return fmt.Errorf("failed to get user home directory: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Setup global gitattributes
|
|
||||||
if err := setupGlobalGitAttributes(homeDir, verbose); err != nil {
|
|
||||||
return fmt.Errorf("failed to setup global gitattributes: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Setup global gitignore
|
// Setup global gitignore
|
||||||
if err := setupGlobalGitIgnore(homeDir, verbose); err != nil {
|
if err := setupGlobalGitIgnore(homeDir, verbose); err != nil {
|
||||||
return fmt.Errorf("failed to setup global gitignore: %w", err)
|
return fmt.Errorf("failed to setup global gitignore: %w", err)
|
||||||
@@ -1185,7 +1179,6 @@ func setupStealthMode(verbose bool) error {
|
|||||||
green := color.New(color.FgGreen).SprintFunc()
|
green := color.New(color.FgGreen).SprintFunc()
|
||||||
cyan := color.New(color.FgCyan).SprintFunc()
|
cyan := color.New(color.FgCyan).SprintFunc()
|
||||||
fmt.Printf("\n%s Stealth mode configured successfully!\n\n", green("✓"))
|
fmt.Printf("\n%s Stealth mode configured successfully!\n\n", green("✓"))
|
||||||
fmt.Printf(" Global gitattributes: %s\n", cyan("configured for beads merge"))
|
|
||||||
fmt.Printf(" Global gitignore: %s\n", cyan(".beads/ and .claude/settings.local.json ignored"))
|
fmt.Printf(" Global gitignore: %s\n", cyan(".beads/ and .claude/settings.local.json ignored"))
|
||||||
fmt.Printf(" Claude settings: %s\n\n", cyan("configured with bd onboard instruction"))
|
fmt.Printf(" Claude settings: %s\n\n", cyan("configured with bd onboard instruction"))
|
||||||
fmt.Printf("Your beads setup is now %s - other repo collaborators won't see any beads-related files.\n\n", cyan("invisible"))
|
fmt.Printf("Your beads setup is now %s - other repo collaborators won't see any beads-related files.\n\n", cyan("invisible"))
|
||||||
@@ -1194,99 +1187,6 @@ func setupStealthMode(verbose bool) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// setupGlobalGitAttributes configures global gitattributes for beads merge
|
|
||||||
func setupGlobalGitAttributes(homeDir string, verbose bool) error {
|
|
||||||
// Check if user already has a global gitattributes file configured
|
|
||||||
cmd := exec.Command("git", "config", "--global", "core.attributesfile")
|
|
||||||
output, err := cmd.Output()
|
|
||||||
|
|
||||||
var attributesPath string
|
|
||||||
|
|
||||||
if err == nil && len(output) > 0 {
|
|
||||||
// User has already configured a global gitattributes file, use it
|
|
||||||
attributesPath = strings.TrimSpace(string(output))
|
|
||||||
if verbose {
|
|
||||||
fmt.Printf("Using existing configured global gitattributes file: %s\n", attributesPath)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// No global gitattributes file configured, check if standard location exists
|
|
||||||
configDir := filepath.Join(homeDir, ".config", "git")
|
|
||||||
standardAttributesPath := filepath.Join(configDir, "attributes")
|
|
||||||
|
|
||||||
if _, err := os.Stat(standardAttributesPath); err == nil {
|
|
||||||
// Standard global gitattributes file exists, use it
|
|
||||||
// No need to set git config - git automatically uses this standard location
|
|
||||||
attributesPath = standardAttributesPath
|
|
||||||
if verbose {
|
|
||||||
fmt.Printf("Using existing global gitattributes file: %s\n", attributesPath)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// No global gitattributes file exists, create one in standard location
|
|
||||||
// No need to set git config - git automatically uses this standard location
|
|
||||||
attributesPath = standardAttributesPath
|
|
||||||
|
|
||||||
// Ensure config directory exists
|
|
||||||
if err := os.MkdirAll(configDir, 0755); err != nil {
|
|
||||||
return fmt.Errorf("failed to create git config directory: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if verbose {
|
|
||||||
fmt.Printf("Creating new global gitattributes file: %s\n", attributesPath)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Read existing attributes file if it exists
|
|
||||||
var existingContent string
|
|
||||||
// #nosec G304 - user config path
|
|
||||||
if content, err := os.ReadFile(attributesPath); err == nil {
|
|
||||||
existingContent = string(content)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if beads merge attribute already exists
|
|
||||||
beadsPattern := "**/.beads/issues.jsonl merge=beads"
|
|
||||||
if strings.Contains(existingContent, beadsPattern) {
|
|
||||||
if verbose {
|
|
||||||
fmt.Printf("Global gitattributes already configured for beads\n")
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Append beads configuration
|
|
||||||
newContent := existingContent
|
|
||||||
if !strings.HasSuffix(newContent, "\n") && len(newContent) > 0 {
|
|
||||||
newContent += "\n"
|
|
||||||
}
|
|
||||||
newContent += "\n# Beads merge configuration (added by bd init --stealth)\n"
|
|
||||||
newContent += beadsPattern + "\n"
|
|
||||||
|
|
||||||
// Write the updated attributes file
|
|
||||||
// #nosec G306 - config file needs 0644
|
|
||||||
if err := os.WriteFile(attributesPath, []byte(newContent), 0644); err != nil {
|
|
||||||
return fmt.Errorf("failed to write global gitattributes: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Configure the beads merge driver
|
|
||||||
cmd = exec.Command("git", "config", "--global", "merge.beads.driver", "bd merge %A %O %A %B")
|
|
||||||
if output, err := cmd.CombinedOutput(); err != nil {
|
|
||||||
return fmt.Errorf("failed to configure beads merge driver: %w\n%s", err, output)
|
|
||||||
}
|
|
||||||
|
|
||||||
cmd = exec.Command("git", "config", "--global", "merge.beads.name", "bd JSONL merge driver")
|
|
||||||
if output, err := cmd.CombinedOutput(); err != nil {
|
|
||||||
// Non-fatal, the name is just descriptive
|
|
||||||
if verbose {
|
|
||||||
fmt.Fprintf(os.Stderr, "Warning: failed to set merge driver name: %v\n%s", err, output)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if verbose {
|
|
||||||
fmt.Printf("Configured global gitattributes for beads merge\n")
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// setupGlobalGitIgnore configures global gitignore to ignore beads and claude files
|
// setupGlobalGitIgnore configures global gitignore to ignore beads and claude files
|
||||||
func setupGlobalGitIgnore(homeDir string, verbose bool) error {
|
func setupGlobalGitIgnore(homeDir string, verbose bool) error {
|
||||||
// Check if user already has a global gitignore file configured
|
// Check if user already has a global gitignore file configured
|
||||||
|
|||||||
@@ -30,6 +30,9 @@ type MemoryStorage struct {
|
|||||||
metadata map[string]string // Metadata key-value pairs
|
metadata map[string]string // Metadata key-value pairs
|
||||||
counters map[string]int // Prefix -> Last ID
|
counters map[string]int // Prefix -> Last ID
|
||||||
|
|
||||||
|
// Indexes for O(1) lookups
|
||||||
|
externalRefToID map[string]string // ExternalRef -> IssueID
|
||||||
|
|
||||||
// For tracking
|
// For tracking
|
||||||
dirty map[string]bool // IssueIDs that have been modified
|
dirty map[string]bool // IssueIDs that have been modified
|
||||||
|
|
||||||
@@ -40,16 +43,17 @@ type MemoryStorage struct {
|
|||||||
// New creates a new in-memory storage backend
|
// New creates a new in-memory storage backend
|
||||||
func New(jsonlPath string) *MemoryStorage {
|
func New(jsonlPath string) *MemoryStorage {
|
||||||
return &MemoryStorage{
|
return &MemoryStorage{
|
||||||
issues: make(map[string]*types.Issue),
|
issues: make(map[string]*types.Issue),
|
||||||
dependencies: make(map[string][]*types.Dependency),
|
dependencies: make(map[string][]*types.Dependency),
|
||||||
labels: make(map[string][]string),
|
labels: make(map[string][]string),
|
||||||
events: make(map[string][]*types.Event),
|
events: make(map[string][]*types.Event),
|
||||||
comments: make(map[string][]*types.Comment),
|
comments: make(map[string][]*types.Comment),
|
||||||
config: make(map[string]string),
|
config: make(map[string]string),
|
||||||
metadata: make(map[string]string),
|
metadata: make(map[string]string),
|
||||||
counters: make(map[string]int),
|
counters: make(map[string]int),
|
||||||
dirty: make(map[string]bool),
|
externalRefToID: make(map[string]string),
|
||||||
jsonlPath: jsonlPath,
|
dirty: make(map[string]bool),
|
||||||
|
jsonlPath: jsonlPath,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -67,6 +71,11 @@ func (m *MemoryStorage) LoadFromIssues(issues []*types.Issue) error {
|
|||||||
// Store the issue
|
// Store the issue
|
||||||
m.issues[issue.ID] = issue
|
m.issues[issue.ID] = issue
|
||||||
|
|
||||||
|
// Index external ref for O(1) lookup
|
||||||
|
if issue.ExternalRef != nil && *issue.ExternalRef != "" {
|
||||||
|
m.externalRefToID[*issue.ExternalRef] = issue.ID
|
||||||
|
}
|
||||||
|
|
||||||
// Store dependencies
|
// Store dependencies
|
||||||
if len(issue.Dependencies) > 0 {
|
if len(issue.Dependencies) > 0 {
|
||||||
m.dependencies[issue.ID] = issue.Dependencies
|
m.dependencies[issue.ID] = issue.Dependencies
|
||||||
@@ -184,6 +193,11 @@ func (m *MemoryStorage) CreateIssue(ctx context.Context, issue *types.Issue, act
|
|||||||
m.issues[issue.ID] = issue
|
m.issues[issue.ID] = issue
|
||||||
m.dirty[issue.ID] = true
|
m.dirty[issue.ID] = true
|
||||||
|
|
||||||
|
// Index external ref for O(1) lookup
|
||||||
|
if issue.ExternalRef != nil && *issue.ExternalRef != "" {
|
||||||
|
m.externalRefToID[*issue.ExternalRef] = issue.ID
|
||||||
|
}
|
||||||
|
|
||||||
// Record event
|
// Record event
|
||||||
event := &types.Event{
|
event := &types.Event{
|
||||||
IssueID: issue.ID,
|
IssueID: issue.ID,
|
||||||
@@ -244,6 +258,11 @@ func (m *MemoryStorage) CreateIssues(ctx context.Context, issues []*types.Issue,
|
|||||||
m.issues[issue.ID] = issue
|
m.issues[issue.ID] = issue
|
||||||
m.dirty[issue.ID] = true
|
m.dirty[issue.ID] = true
|
||||||
|
|
||||||
|
// Index external ref for O(1) lookup
|
||||||
|
if issue.ExternalRef != nil && *issue.ExternalRef != "" {
|
||||||
|
m.externalRefToID[*issue.ExternalRef] = issue.ID
|
||||||
|
}
|
||||||
|
|
||||||
// Record event
|
// Record event
|
||||||
event := &types.Event{
|
event := &types.Event{
|
||||||
IssueID: issue.ID,
|
IssueID: issue.ID,
|
||||||
@@ -288,28 +307,31 @@ func (m *MemoryStorage) GetIssueByExternalRef(ctx context.Context, externalRef s
|
|||||||
m.mu.RLock()
|
m.mu.RLock()
|
||||||
defer m.mu.RUnlock()
|
defer m.mu.RUnlock()
|
||||||
|
|
||||||
// Linear search through all issues to find match by external_ref
|
// O(1) lookup using index
|
||||||
for _, issue := range m.issues {
|
issueID, exists := m.externalRefToID[externalRef]
|
||||||
if issue.ExternalRef != nil && *issue.ExternalRef == externalRef {
|
if !exists {
|
||||||
// Return a copy to avoid mutations
|
return nil, nil
|
||||||
issueCopy := *issue
|
|
||||||
|
|
||||||
// Attach dependencies
|
|
||||||
if deps, ok := m.dependencies[issue.ID]; ok {
|
|
||||||
issueCopy.Dependencies = deps
|
|
||||||
}
|
|
||||||
|
|
||||||
// Attach labels
|
|
||||||
if labels, ok := m.labels[issue.ID]; ok {
|
|
||||||
issueCopy.Labels = labels
|
|
||||||
}
|
|
||||||
|
|
||||||
return &issueCopy, nil
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Not found
|
issue, exists := m.issues[issueID]
|
||||||
return nil, nil
|
if !exists {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return a copy to avoid mutations
|
||||||
|
issueCopy := *issue
|
||||||
|
|
||||||
|
// Attach dependencies
|
||||||
|
if deps, ok := m.dependencies[issue.ID]; ok {
|
||||||
|
issueCopy.Dependencies = deps
|
||||||
|
}
|
||||||
|
|
||||||
|
// Attach labels
|
||||||
|
if labels, ok := m.labels[issue.ID]; ok {
|
||||||
|
issueCopy.Labels = labels
|
||||||
|
}
|
||||||
|
|
||||||
|
return &issueCopy, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// UpdateIssue updates fields on an issue
|
// UpdateIssue updates fields on an issue
|
||||||
@@ -375,9 +397,23 @@ func (m *MemoryStorage) UpdateIssue(ctx context.Context, id string, updates map[
|
|||||||
issue.Assignee = ""
|
issue.Assignee = ""
|
||||||
}
|
}
|
||||||
case "external_ref":
|
case "external_ref":
|
||||||
|
// Update external ref index
|
||||||
|
oldRef := issue.ExternalRef
|
||||||
if v, ok := value.(string); ok {
|
if v, ok := value.(string); ok {
|
||||||
|
// Remove old index entry if exists
|
||||||
|
if oldRef != nil && *oldRef != "" {
|
||||||
|
delete(m.externalRefToID, *oldRef)
|
||||||
|
}
|
||||||
|
// Add new index entry
|
||||||
|
if v != "" {
|
||||||
|
m.externalRefToID[v] = id
|
||||||
|
}
|
||||||
issue.ExternalRef = &v
|
issue.ExternalRef = &v
|
||||||
} else if value == nil {
|
} else if value == nil {
|
||||||
|
// Remove old index entry if exists
|
||||||
|
if oldRef != nil && *oldRef != "" {
|
||||||
|
delete(m.externalRefToID, *oldRef)
|
||||||
|
}
|
||||||
issue.ExternalRef = nil
|
issue.ExternalRef = nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -417,10 +453,16 @@ func (m *MemoryStorage) DeleteIssue(ctx context.Context, id string) error {
|
|||||||
defer m.mu.Unlock()
|
defer m.mu.Unlock()
|
||||||
|
|
||||||
// Check if issue exists
|
// Check if issue exists
|
||||||
if _, ok := m.issues[id]; !ok {
|
issue, ok := m.issues[id]
|
||||||
|
if !ok {
|
||||||
return fmt.Errorf("issue not found: %s", id)
|
return fmt.Errorf("issue not found: %s", id)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Remove external ref index entry
|
||||||
|
if issue.ExternalRef != nil && *issue.ExternalRef != "" {
|
||||||
|
delete(m.externalRefToID, *issue.ExternalRef)
|
||||||
|
}
|
||||||
|
|
||||||
// Delete the issue
|
// Delete the issue
|
||||||
delete(m.issues, id)
|
delete(m.issues, id)
|
||||||
|
|
||||||
|
|||||||
@@ -879,3 +879,145 @@ func TestClose(t *testing.T) {
|
|||||||
t.Error("Store should be closed")
|
t.Error("Store should be closed")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestGetIssueByExternalRef(t *testing.T) {
|
||||||
|
store := setupTestMemory(t)
|
||||||
|
defer store.Close()
|
||||||
|
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
// Create an issue with external ref
|
||||||
|
extRef := "github#123"
|
||||||
|
issue := &types.Issue{
|
||||||
|
Title: "Test issue with external ref",
|
||||||
|
Status: types.StatusOpen,
|
||||||
|
Priority: 1,
|
||||||
|
IssueType: types.TypeTask,
|
||||||
|
ExternalRef: &extRef,
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := store.CreateIssue(ctx, issue, "test-user"); err != nil {
|
||||||
|
t.Fatalf("CreateIssue failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Lookup by external ref should find it
|
||||||
|
found, err := store.GetIssueByExternalRef(ctx, "github#123")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("GetIssueByExternalRef failed: %v", err)
|
||||||
|
}
|
||||||
|
if found == nil {
|
||||||
|
t.Fatal("Expected to find issue by external ref")
|
||||||
|
}
|
||||||
|
if found.ID != issue.ID {
|
||||||
|
t.Errorf("Expected issue ID %s, got %s", issue.ID, found.ID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Lookup by non-existent ref should return nil
|
||||||
|
notFound, err := store.GetIssueByExternalRef(ctx, "nonexistent")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("GetIssueByExternalRef failed: %v", err)
|
||||||
|
}
|
||||||
|
if notFound != nil {
|
||||||
|
t.Error("Expected nil for non-existent external ref")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update external ref and verify index is updated
|
||||||
|
newRef := "github#456"
|
||||||
|
if err := store.UpdateIssue(ctx, issue.ID, map[string]interface{}{
|
||||||
|
"external_ref": newRef,
|
||||||
|
}, "test-user"); err != nil {
|
||||||
|
t.Fatalf("UpdateIssue failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Old ref should not find anything
|
||||||
|
oldRefResult, err := store.GetIssueByExternalRef(ctx, "github#123")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("GetIssueByExternalRef failed: %v", err)
|
||||||
|
}
|
||||||
|
if oldRefResult != nil {
|
||||||
|
t.Error("Old external ref should not find issue after update")
|
||||||
|
}
|
||||||
|
|
||||||
|
// New ref should find the issue
|
||||||
|
newRefResult, err := store.GetIssueByExternalRef(ctx, "github#456")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("GetIssueByExternalRef failed: %v", err)
|
||||||
|
}
|
||||||
|
if newRefResult == nil {
|
||||||
|
t.Fatal("New external ref should find issue")
|
||||||
|
}
|
||||||
|
if newRefResult.ID != issue.ID {
|
||||||
|
t.Errorf("Expected issue ID %s, got %s", issue.ID, newRefResult.ID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete issue and verify index is cleaned up
|
||||||
|
if err := store.DeleteIssue(ctx, issue.ID); err != nil {
|
||||||
|
t.Fatalf("DeleteIssue failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// External ref should not find anything after delete
|
||||||
|
deletedResult, err := store.GetIssueByExternalRef(ctx, "github#456")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("GetIssueByExternalRef failed: %v", err)
|
||||||
|
}
|
||||||
|
if deletedResult != nil {
|
||||||
|
t.Error("External ref should not find issue after delete")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetIssueByExternalRefLoadFromIssues(t *testing.T) {
|
||||||
|
store := New("")
|
||||||
|
defer store.Close()
|
||||||
|
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
// Load issues with external refs
|
||||||
|
extRef1 := "jira#100"
|
||||||
|
extRef2 := "jira#200"
|
||||||
|
issues := []*types.Issue{
|
||||||
|
{
|
||||||
|
ID: "bd-1",
|
||||||
|
Title: "Issue 1",
|
||||||
|
Status: types.StatusOpen,
|
||||||
|
Priority: 1,
|
||||||
|
IssueType: types.TypeTask,
|
||||||
|
ExternalRef: &extRef1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ID: "bd-2",
|
||||||
|
Title: "Issue 2",
|
||||||
|
Status: types.StatusOpen,
|
||||||
|
Priority: 2,
|
||||||
|
IssueType: types.TypeBug,
|
||||||
|
ExternalRef: &extRef2,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ID: "bd-3",
|
||||||
|
Title: "Issue 3 (no external ref)",
|
||||||
|
Status: types.StatusOpen,
|
||||||
|
Priority: 3,
|
||||||
|
IssueType: types.TypeFeature,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := store.LoadFromIssues(issues); err != nil {
|
||||||
|
t.Fatalf("LoadFromIssues failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Both external refs should be indexed
|
||||||
|
found1, err := store.GetIssueByExternalRef(ctx, "jira#100")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("GetIssueByExternalRef failed: %v", err)
|
||||||
|
}
|
||||||
|
if found1 == nil || found1.ID != "bd-1" {
|
||||||
|
t.Errorf("Expected to find bd-1 by external ref jira#100")
|
||||||
|
}
|
||||||
|
|
||||||
|
found2, err := store.GetIssueByExternalRef(ctx, "jira#200")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("GetIssueByExternalRef failed: %v", err)
|
||||||
|
}
|
||||||
|
if found2 == nil || found2.ID != "bd-2" {
|
||||||
|
t.Errorf("Expected to find bd-2 by external ref jira#200")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user