Add nullable external_ref TEXT field to link bd issues with external systems like GitHub Issues, Jira, etc. Includes automatic schema migration for backward compatibility. Changes: - Added external_ref column to issues table with feature-based migration - Updated Issue struct with ExternalRef *string field - Added --external-ref flag to bd create and bd update commands - Updated all SQL queries across the codebase to include external_ref: - GetIssue, CreateIssue, UpdateIssue, SearchIssues - GetDependencies, GetDependents, GetDependencyTree - GetReadyWork, GetBlockedIssues, GetIssuesByLabel - Added external_ref handling in import/export logic - Follows existing patterns for nullable fields (sql.NullString) This enables tracking relationships between bd issues and external systems without requiring changes to existing databases or JSONL files. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
124 lines
3.4 KiB
Go
124 lines
3.4 KiB
Go
package sqlite
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"time"
|
|
|
|
"github.com/steveyegge/beads/internal/types"
|
|
)
|
|
|
|
// AddLabel adds a label to an issue
|
|
func (s *SQLiteStorage) AddLabel(ctx context.Context, issueID, label, actor string) error {
|
|
tx, err := s.db.BeginTx(ctx, nil)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to begin transaction: %w", err)
|
|
}
|
|
defer tx.Rollback()
|
|
|
|
_, err = tx.ExecContext(ctx, `
|
|
INSERT OR IGNORE INTO labels (issue_id, label)
|
|
VALUES (?, ?)
|
|
`, issueID, label)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to add label: %w", err)
|
|
}
|
|
|
|
_, err = tx.ExecContext(ctx, `
|
|
INSERT INTO events (issue_id, event_type, actor, comment)
|
|
VALUES (?, ?, ?, ?)
|
|
`, issueID, types.EventLabelAdded, actor, fmt.Sprintf("Added label: %s", label))
|
|
if err != nil {
|
|
return fmt.Errorf("failed to record event: %w", err)
|
|
}
|
|
|
|
// Mark issue as dirty for incremental export
|
|
_, err = tx.ExecContext(ctx, `
|
|
INSERT INTO dirty_issues (issue_id, marked_at)
|
|
VALUES (?, ?)
|
|
ON CONFLICT (issue_id) DO UPDATE SET marked_at = excluded.marked_at
|
|
`, issueID, time.Now())
|
|
if err != nil {
|
|
return fmt.Errorf("failed to mark issue dirty: %w", err)
|
|
}
|
|
|
|
return tx.Commit()
|
|
}
|
|
|
|
// RemoveLabel removes a label from an issue
|
|
func (s *SQLiteStorage) RemoveLabel(ctx context.Context, issueID, label, actor string) error {
|
|
tx, err := s.db.BeginTx(ctx, nil)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to begin transaction: %w", err)
|
|
}
|
|
defer tx.Rollback()
|
|
|
|
_, err = tx.ExecContext(ctx, `
|
|
DELETE FROM labels WHERE issue_id = ? AND label = ?
|
|
`, issueID, label)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to remove label: %w", err)
|
|
}
|
|
|
|
_, err = tx.ExecContext(ctx, `
|
|
INSERT INTO events (issue_id, event_type, actor, comment)
|
|
VALUES (?, ?, ?, ?)
|
|
`, issueID, types.EventLabelRemoved, actor, fmt.Sprintf("Removed label: %s", label))
|
|
if err != nil {
|
|
return fmt.Errorf("failed to record event: %w", err)
|
|
}
|
|
|
|
// Mark issue as dirty for incremental export
|
|
_, err = tx.ExecContext(ctx, `
|
|
INSERT INTO dirty_issues (issue_id, marked_at)
|
|
VALUES (?, ?)
|
|
ON CONFLICT (issue_id) DO UPDATE SET marked_at = excluded.marked_at
|
|
`, issueID, time.Now())
|
|
if err != nil {
|
|
return fmt.Errorf("failed to mark issue dirty: %w", err)
|
|
}
|
|
|
|
return tx.Commit()
|
|
}
|
|
|
|
// GetLabels returns all labels for an issue
|
|
func (s *SQLiteStorage) GetLabels(ctx context.Context, issueID string) ([]string, error) {
|
|
rows, err := s.db.QueryContext(ctx, `
|
|
SELECT label FROM labels WHERE issue_id = ? ORDER BY label
|
|
`, issueID)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to get labels: %w", err)
|
|
}
|
|
defer rows.Close()
|
|
|
|
var labels []string
|
|
for rows.Next() {
|
|
var label string
|
|
if err := rows.Scan(&label); err != nil {
|
|
return nil, err
|
|
}
|
|
labels = append(labels, label)
|
|
}
|
|
|
|
return labels, nil
|
|
}
|
|
|
|
// GetIssuesByLabel returns issues with a specific label
|
|
func (s *SQLiteStorage) GetIssuesByLabel(ctx context.Context, label string) ([]*types.Issue, error) {
|
|
rows, err := s.db.QueryContext(ctx, `
|
|
SELECT i.id, i.title, i.description, i.design, i.acceptance_criteria, i.notes,
|
|
i.status, i.priority, i.issue_type, i.assignee, i.estimated_minutes,
|
|
i.created_at, i.updated_at, i.closed_at, i.external_ref
|
|
FROM issues i
|
|
JOIN labels l ON i.id = l.issue_id
|
|
WHERE l.label = ?
|
|
ORDER BY i.priority ASC, i.created_at DESC
|
|
`, label)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to get issues by label: %w", err)
|
|
}
|
|
defer rows.Close()
|
|
|
|
return scanIssues(rows)
|
|
}
|