Merge remote-tracking branch 'origin/main' into improve-sync-jsonl-msg

This commit is contained in:
Steve Yegge
2025-11-26 14:42:30 -08:00
5 changed files with 260 additions and 177 deletions
+45 -45
View File
File diff suppressed because one or more lines are too long
-1
View File
@@ -193,7 +193,6 @@ bd init --stealth
```
**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
- **Claude Code settings** (`.claude/settings.local.json`) - Adds `bd onboard` instruction for AI agents
-100
View File
@@ -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 --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
Claude Code settings with bd onboard instruction
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)
}
// Setup global gitattributes
if err := setupGlobalGitAttributes(homeDir, verbose); err != nil {
return fmt.Errorf("failed to setup global gitattributes: %w", err)
}
// Setup global gitignore
if err := setupGlobalGitIgnore(homeDir, verbose); err != nil {
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()
cyan := color.New(color.FgCyan).SprintFunc()
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(" 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"))
@@ -1194,99 +1187,6 @@ func setupStealthMode(verbose bool) error {
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
func setupGlobalGitIgnore(homeDir string, verbose bool) error {
// Check if user already has a global gitignore file configured
+73 -31
View File
@@ -30,6 +30,9 @@ type MemoryStorage struct {
metadata map[string]string // Metadata key-value pairs
counters map[string]int // Prefix -> Last ID
// Indexes for O(1) lookups
externalRefToID map[string]string // ExternalRef -> IssueID
// For tracking
dirty map[string]bool // IssueIDs that have been modified
@@ -40,16 +43,17 @@ type MemoryStorage struct {
// New creates a new in-memory storage backend
func New(jsonlPath string) *MemoryStorage {
return &MemoryStorage{
issues: make(map[string]*types.Issue),
dependencies: make(map[string][]*types.Dependency),
labels: make(map[string][]string),
events: make(map[string][]*types.Event),
comments: make(map[string][]*types.Comment),
config: make(map[string]string),
metadata: make(map[string]string),
counters: make(map[string]int),
dirty: make(map[string]bool),
jsonlPath: jsonlPath,
issues: make(map[string]*types.Issue),
dependencies: make(map[string][]*types.Dependency),
labels: make(map[string][]string),
events: make(map[string][]*types.Event),
comments: make(map[string][]*types.Comment),
config: make(map[string]string),
metadata: make(map[string]string),
counters: make(map[string]int),
externalRefToID: make(map[string]string),
dirty: make(map[string]bool),
jsonlPath: jsonlPath,
}
}
@@ -67,6 +71,11 @@ func (m *MemoryStorage) LoadFromIssues(issues []*types.Issue) error {
// Store the 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
if len(issue.Dependencies) > 0 {
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.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
event := &types.Event{
IssueID: issue.ID,
@@ -244,6 +258,11 @@ func (m *MemoryStorage) CreateIssues(ctx context.Context, issues []*types.Issue,
m.issues[issue.ID] = issue
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
event := &types.Event{
IssueID: issue.ID,
@@ -288,28 +307,31 @@ func (m *MemoryStorage) GetIssueByExternalRef(ctx context.Context, externalRef s
m.mu.RLock()
defer m.mu.RUnlock()
// Linear search through all issues to find match by external_ref
for _, issue := range m.issues {
if issue.ExternalRef != nil && *issue.ExternalRef == externalRef {
// 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
}
// O(1) lookup using index
issueID, exists := m.externalRefToID[externalRef]
if !exists {
return nil, nil
}
// Not found
return nil, nil
issue, exists := m.issues[issueID]
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
@@ -375,9 +397,23 @@ func (m *MemoryStorage) UpdateIssue(ctx context.Context, id string, updates map[
issue.Assignee = ""
}
case "external_ref":
// Update external ref index
oldRef := issue.ExternalRef
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
} else if value == nil {
// Remove old index entry if exists
if oldRef != nil && *oldRef != "" {
delete(m.externalRefToID, *oldRef)
}
issue.ExternalRef = nil
}
}
@@ -417,10 +453,16 @@ func (m *MemoryStorage) DeleteIssue(ctx context.Context, id string) error {
defer m.mu.Unlock()
// 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)
}
// Remove external ref index entry
if issue.ExternalRef != nil && *issue.ExternalRef != "" {
delete(m.externalRefToID, *issue.ExternalRef)
}
// Delete the issue
delete(m.issues, id)
+142
View File
@@ -879,3 +879,145 @@ func TestClose(t *testing.T) {
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")
}
}