From 5d2daf2e1ef483cf2aff1f7bf8b9c37fb84563cd Mon Sep 17 00:00:00 2001 From: Steve Yegge Date: Sat, 20 Dec 2025 19:57:16 -0800 Subject: [PATCH] fix(pin): add 'pinned' field to allowed update fields (gt-zr0a) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The 'bd pin' command was failing with "invalid field for update: pinned" because the pinned field was missing from allowedUpdateFields. Fixes: - Add 'pinned' to allowedUpdateFields in queries.go - Update importer to include pinned field in updates during import - Add equalBool comparator for IssueDataChanged to detect pinned changes - Fix stats query to count pinned=1 instead of status='pinned' - Fix pinIndicator in list.go to check issue.Pinned instead of status This unblocks gt mail --pinned functionality. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- cmd/bd/list.go | 4 ++-- internal/importer/importer.go | 4 ++++ internal/importer/utils.go | 11 +++++++++++ internal/storage/sqlite/events.go | 2 +- internal/storage/sqlite/queries.go | 2 ++ 5 files changed, 20 insertions(+), 3 deletions(-) diff --git a/cmd/bd/list.go b/cmd/bd/list.go index 7b6b1a9c..5011f52e 100644 --- a/cmd/bd/list.go +++ b/cmd/bd/list.go @@ -38,9 +38,9 @@ func parseTimeFlag(s string) (time.Time, error) { return time.Time{}, fmt.Errorf("unable to parse time %q (try formats: 2006-01-02, 2006-01-02T15:04:05, or RFC3339)", s) } -// pinIndicator returns a pushpin emoji prefix for pinned issues (bd-18b) +// pinIndicator returns a pushpin emoji prefix for pinned issues (bd-18b, bd-7h5) func pinIndicator(issue *types.Issue) string { - if issue.Status == types.StatusPinned { + if issue.Pinned { return "📌 " } return "" diff --git a/internal/importer/importer.go b/internal/importer/importer.go index 88db09b3..5d21c75f 100644 --- a/internal/importer/importer.go +++ b/internal/importer/importer.go @@ -554,6 +554,8 @@ func upsertIssues(ctx context.Context, sqliteStore *sqlite.SQLiteStorage, issues updates["acceptance_criteria"] = incoming.AcceptanceCriteria updates["notes"] = incoming.Notes updates["closed_at"] = incoming.ClosedAt + // Pinned field (bd-7h5) + updates["pinned"] = incoming.Pinned if incoming.Assignee != "" { updates["assignee"] = incoming.Assignee @@ -647,6 +649,8 @@ func upsertIssues(ctx context.Context, sqliteStore *sqlite.SQLiteStorage, issues updates["acceptance_criteria"] = incoming.AcceptanceCriteria updates["notes"] = incoming.Notes updates["closed_at"] = incoming.ClosedAt + // Pinned field (bd-7h5) + updates["pinned"] = incoming.Pinned if incoming.Assignee != "" { updates["assignee"] = incoming.Assignee diff --git a/internal/importer/utils.go b/internal/importer/utils.go index b6828730..0cf5c6ab 100644 --- a/internal/importer/utils.go +++ b/internal/importer/utils.go @@ -112,6 +112,15 @@ func (fc *fieldComparator) equalPriority(existing int, newVal interface{}) bool return ok && int64(existing) == newPriority } +func (fc *fieldComparator) equalBool(existingVal bool, newVal interface{}) bool { + switch t := newVal.(type) { + case bool: + return existingVal == t + default: + return false + } +} + func (fc *fieldComparator) checkFieldChanged(key string, existing *types.Issue, newVal interface{}) bool { switch key { case "title": @@ -134,6 +143,8 @@ func (fc *fieldComparator) checkFieldChanged(key string, existing *types.Issue, return !fc.equalStr(existing.Assignee, newVal) case "external_ref": return !fc.equalPtrStr(existing.ExternalRef, newVal) + case "pinned": + return !fc.equalBool(existing.Pinned, newVal) default: return false } diff --git a/internal/storage/sqlite/events.go b/internal/storage/sqlite/events.go index 1bdd11e8..3e24b692 100644 --- a/internal/storage/sqlite/events.go +++ b/internal/storage/sqlite/events.go @@ -121,7 +121,7 @@ func (s *SQLiteStorage) GetStatistics(ctx context.Context) (*types.Statistics, e COALESCE(SUM(CASE WHEN status = 'closed' THEN 1 ELSE 0 END), 0) as closed, COALESCE(SUM(CASE WHEN status = 'deferred' THEN 1 ELSE 0 END), 0) as deferred, COALESCE(SUM(CASE WHEN status = 'tombstone' THEN 1 ELSE 0 END), 0) as tombstone, - COALESCE(SUM(CASE WHEN status = 'pinned' THEN 1 ELSE 0 END), 0) as pinned + COALESCE(SUM(CASE WHEN pinned = 1 THEN 1 ELSE 0 END), 0) as pinned FROM issues `).Scan(&stats.TotalIssues, &stats.OpenIssues, &stats.InProgressIssues, &stats.ClosedIssues, &stats.DeferredIssues, &stats.TombstoneIssues, &stats.PinnedIssues) if err != nil { diff --git a/internal/storage/sqlite/queries.go b/internal/storage/sqlite/queries.go index 13bca543..b9c90d2d 100644 --- a/internal/storage/sqlite/queries.go +++ b/internal/storage/sqlite/queries.go @@ -558,6 +558,8 @@ var allowedUpdateFields = map[string]bool{ // Messaging fields (bd-kwro) "sender": true, "ephemeral": true, + // Pinned field (bd-7h5) + "pinned": true, // NOTE: replies_to, relates_to, duplicate_of, superseded_by removed per Decision 004 // Use AddDependency() to create graph edges instead }