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

View File

@@ -3,6 +3,7 @@ package main
import (
"bufio"
"bytes"
"cmp"
"context"
"crypto/sha256"
"encoding/hex"
@@ -10,7 +11,7 @@ import (
"fmt"
"os"
"path/filepath"
"sort"
"slices"
"strings"
"time"
@@ -218,8 +219,8 @@ func autoImportIfNewer() {
for oldID, newID := range result.IDMapping {
mappings = append(mappings, mapping{oldID, newID})
}
sort.Slice(mappings, func(i, j int) bool {
return mappings[i].oldID < mappings[j].oldID
slices.SortFunc(mappings, func(a, b mapping) int {
return cmp.Compare(a.oldID, b.oldID)
})
maxShow := 10
@@ -442,8 +443,8 @@ func validateJSONLIntegrity(ctx context.Context, jsonlPath string) (bool, error)
func writeJSONLAtomic(jsonlPath string, issues []*types.Issue) ([]string, error) {
// Sort issues by ID for consistent output
sort.Slice(issues, func(i, j int) bool {
return issues[i].ID < issues[j].ID
slices.SortFunc(issues, func(a, b *types.Issue) int {
return cmp.Compare(a.ID, b.ID)
})
// Create temp file with PID suffix to avoid collisions (bd-306)

View File

@@ -1,10 +1,11 @@
package main
import (
"cmp"
"encoding/json"
"fmt"
"os"
"sort"
"slices"
"strings"
"github.com/spf13/cobra"
@@ -205,8 +206,11 @@ Examples:
outputJSON(result)
} else {
// Sort groups for consistent output
sort.Slice(result.Groups, func(i, j int) bool {
return result.Groups[i].Group < result.Groups[j].Group
slices.SortFunc(result.Groups, func(a, b struct {
Group string `json:"group"`
Count int `json:"count"`
}) int {
return cmp.Compare(a.Group, b.Group)
})
fmt.Printf("Total: %d\n\n", result.Total)
@@ -397,8 +401,8 @@ Examples:
}
// Sort for consistent output
sort.Slice(groups, func(i, j int) bool {
return groups[i].Group < groups[j].Group
slices.SortFunc(groups, func(a, b GroupCount) int {
return cmp.Compare(a.Group, b.Group)
})
if jsonOutput {

View File

@@ -2,12 +2,13 @@ package main
import (
"bufio"
"cmp"
"context"
"encoding/json"
"fmt"
"os"
"path/filepath"
"sort"
"slices"
"strings"
"time"
@@ -59,8 +60,8 @@ func exportToJSONLWithStore(ctx context.Context, store storage.Storage, jsonlPat
}
// Sort by ID for consistent output
sort.Slice(issues, func(i, j int) bool {
return issues[i].ID < issues[j].ID
slices.SortFunc(issues, func(a, b *types.Issue) int {
return cmp.Compare(a.ID, b.ID)
})
// Populate dependencies for all issues

View File

@@ -7,7 +7,7 @@ import (
"fmt"
"os"
"path/filepath"
"sort"
"slices"
"strings"
"time"
@@ -895,15 +895,15 @@ func printDiagnostics(result doctorResult) {
fmt.Println(ui.RenderWarn(ui.IconWarn + " 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)
if warnings[i].Status == statusError && warnings[j].Status != statusError {
return true
if a.Status == statusError && b.Status != statusError {
return -1
}
if warnings[i].Status != statusError && warnings[j].Status == statusError {
return false
if a.Status != statusError && b.Status == statusError {
return 1
}
return false // maintain original order within same severity
return 0 // maintain original order within same severity
})
for i, check := range warnings {

View File

@@ -1,13 +1,14 @@
package main
import (
"cmp"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"fmt"
"os"
"path/filepath"
"sort"
"slices"
"strings"
"github.com/spf13/cobra"
@@ -316,7 +317,7 @@ Examples:
len(jsonlIDs), len(issues), len(missingIDs))
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, " Database has %d issues\n", len(issues))
fmt.Fprintf(os.Stderr, " JSONL has %d issues\n", len(jsonlIDs))
@@ -357,8 +358,8 @@ Examples:
issues = filtered
// Sort by ID for consistent output
sort.Slice(issues, func(i, j int) bool {
return issues[i].ID < issues[j].ID
slices.SortFunc(issues, func(a, b *types.Issue) int {
return cmp.Compare(a.ID, b.ID)
})
// Populate dependencies for all issues in one query (avoids N+1 problem)

View File

@@ -3,12 +3,13 @@ package main
import (
"bufio"
"bytes"
"cmp"
"encoding/json"
"fmt"
"os"
"os/exec"
"path/filepath"
"sort"
"slices"
"strings"
"time"
@@ -359,8 +360,8 @@ NOTE: Import requires direct database access and does not work with daemon mode.
for oldID, newID := range result.IDMapping {
mappings = append(mappings, mapping{oldID, newID})
}
sort.Slice(mappings, func(i, j int) bool {
return mappings[i].oldID < mappings[j].oldID
slices.SortFunc(mappings, func(a, b mapping) int {
return cmp.Compare(a.oldID, b.oldID)
})
fmt.Fprintf(os.Stderr, "Remappings:\n")

View File

@@ -2,6 +2,7 @@ package main
import (
"bytes"
"cmp"
"context"
"crypto/sha256"
"database/sql"
@@ -10,7 +11,7 @@ import (
"fmt"
"os"
"path/filepath"
"sort"
"slices"
"strings"
"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.Slice(issues, func(i, j int) bool {
return issues[i].ID < issues[j].ID
slices.SortFunc(issues, func(a, b *types.Issue) int {
return cmp.Compare(a.ID, b.ID)
})
// Populate dependencies

View File

@@ -2,13 +2,14 @@ package main
import (
"bufio"
"cmp"
"context"
"encoding/json"
"fmt"
"os"
"os/exec"
"path/filepath"
"sort"
"slices"
"strings"
"time"
@@ -479,8 +480,8 @@ func doPushToJira(ctx context.Context, dryRun bool, createOnly bool, updateRefs
}
// Sort by ID for consistent output
sort.Slice(issues, func(i, j int) bool {
return issues[i].ID < issues[j].ID
slices.SortFunc(issues, func(a, b *types.Issue) int {
return cmp.Compare(a.ID, b.ID)
})
// Generate JSONL for export

View File

@@ -2,11 +2,12 @@ package main
import (
"bytes"
"cmp"
"context"
"encoding/json"
"fmt"
"os"
"sort"
"slices"
"strings"
"text/template"
"time"
@@ -53,50 +54,50 @@ func sortIssues(issues []*types.Issue, sortBy string, reverse bool) {
return
}
sort.Slice(issues, func(i, j int) bool {
var less bool
slices.SortFunc(issues, func(a, b *types.Issue) int {
var result int
switch sortBy {
case "priority":
// 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":
// Default: newest first (descending)
less = issues[i].CreatedAt.After(issues[j].CreatedAt)
result = b.CreatedAt.Compare(a.CreatedAt)
case "updated":
// Default: newest first (descending)
less = issues[i].UpdatedAt.After(issues[j].UpdatedAt)
result = b.UpdatedAt.Compare(a.UpdatedAt)
case "closed":
// Default: newest first (descending)
// Handle nil ClosedAt values
if issues[i].ClosedAt == nil && issues[j].ClosedAt == nil {
less = false
} else if issues[i].ClosedAt == nil {
less = false // nil sorts last
} else if issues[j].ClosedAt == nil {
less = true // non-nil sorts before nil
if a.ClosedAt == nil && b.ClosedAt == nil {
result = 0
} else if a.ClosedAt == nil {
result = 1 // nil sorts last
} else if b.ClosedAt == nil {
result = -1 // non-nil sorts before nil
} else {
less = issues[i].ClosedAt.After(*issues[j].ClosedAt)
result = b.ClosedAt.Compare(*a.ClosedAt)
}
case "status":
less = issues[i].Status < issues[j].Status
result = cmp.Compare(a.Status, b.Status)
case "id":
less = issues[i].ID < issues[j].ID
result = cmp.Compare(a.ID, b.ID)
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":
less = issues[i].IssueType < issues[j].IssueType
result = cmp.Compare(a.IssueType, b.IssueType)
case "assignee":
less = issues[i].Assignee < issues[j].Assignee
result = cmp.Compare(a.Assignee, b.Assignee)
default:
// Unknown sort field, no sorting
less = false
result = 0
}
if reverse {
return !less
return -result
}
return less
return result
})
}

View File

@@ -1,6 +1,7 @@
package main
import (
"cmp"
"context"
"crypto/sha256"
"encoding/hex"
@@ -9,7 +10,7 @@ import (
"os"
"path/filepath"
"regexp"
"sort"
"slices"
"strings"
"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
// Sort issues by ID to process parents before children
sort.Slice(issues, func(i, j int) bool {
return issues[i].ID < issues[j].ID
slices.SortFunc(issues, func(a, b *types.Issue) int {
return cmp.Compare(a.ID, b.ID)
})
// Update all issues
@@ -394,8 +395,8 @@ func saveMappingFile(path string, mapping map[string]string) error {
}
// Sort by old ID for readability
sort.Slice(entries, func(i, j int) bool {
return entries[i].OldID < entries[j].OldID
slices.SortFunc(entries, func(a, b mappingEntry) int {
return cmp.Compare(a.OldID, b.OldID)
})
data, err := json.MarshalIndent(map[string]interface{}{

View File

@@ -1,13 +1,14 @@
package main
import (
"cmp"
"context"
"database/sql"
"encoding/json"
"fmt"
"os"
"regexp"
"sort"
"slices"
"strings"
"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.Slice(incorrectIssues, func(i, j int) bool {
if incorrectIssues[i].prefix != incorrectIssues[j].prefix {
return incorrectIssues[i].prefix < incorrectIssues[j].prefix
}
return incorrectIssues[i].number < incorrectIssues[j].number
slices.SortFunc(incorrectIssues, func(a, b issueSort) int {
return cmp.Or(
cmp.Compare(a.prefix, b.prefix),
cmp.Compare(a.number, b.number),
)
})
// Get a database connection for ID generation

View File

@@ -6,7 +6,7 @@ import (
"fmt"
"os"
"os/exec"
"sort"
"slices"
"strings"
"github.com/spf13/cobra"
@@ -1204,8 +1204,8 @@ func showMessageThread(ctx context.Context, messageID string, jsonOutput bool) {
}
// Sort by creation time
sort.Slice(threadMessages, func(i, j int) bool {
return threadMessages[i].CreatedAt.Before(threadMessages[j].CreatedAt)
slices.SortFunc(threadMessages, func(a, b *types.Issue) int {
return a.CreatedAt.Compare(b.CreatedAt)
})
if jsonOutput {

View File

@@ -3,13 +3,14 @@ package main
import (
"bufio"
"bytes"
"cmp"
"context"
"encoding/json"
"fmt"
"os"
"os/exec"
"path/filepath"
"sort"
"slices"
"strings"
"time"
@@ -1346,8 +1347,8 @@ func exportToJSONL(ctx context.Context, jsonlPath string) error {
}
// Sort by ID for consistent output
sort.Slice(issues, func(i, j int) bool {
return issues[i].ID < issues[j].ID
slices.SortFunc(issues, func(a, b *types.Issue) int {
return cmp.Compare(a.ID, b.ID)
})
// Populate dependencies for all issues (avoid N+1)

View File

@@ -1,8 +1,9 @@
package main
import (
"cmp"
"fmt"
"sort"
"slices"
"github.com/charmbracelet/lipgloss"
"github.com/spf13/cobra"
@@ -130,8 +131,8 @@ func getContributorsSorted() []string {
for name, commits := range beadsContributors {
sorted = append(sorted, kv{name, commits})
}
sort.Slice(sorted, func(i, j int) bool {
return sorted[i].commits > sorted[j].commits
slices.SortFunc(sorted, func(a, b kv) int {
return cmp.Compare(b.commits, a.commits) // descending order
})
names := make([]string, len(sorted))
for i, kv := range sorted {

View File

@@ -1,13 +1,14 @@
package main
import (
"cmp"
"context"
"encoding/json"
"fmt"
"math/rand"
"os"
"path/filepath"
"sort"
"slices"
"strconv"
"strings"
"sync"
@@ -116,8 +117,8 @@ func selectNextTip(store storage.Storage) *Tip {
}
// Sort by priority (highest first)
sort.Slice(eligibleTips, func(i, j int) bool {
return eligibleTips[i].Priority > eligibleTips[j].Priority
slices.SortFunc(eligibleTips, func(a, b Tip) int {
return cmp.Compare(b.Priority, a.Priority) // descending order
})
// Apply probability roll (in priority order)

View File

@@ -4,7 +4,7 @@ import (
"context"
"fmt"
"os"
"sort"
"slices"
"strings"
"time"
@@ -397,8 +397,8 @@ func runWispList(cmd *cobra.Command, args []string) {
}
// Sort by updated_at descending (most recent first)
sort.Slice(items, func(i, j int) bool {
return items[i].UpdatedAt.After(items[j].UpdatedAt)
slices.SortFunc(items, func(a, b WispListItem) int {
return b.UpdatedAt.Compare(a.UpdatedAt) // descending order
})
result := WispListResult{