Files
beads/cmd/bd/doctor/multirepo.go
beads/crew/dave 40ae598751 fix(lint): resolve unparam warnings in doctor and rpc packages
- multirepo.go: discoverChildTypes now returns []string instead of
  ([]string, error) since error was always nil
- socket_path.go: tmpDir changed from function to const since it
  always returned "/tmp" regardless of platform

Fixes CI lint failures caused by unparam linter detecting unused
error returns and constant function results.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

Executed-By: beads/crew/dave
Rig: beads
Role: crew
2026-01-10 23:17:37 -08:00

262 lines
7.0 KiB
Go

package doctor
import (
"database/sql"
"fmt"
"os"
"path/filepath"
"strings"
"github.com/steveyegge/beads/internal/beads"
"github.com/steveyegge/beads/internal/config"
"github.com/steveyegge/beads/internal/configfile"
)
// CheckMultiRepoTypes discovers and reports custom types used by child repos in multi-repo setups.
// This is informational - the federation trust model means we don't require parent config to
// list child types, but it's useful to know what types each child uses.
func CheckMultiRepoTypes(repoPath string) DoctorCheck {
multiRepo := config.GetMultiRepoConfig()
if multiRepo == nil || len(multiRepo.Additional) == 0 {
return DoctorCheck{
Name: "Multi-Repo Types",
Status: StatusOK,
Message: "N/A (single-repo mode)",
Category: CategoryData,
}
}
var details []string
var warnings []string
// Discover types from each child repo
for _, repoPathStr := range multiRepo.Additional {
childTypes := discoverChildTypes(repoPathStr)
if len(childTypes) > 0 {
details = append(details, fmt.Sprintf(" %s: %s", repoPathStr, strings.Join(childTypes, ", ")))
} else {
details = append(details, fmt.Sprintf(" %s: (no custom types)", repoPathStr))
}
}
// Check for hydrated issues using types not found anywhere
unknownTypes := findUnknownTypesInHydratedIssues(repoPath, multiRepo)
if len(unknownTypes) > 0 {
warnings = append(warnings, fmt.Sprintf("Issues with unknown types: %s", strings.Join(unknownTypes, ", ")))
}
status := StatusOK
message := fmt.Sprintf("Discovered types from %d child repo(s)", len(multiRepo.Additional))
if len(warnings) > 0 {
status = StatusWarning
message = fmt.Sprintf("Found %d type warning(s)", len(warnings))
details = append(details, "")
details = append(details, "Warnings:")
details = append(details, warnings...)
}
return DoctorCheck{
Name: "Multi-Repo Types",
Status: status,
Message: message,
Detail: strings.Join(details, "\n"),
Category: CategoryData,
}
}
// discoverChildTypes reads custom types from a child repo's config or database.
// Returns nil if no custom types are found (not an error - child may not have any).
func discoverChildTypes(repoPath string) []string {
// Expand tilde
if strings.HasPrefix(repoPath, "~") {
if home, err := os.UserHomeDir(); err == nil {
repoPath = filepath.Join(home, repoPath[1:])
}
}
beadsDir := filepath.Join(repoPath, ".beads")
// First try reading from database config table
types, err := readTypesFromDB(beadsDir)
if err == nil && len(types) > 0 {
return types
}
// Fall back to reading from config.yaml
types, err = readTypesFromYAML(beadsDir)
if err == nil {
return types
}
// No custom types found
return nil
}
// readTypesFromDB reads types.custom from the database config table
func readTypesFromDB(beadsDir string) ([]string, error) {
// Get database path
var dbPath string
if cfg, err := configfile.Load(beadsDir); err == nil && cfg != nil && cfg.Database != "" {
dbPath = cfg.DatabasePath(beadsDir)
} else {
dbPath = filepath.Join(beadsDir, beads.CanonicalDatabaseName)
}
if _, err := os.Stat(dbPath); os.IsNotExist(err) {
return nil, err
}
db, err := sql.Open("sqlite3", sqliteConnString(dbPath, true))
if err != nil {
return nil, err
}
defer db.Close()
var typesStr string
err = db.QueryRow("SELECT value FROM config WHERE key = 'types.custom'").Scan(&typesStr)
if err != nil {
return nil, err
}
if typesStr == "" {
return nil, nil
}
// Parse comma-separated list
var types []string
for _, t := range strings.Split(typesStr, ",") {
t = strings.TrimSpace(t)
if t != "" {
types = append(types, t)
}
}
return types, nil
}
// readTypesFromYAML reads types.custom from config.yaml
func readTypesFromYAML(beadsDir string) ([]string, error) {
configPath := filepath.Join(beadsDir, "config.yaml")
if _, err := os.Stat(configPath); os.IsNotExist(err) {
return nil, err
}
// Use viper to read the config
// For simplicity, we'll parse it manually to avoid viper state issues
content, err := os.ReadFile(configPath) // #nosec G304 - path is controlled
if err != nil {
return nil, err
}
// Simple YAML parsing for types.custom
// Looking for "types:" section with "custom:" key
lines := strings.Split(string(content), "\n")
inTypes := false
for _, line := range lines {
trimmed := strings.TrimSpace(line)
if strings.HasPrefix(trimmed, "types:") {
inTypes = true
continue
}
if inTypes && strings.HasPrefix(trimmed, "custom:") {
// Parse the value
value := strings.TrimPrefix(trimmed, "custom:")
value = strings.TrimSpace(value)
// Handle array format [a, b, c] or string format "a,b,c"
value = strings.Trim(value, "[]\"'")
if value == "" {
return nil, nil
}
var types []string
for _, t := range strings.Split(value, ",") {
t = strings.TrimSpace(t)
t = strings.Trim(t, "\"'")
if t != "" {
types = append(types, t)
}
}
return types, nil
}
// Exit types section if we hit another top-level key
if inTypes && len(line) > 0 && line[0] != ' ' && line[0] != '\t' {
break
}
}
return nil, nil
}
// findUnknownTypesInHydratedIssues checks if any hydrated issues use types not found in any config
func findUnknownTypesInHydratedIssues(repoPath string, multiRepo *config.MultiRepoConfig) []string {
beadsDir := filepath.Join(repoPath, ".beads")
// Get database path
var dbPath string
if cfg, err := configfile.Load(beadsDir); err == nil && cfg != nil && cfg.Database != "" {
dbPath = cfg.DatabasePath(beadsDir)
} else {
dbPath = filepath.Join(beadsDir, beads.CanonicalDatabaseName)
}
if _, err := os.Stat(dbPath); os.IsNotExist(err) {
return nil
}
db, err := sql.Open("sqlite3", sqliteConnString(dbPath, true))
if err != nil {
return nil
}
defer db.Close()
// Collect all known types (built-in + parent custom + all child custom)
knownTypes := map[string]bool{
"bug": true, "feature": true, "task": true, "epic": true, "chore": true,
"message": true, "merge-request": true, "molecule": true, "gate": true, "event": true,
}
// Add parent's custom types
var parentTypes string
if err := db.QueryRow("SELECT value FROM config WHERE key = 'types.custom'").Scan(&parentTypes); err == nil {
for _, t := range strings.Split(parentTypes, ",") {
t = strings.TrimSpace(t)
if t != "" {
knownTypes[t] = true
}
}
}
// Add child types
for _, repoPathStr := range multiRepo.Additional {
childTypes := discoverChildTypes(repoPathStr)
for _, t := range childTypes {
knownTypes[t] = true
}
}
// Find issues with types not in knownTypes
rows, err := db.Query(`
SELECT DISTINCT issue_type FROM issues
WHERE status != 'tombstone' AND source_repo != '' AND source_repo != '.'
`)
if err != nil {
return nil
}
defer rows.Close()
var unknownTypes []string
seen := make(map[string]bool)
for rows.Next() {
var issueType string
if err := rows.Scan(&issueType); err != nil {
continue
}
if !knownTypes[issueType] && !seen[issueType] {
unknownTypes = append(unknownTypes, issueType)
seen[issueType] = true
}
}
return unknownTypes
}