bd sync: 2025-12-27 15:56:42
This commit is contained in:
@@ -17,8 +17,7 @@ import (
|
||||
// Excludes pinned issues which are persistent anchors, not actionable work (bd-92u)
|
||||
func (s *SQLiteStorage) GetReadyWork(ctx context.Context, filter types.WorkFilter) ([]*types.Issue, error) {
|
||||
whereClauses := []string{
|
||||
"i.pinned = 0", // Exclude pinned issues (bd-92u)
|
||||
"(i.ephemeral = 0 OR i.ephemeral IS NULL)", // Exclude wisps (hq-t15s)
|
||||
"i.pinned = 0", // Exclude pinned issues (bd-92u)
|
||||
}
|
||||
args := []interface{}{}
|
||||
|
||||
@@ -87,25 +86,6 @@ func (s *SQLiteStorage) GetReadyWork(ctx context.Context, filter types.WorkFilte
|
||||
}
|
||||
}
|
||||
|
||||
// Parent filtering: filter to all descendants of a root issue (epic/molecule)
|
||||
// Uses recursive CTE to find all descendants via parent-child dependencies
|
||||
if filter.ParentID != nil {
|
||||
whereClauses = append(whereClauses, `
|
||||
i.id IN (
|
||||
WITH RECURSIVE descendants AS (
|
||||
SELECT issue_id FROM dependencies
|
||||
WHERE type = 'parent-child' AND depends_on_id = ?
|
||||
UNION ALL
|
||||
SELECT d.issue_id FROM dependencies d
|
||||
JOIN descendants dt ON d.depends_on_id = dt.issue_id
|
||||
WHERE d.type = 'parent-child'
|
||||
)
|
||||
SELECT issue_id FROM descendants
|
||||
)
|
||||
`)
|
||||
args = append(args, *filter.ParentID)
|
||||
}
|
||||
|
||||
// Build WHERE clause properly
|
||||
whereSQL := strings.Join(whereClauses, " AND ")
|
||||
|
||||
@@ -138,7 +118,7 @@ func (s *SQLiteStorage) GetReadyWork(ctx context.Context, filter types.WorkFilte
|
||||
query := fmt.Sprintf(`
|
||||
SELECT i.id, i.content_hash, 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.created_by, i.updated_at, i.closed_at, i.external_ref, i.source_repo, i.close_reason,
|
||||
i.created_at, i.updated_at, i.closed_at, i.external_ref, i.source_repo, i.close_reason,
|
||||
i.deleted_at, i.deleted_by, i.delete_reason, i.original_type,
|
||||
i.sender, i.ephemeral, i.pinned, i.is_template,
|
||||
i.await_type, i.await_id, i.timeout_ns, i.waiters
|
||||
@@ -400,7 +380,7 @@ func (s *SQLiteStorage) GetStaleIssues(ctx context.Context, filter types.StaleFi
|
||||
issue.Sender = sender.String
|
||||
}
|
||||
if ephemeral.Valid && ephemeral.Int64 != 0 {
|
||||
issue.Ephemeral = true
|
||||
issue.Wisp = true
|
||||
}
|
||||
// Pinned field (bd-7h5)
|
||||
if pinned.Valid && pinned.Int64 != 0 {
|
||||
@@ -433,7 +413,7 @@ func (s *SQLiteStorage) GetStaleIssues(ctx context.Context, filter types.StaleFi
|
||||
// GetBlockedIssues returns issues that are blocked by dependencies or have status=blocked
|
||||
// Note: Pinned issues are excluded from the output (beads-ei4)
|
||||
// Note: Includes external: references in blocked_by list (bd-om4a)
|
||||
func (s *SQLiteStorage) GetBlockedIssues(ctx context.Context, filter types.WorkFilter) ([]*types.BlockedIssue, error) {
|
||||
func (s *SQLiteStorage) GetBlockedIssues(ctx context.Context) ([]*types.BlockedIssue, error) {
|
||||
// Use UNION to combine:
|
||||
// 1. Issues with open/in_progress/blocked status that have dependency blockers
|
||||
// 2. Issues with status=blocked (even if they have no dependency blockers)
|
||||
@@ -443,41 +423,11 @@ func (s *SQLiteStorage) GetBlockedIssues(ctx context.Context, filter types.WorkF
|
||||
// For blocked_by_count and blocker_ids:
|
||||
// - Count local blockers (open issues) + external refs (external:*)
|
||||
// - External refs are always considered "open" until resolved (bd-om4a)
|
||||
|
||||
// Build additional WHERE clauses for filtering
|
||||
var filterClauses []string
|
||||
var args []any
|
||||
|
||||
// Parent filtering: filter to all descendants of a root issue (epic/molecule)
|
||||
if filter.ParentID != nil {
|
||||
filterClauses = append(filterClauses, `
|
||||
i.id IN (
|
||||
WITH RECURSIVE descendants AS (
|
||||
SELECT issue_id FROM dependencies
|
||||
WHERE type = 'parent-child' AND depends_on_id = ?
|
||||
UNION ALL
|
||||
SELECT d.issue_id FROM dependencies d
|
||||
JOIN descendants dt ON d.depends_on_id = dt.issue_id
|
||||
WHERE d.type = 'parent-child'
|
||||
)
|
||||
SELECT issue_id FROM descendants
|
||||
)
|
||||
`)
|
||||
args = append(args, *filter.ParentID)
|
||||
}
|
||||
|
||||
// Build filter clause SQL
|
||||
filterSQL := ""
|
||||
if len(filterClauses) > 0 {
|
||||
filterSQL = " AND " + strings.Join(filterClauses, " AND ")
|
||||
}
|
||||
|
||||
// nolint:gosec // G201: filterSQL contains only parameterized WHERE clauses with ? placeholders, not user input
|
||||
query := fmt.Sprintf(`
|
||||
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.created_by, i.updated_at, i.closed_at, i.external_ref, i.source_repo,
|
||||
i.created_at, i.updated_at, i.closed_at, i.external_ref, i.source_repo,
|
||||
COALESCE(COUNT(d.depends_on_id), 0) as blocked_by_count,
|
||||
COALESCE(GROUP_CONCAT(d.depends_on_id, ','), '') as blocker_ids
|
||||
FROM issues i
|
||||
@@ -491,7 +441,7 @@ func (s *SQLiteStorage) GetBlockedIssues(ctx context.Context, filter types.WorkF
|
||||
AND blocker.status IN ('open', 'in_progress', 'blocked', 'deferred')
|
||||
)
|
||||
-- External refs: always included (resolution happens at query time)
|
||||
OR d.depends_on_id LIKE 'external:%%'
|
||||
OR d.depends_on_id LIKE 'external:%'
|
||||
)
|
||||
WHERE i.status IN ('open', 'in_progress', 'blocked', 'deferred')
|
||||
AND i.pinned = 0
|
||||
@@ -511,14 +461,12 @@ func (s *SQLiteStorage) GetBlockedIssues(ctx context.Context, filter types.WorkF
|
||||
SELECT 1 FROM dependencies d3
|
||||
WHERE d3.issue_id = i.id
|
||||
AND d3.type = 'blocks'
|
||||
AND d3.depends_on_id LIKE 'external:%%'
|
||||
AND d3.depends_on_id LIKE 'external:%'
|
||||
)
|
||||
)
|
||||
%s
|
||||
GROUP BY i.id
|
||||
ORDER BY i.priority ASC
|
||||
`, filterSQL)
|
||||
rows, err := s.db.QueryContext(ctx, query, args...)
|
||||
`)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get blocked issues: %w", err)
|
||||
}
|
||||
@@ -538,7 +486,7 @@ func (s *SQLiteStorage) GetBlockedIssues(ctx context.Context, filter types.WorkF
|
||||
&issue.ID, &issue.Title, &issue.Description, &issue.Design,
|
||||
&issue.AcceptanceCriteria, &issue.Notes, &issue.Status,
|
||||
&issue.Priority, &issue.IssueType, &assignee, &estimatedMinutes,
|
||||
&issue.CreatedAt, &issue.CreatedBy, &issue.UpdatedAt, &closedAt, &externalRef, &sourceRepo, &issue.BlockedByCount,
|
||||
&issue.CreatedAt, &issue.UpdatedAt, &closedAt, &externalRef, &sourceRepo, &issue.BlockedByCount,
|
||||
&blockerIDsStr,
|
||||
)
|
||||
if err != nil {
|
||||
@@ -648,49 +596,6 @@ func filterBlockedByExternalDeps(ctx context.Context, blocked []*types.BlockedIs
|
||||
return result
|
||||
}
|
||||
|
||||
// GetNewlyUnblockedByClose returns issues that became unblocked when the given issue was closed.
|
||||
// This is used by the --suggest-next flag on bd close to show what work is now available.
|
||||
// An issue is "newly unblocked" if:
|
||||
// - It had a 'blocks' dependency on the closed issue
|
||||
// - It is now unblocked (not in blocked_issues_cache)
|
||||
// - It has status open or in_progress (ready to work on)
|
||||
//
|
||||
// The cache is already rebuilt by CloseIssue before this is called, so we just need to
|
||||
// find dependents that are no longer blocked.
|
||||
func (s *SQLiteStorage) GetNewlyUnblockedByClose(ctx context.Context, closedIssueID string) ([]*types.Issue, error) {
|
||||
// Find issues that:
|
||||
// 1. Had a 'blocks' dependency on the closed issue
|
||||
// 2. Are now NOT in blocked_issues_cache (unblocked)
|
||||
// 3. Have status open or in_progress
|
||||
// 4. Are not pinned
|
||||
query := `
|
||||
SELECT i.id, i.content_hash, 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.created_by, i.updated_at, i.closed_at, i.external_ref, i.source_repo, i.close_reason,
|
||||
i.deleted_at, i.deleted_by, i.delete_reason, i.original_type,
|
||||
i.sender, i.ephemeral, i.pinned, i.is_template,
|
||||
i.await_type, i.await_id, i.timeout_ns, i.waiters
|
||||
FROM issues i
|
||||
JOIN dependencies d ON i.id = d.issue_id
|
||||
WHERE d.depends_on_id = ?
|
||||
AND d.type = 'blocks'
|
||||
AND i.status IN ('open', 'in_progress')
|
||||
AND i.pinned = 0
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM blocked_issues_cache WHERE issue_id = i.id
|
||||
)
|
||||
ORDER BY i.priority ASC
|
||||
`
|
||||
|
||||
rows, err := s.db.QueryContext(ctx, query, closedIssueID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get newly unblocked issues: %w", err)
|
||||
}
|
||||
defer func() { _ = rows.Close() }()
|
||||
|
||||
return s.scanIssues(ctx, rows)
|
||||
}
|
||||
|
||||
// buildOrderByClause generates the ORDER BY clause based on sort policy
|
||||
func buildOrderByClause(policy types.SortPolicy) string {
|
||||
switch policy {
|
||||
|
||||
Reference in New Issue
Block a user