refactor(cmd): migrate sort.Slice to slices.SortFunc (bd-u2sc.2)

Modernize sorting code to use Go 1.21+ slices package:
- Replace sort.Slice with slices.SortFunc across 16 files
- Use cmp.Compare for orderable types (strings, ints)
- Use time.Time.Compare for time comparisons
- Use cmp.Or for multi-field sorting
- Use slices.SortStableFunc where stability matters

Benefits: cleaner 3-way comparison, slightly better performance,
modern idiomatic Go.

Part of GH#692 refactoring epic.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Steve Yegge
2025-12-22 15:39:55 -08:00
parent 82cbd98e50
commit e67712dcd4
16 changed files with 96 additions and 80 deletions
+6 -5
View File
@@ -3,6 +3,7 @@ package main
import ( import (
"bufio" "bufio"
"bytes" "bytes"
"cmp"
"context" "context"
"crypto/sha256" "crypto/sha256"
"encoding/hex" "encoding/hex"
@@ -10,7 +11,7 @@ import (
"fmt" "fmt"
"os" "os"
"path/filepath" "path/filepath"
"sort" "slices"
"strings" "strings"
"time" "time"
@@ -218,8 +219,8 @@ func autoImportIfNewer() {
for oldID, newID := range result.IDMapping { for oldID, newID := range result.IDMapping {
mappings = append(mappings, mapping{oldID, newID}) mappings = append(mappings, mapping{oldID, newID})
} }
sort.Slice(mappings, func(i, j int) bool { slices.SortFunc(mappings, func(a, b mapping) int {
return mappings[i].oldID < mappings[j].oldID return cmp.Compare(a.oldID, b.oldID)
}) })
maxShow := 10 maxShow := 10
@@ -442,8 +443,8 @@ func validateJSONLIntegrity(ctx context.Context, jsonlPath string) (bool, error)
func writeJSONLAtomic(jsonlPath string, issues []*types.Issue) ([]string, error) { func writeJSONLAtomic(jsonlPath string, issues []*types.Issue) ([]string, error) {
// Sort issues by ID for consistent output // Sort issues by ID for consistent output
sort.Slice(issues, func(i, j int) bool { slices.SortFunc(issues, func(a, b *types.Issue) int {
return issues[i].ID < issues[j].ID return cmp.Compare(a.ID, b.ID)
}) })
// Create temp file with PID suffix to avoid collisions (bd-306) // Create temp file with PID suffix to avoid collisions (bd-306)
+9 -5
View File
@@ -1,10 +1,11 @@
package main package main
import ( import (
"cmp"
"encoding/json" "encoding/json"
"fmt" "fmt"
"os" "os"
"sort" "slices"
"strings" "strings"
"github.com/spf13/cobra" "github.com/spf13/cobra"
@@ -205,8 +206,11 @@ Examples:
outputJSON(result) outputJSON(result)
} else { } else {
// Sort groups for consistent output // Sort groups for consistent output
sort.Slice(result.Groups, func(i, j int) bool { slices.SortFunc(result.Groups, func(a, b struct {
return result.Groups[i].Group < result.Groups[j].Group Group string `json:"group"`
Count int `json:"count"`
}) int {
return cmp.Compare(a.Group, b.Group)
}) })
fmt.Printf("Total: %d\n\n", result.Total) fmt.Printf("Total: %d\n\n", result.Total)
@@ -397,8 +401,8 @@ Examples:
} }
// Sort for consistent output // Sort for consistent output
sort.Slice(groups, func(i, j int) bool { slices.SortFunc(groups, func(a, b GroupCount) int {
return groups[i].Group < groups[j].Group return cmp.Compare(a.Group, b.Group)
}) })
if jsonOutput { if jsonOutput {
+4 -3
View File
@@ -2,12 +2,13 @@ package main
import ( import (
"bufio" "bufio"
"cmp"
"context" "context"
"encoding/json" "encoding/json"
"fmt" "fmt"
"os" "os"
"path/filepath" "path/filepath"
"sort" "slices"
"strings" "strings"
"time" "time"
@@ -59,8 +60,8 @@ func exportToJSONLWithStore(ctx context.Context, store storage.Storage, jsonlPat
} }
// Sort by ID for consistent output // Sort by ID for consistent output
sort.Slice(issues, func(i, j int) bool { slices.SortFunc(issues, func(a, b *types.Issue) int {
return issues[i].ID < issues[j].ID return cmp.Compare(a.ID, b.ID)
}) })
// Populate dependencies for all issues // Populate dependencies for all issues
+7 -7
View File
@@ -7,7 +7,7 @@ import (
"fmt" "fmt"
"os" "os"
"path/filepath" "path/filepath"
"sort" "slices"
"strings" "strings"
"time" "time"
@@ -895,15 +895,15 @@ func printDiagnostics(result doctorResult) {
fmt.Println(ui.RenderWarn(ui.IconWarn + " WARNINGS")) fmt.Println(ui.RenderWarn(ui.IconWarn + " WARNINGS"))
// Sort by severity: errors first, then warnings // Sort by severity: errors first, then warnings
sort.Slice(warnings, func(i, j int) bool { slices.SortStableFunc(warnings, func(a, b doctorCheck) int {
// Errors (statusError) come before warnings (statusWarning) // Errors (statusError) come before warnings (statusWarning)
if warnings[i].Status == statusError && warnings[j].Status != statusError { if a.Status == statusError && b.Status != statusError {
return true return -1
} }
if warnings[i].Status != statusError && warnings[j].Status == statusError { if a.Status != statusError && b.Status == statusError {
return false return 1
} }
return false // maintain original order within same severity return 0 // maintain original order within same severity
}) })
for i, check := range warnings { for i, check := range warnings {
+5 -4
View File
@@ -1,13 +1,14 @@
package main package main
import ( import (
"cmp"
"crypto/sha256" "crypto/sha256"
"encoding/hex" "encoding/hex"
"encoding/json" "encoding/json"
"fmt" "fmt"
"os" "os"
"path/filepath" "path/filepath"
"sort" "slices"
"strings" "strings"
"github.com/spf13/cobra" "github.com/spf13/cobra"
@@ -316,7 +317,7 @@ Examples:
len(jsonlIDs), len(issues), len(missingIDs)) len(jsonlIDs), len(issues), len(missingIDs))
if len(missingIDs) > 0 { if len(missingIDs) > 0 {
sort.Strings(missingIDs) slices.Sort(missingIDs)
fmt.Fprintf(os.Stderr, "Error: refusing to export stale database that would lose issues\n") fmt.Fprintf(os.Stderr, "Error: refusing to export stale database that would lose issues\n")
fmt.Fprintf(os.Stderr, " Database has %d issues\n", len(issues)) fmt.Fprintf(os.Stderr, " Database has %d issues\n", len(issues))
fmt.Fprintf(os.Stderr, " JSONL has %d issues\n", len(jsonlIDs)) fmt.Fprintf(os.Stderr, " JSONL has %d issues\n", len(jsonlIDs))
@@ -357,8 +358,8 @@ Examples:
issues = filtered issues = filtered
// Sort by ID for consistent output // Sort by ID for consistent output
sort.Slice(issues, func(i, j int) bool { slices.SortFunc(issues, func(a, b *types.Issue) int {
return issues[i].ID < issues[j].ID return cmp.Compare(a.ID, b.ID)
}) })
// Populate dependencies for all issues in one query (avoids N+1 problem) // Populate dependencies for all issues in one query (avoids N+1 problem)
+4 -3
View File
@@ -3,12 +3,13 @@ package main
import ( import (
"bufio" "bufio"
"bytes" "bytes"
"cmp"
"encoding/json" "encoding/json"
"fmt" "fmt"
"os" "os"
"os/exec" "os/exec"
"path/filepath" "path/filepath"
"sort" "slices"
"strings" "strings"
"time" "time"
@@ -359,8 +360,8 @@ NOTE: Import requires direct database access and does not work with daemon mode.
for oldID, newID := range result.IDMapping { for oldID, newID := range result.IDMapping {
mappings = append(mappings, mapping{oldID, newID}) mappings = append(mappings, mapping{oldID, newID})
} }
sort.Slice(mappings, func(i, j int) bool { slices.SortFunc(mappings, func(a, b mapping) int {
return mappings[i].oldID < mappings[j].oldID return cmp.Compare(a.oldID, b.oldID)
}) })
fmt.Fprintf(os.Stderr, "Remappings:\n") fmt.Fprintf(os.Stderr, "Remappings:\n")
+4 -3
View File
@@ -2,6 +2,7 @@ package main
import ( import (
"bytes" "bytes"
"cmp"
"context" "context"
"crypto/sha256" "crypto/sha256"
"database/sql" "database/sql"
@@ -10,7 +11,7 @@ import (
"fmt" "fmt"
"os" "os"
"path/filepath" "path/filepath"
"sort" "slices"
"strings" "strings"
"github.com/steveyegge/beads/internal/storage" "github.com/steveyegge/beads/internal/storage"
@@ -431,8 +432,8 @@ func computeDBHash(ctx context.Context, store storage.Storage) (string, error) {
} }
// Sort by ID for consistent hash // Sort by ID for consistent hash
sort.Slice(issues, func(i, j int) bool { slices.SortFunc(issues, func(a, b *types.Issue) int {
return issues[i].ID < issues[j].ID return cmp.Compare(a.ID, b.ID)
}) })
// Populate dependencies // Populate dependencies
+4 -3
View File
@@ -2,13 +2,14 @@ package main
import ( import (
"bufio" "bufio"
"cmp"
"context" "context"
"encoding/json" "encoding/json"
"fmt" "fmt"
"os" "os"
"os/exec" "os/exec"
"path/filepath" "path/filepath"
"sort" "slices"
"strings" "strings"
"time" "time"
@@ -479,8 +480,8 @@ func doPushToJira(ctx context.Context, dryRun bool, createOnly bool, updateRefs
} }
// Sort by ID for consistent output // Sort by ID for consistent output
sort.Slice(issues, func(i, j int) bool { slices.SortFunc(issues, func(a, b *types.Issue) int {
return issues[i].ID < issues[j].ID return cmp.Compare(a.ID, b.ID)
}) })
// Generate JSONL for export // Generate JSONL for export
+22 -21
View File
@@ -2,11 +2,12 @@ package main
import ( import (
"bytes" "bytes"
"cmp"
"context" "context"
"encoding/json" "encoding/json"
"fmt" "fmt"
"os" "os"
"sort" "slices"
"strings" "strings"
"text/template" "text/template"
"time" "time"
@@ -53,50 +54,50 @@ func sortIssues(issues []*types.Issue, sortBy string, reverse bool) {
return return
} }
sort.Slice(issues, func(i, j int) bool { slices.SortFunc(issues, func(a, b *types.Issue) int {
var less bool var result int
switch sortBy { switch sortBy {
case "priority": case "priority":
// Lower priority numbers come first (P0 > P1 > P2 > P3 > P4) // Lower priority numbers come first (P0 > P1 > P2 > P3 > P4)
less = issues[i].Priority < issues[j].Priority result = cmp.Compare(a.Priority, b.Priority)
case "created": case "created":
// Default: newest first (descending) // Default: newest first (descending)
less = issues[i].CreatedAt.After(issues[j].CreatedAt) result = b.CreatedAt.Compare(a.CreatedAt)
case "updated": case "updated":
// Default: newest first (descending) // Default: newest first (descending)
less = issues[i].UpdatedAt.After(issues[j].UpdatedAt) result = b.UpdatedAt.Compare(a.UpdatedAt)
case "closed": case "closed":
// Default: newest first (descending) // Default: newest first (descending)
// Handle nil ClosedAt values // Handle nil ClosedAt values
if issues[i].ClosedAt == nil && issues[j].ClosedAt == nil { if a.ClosedAt == nil && b.ClosedAt == nil {
less = false result = 0
} else if issues[i].ClosedAt == nil { } else if a.ClosedAt == nil {
less = false // nil sorts last result = 1 // nil sorts last
} else if issues[j].ClosedAt == nil { } else if b.ClosedAt == nil {
less = true // non-nil sorts before nil result = -1 // non-nil sorts before nil
} else { } else {
less = issues[i].ClosedAt.After(*issues[j].ClosedAt) result = b.ClosedAt.Compare(*a.ClosedAt)
} }
case "status": case "status":
less = issues[i].Status < issues[j].Status result = cmp.Compare(a.Status, b.Status)
case "id": case "id":
less = issues[i].ID < issues[j].ID result = cmp.Compare(a.ID, b.ID)
case "title": case "title":
less = strings.ToLower(issues[i].Title) < strings.ToLower(issues[j].Title) result = cmp.Compare(strings.ToLower(a.Title), strings.ToLower(b.Title))
case "type": case "type":
less = issues[i].IssueType < issues[j].IssueType result = cmp.Compare(a.IssueType, b.IssueType)
case "assignee": case "assignee":
less = issues[i].Assignee < issues[j].Assignee result = cmp.Compare(a.Assignee, b.Assignee)
default: default:
// Unknown sort field, no sorting // Unknown sort field, no sorting
less = false result = 0
} }
if reverse { if reverse {
return !less return -result
} }
return less return result
}) })
} }
+6 -5
View File
@@ -1,6 +1,7 @@
package main package main
import ( import (
"cmp"
"context" "context"
"crypto/sha256" "crypto/sha256"
"encoding/hex" "encoding/hex"
@@ -9,7 +10,7 @@ import (
"os" "os"
"path/filepath" "path/filepath"
"regexp" "regexp"
"sort" "slices"
"strings" "strings"
"time" "time"
@@ -272,8 +273,8 @@ func migrateToHashIDs(ctx context.Context, store *sqlite.SQLiteStorage, issues [
// We need to also update text references in descriptions, notes, design, acceptance criteria // We need to also update text references in descriptions, notes, design, acceptance criteria
// Sort issues by ID to process parents before children // Sort issues by ID to process parents before children
sort.Slice(issues, func(i, j int) bool { slices.SortFunc(issues, func(a, b *types.Issue) int {
return issues[i].ID < issues[j].ID return cmp.Compare(a.ID, b.ID)
}) })
// Update all issues // Update all issues
@@ -394,8 +395,8 @@ func saveMappingFile(path string, mapping map[string]string) error {
} }
// Sort by old ID for readability // Sort by old ID for readability
sort.Slice(entries, func(i, j int) bool { slices.SortFunc(entries, func(a, b mappingEntry) int {
return entries[i].OldID < entries[j].OldID return cmp.Compare(a.OldID, b.OldID)
}) })
data, err := json.MarshalIndent(map[string]interface{}{ data, err := json.MarshalIndent(map[string]interface{}{
+7 -6
View File
@@ -1,13 +1,14 @@
package main package main
import ( import (
"cmp"
"context" "context"
"database/sql" "database/sql"
"encoding/json" "encoding/json"
"fmt" "fmt"
"os" "os"
"regexp" "regexp"
"sort" "slices"
"strings" "strings"
"time" "time"
@@ -247,11 +248,11 @@ func repairPrefixes(ctx context.Context, st storage.Storage, actorName string, t
} }
// Sort incorrect issues: first by prefix lexicographically, then by number // Sort incorrect issues: first by prefix lexicographically, then by number
sort.Slice(incorrectIssues, func(i, j int) bool { slices.SortFunc(incorrectIssues, func(a, b issueSort) int {
if incorrectIssues[i].prefix != incorrectIssues[j].prefix { return cmp.Or(
return incorrectIssues[i].prefix < incorrectIssues[j].prefix cmp.Compare(a.prefix, b.prefix),
} cmp.Compare(a.number, b.number),
return incorrectIssues[i].number < incorrectIssues[j].number )
}) })
// Get a database connection for ID generation // Get a database connection for ID generation
+3 -3
View File
@@ -6,7 +6,7 @@ import (
"fmt" "fmt"
"os" "os"
"os/exec" "os/exec"
"sort" "slices"
"strings" "strings"
"github.com/spf13/cobra" "github.com/spf13/cobra"
@@ -1204,8 +1204,8 @@ func showMessageThread(ctx context.Context, messageID string, jsonOutput bool) {
} }
// Sort by creation time // Sort by creation time
sort.Slice(threadMessages, func(i, j int) bool { slices.SortFunc(threadMessages, func(a, b *types.Issue) int {
return threadMessages[i].CreatedAt.Before(threadMessages[j].CreatedAt) return a.CreatedAt.Compare(b.CreatedAt)
}) })
if jsonOutput { if jsonOutput {
+4 -3
View File
@@ -3,13 +3,14 @@ package main
import ( import (
"bufio" "bufio"
"bytes" "bytes"
"cmp"
"context" "context"
"encoding/json" "encoding/json"
"fmt" "fmt"
"os" "os"
"os/exec" "os/exec"
"path/filepath" "path/filepath"
"sort" "slices"
"strings" "strings"
"time" "time"
@@ -1346,8 +1347,8 @@ func exportToJSONL(ctx context.Context, jsonlPath string) error {
} }
// Sort by ID for consistent output // Sort by ID for consistent output
sort.Slice(issues, func(i, j int) bool { slices.SortFunc(issues, func(a, b *types.Issue) int {
return issues[i].ID < issues[j].ID return cmp.Compare(a.ID, b.ID)
}) })
// Populate dependencies for all issues (avoid N+1) // Populate dependencies for all issues (avoid N+1)
+4 -3
View File
@@ -1,8 +1,9 @@
package main package main
import ( import (
"cmp"
"fmt" "fmt"
"sort" "slices"
"github.com/charmbracelet/lipgloss" "github.com/charmbracelet/lipgloss"
"github.com/spf13/cobra" "github.com/spf13/cobra"
@@ -130,8 +131,8 @@ func getContributorsSorted() []string {
for name, commits := range beadsContributors { for name, commits := range beadsContributors {
sorted = append(sorted, kv{name, commits}) sorted = append(sorted, kv{name, commits})
} }
sort.Slice(sorted, func(i, j int) bool { slices.SortFunc(sorted, func(a, b kv) int {
return sorted[i].commits > sorted[j].commits return cmp.Compare(b.commits, a.commits) // descending order
}) })
names := make([]string, len(sorted)) names := make([]string, len(sorted))
for i, kv := range sorted { for i, kv := range sorted {
+4 -3
View File
@@ -1,13 +1,14 @@
package main package main
import ( import (
"cmp"
"context" "context"
"encoding/json" "encoding/json"
"fmt" "fmt"
"math/rand" "math/rand"
"os" "os"
"path/filepath" "path/filepath"
"sort" "slices"
"strconv" "strconv"
"strings" "strings"
"sync" "sync"
@@ -116,8 +117,8 @@ func selectNextTip(store storage.Storage) *Tip {
} }
// Sort by priority (highest first) // Sort by priority (highest first)
sort.Slice(eligibleTips, func(i, j int) bool { slices.SortFunc(eligibleTips, func(a, b Tip) int {
return eligibleTips[i].Priority > eligibleTips[j].Priority return cmp.Compare(b.Priority, a.Priority) // descending order
}) })
// Apply probability roll (in priority order) // Apply probability roll (in priority order)
+3 -3
View File
@@ -4,7 +4,7 @@ import (
"context" "context"
"fmt" "fmt"
"os" "os"
"sort" "slices"
"strings" "strings"
"time" "time"
@@ -397,8 +397,8 @@ func runWispList(cmd *cobra.Command, args []string) {
} }
// Sort by updated_at descending (most recent first) // Sort by updated_at descending (most recent first)
sort.Slice(items, func(i, j int) bool { slices.SortFunc(items, func(a, b WispListItem) int {
return items[i].UpdatedAt.After(items[j].UpdatedAt) return b.UpdatedAt.Compare(a.UpdatedAt) // descending order
}) })
result := WispListResult{ result := WispListResult{