/{cmd,internal}: fix lint issues

This commit is contained in:
coffeegoddd☕️✨
2026-01-20 13:46:57 -08:00
parent 422bc838ed
commit a097fc546b
6 changed files with 47 additions and 83 deletions

View File

@@ -429,13 +429,14 @@ func hookPreCommitDolt(beadsDir, worktreeRoot string) int {
fmt.Fprintf(os.Stderr, "Warning: could not open database: %v\n", err) fmt.Fprintf(os.Stderr, "Warning: could not open database: %v\n", err)
return 0 return 0
} }
defer store.Close() defer func() { _ = store.Close() }()
// Check if store supports versioned operations (required for Dolt) // Check if store supports versioned operations (required for Dolt)
vs, ok := storage.AsVersioned(store) vs, ok := storage.AsVersioned(store)
if !ok { if !ok {
// Fall back to full export if not versioned // Fall back to full export if not versioned
return doExportAndSaveState(ctx, beadsDir, worktreeRoot, "") doExportAndSaveState(ctx, beadsDir, worktreeRoot, "")
return 0
} }
// Get current Dolt commit hash // Get current Dolt commit hash
@@ -443,7 +444,8 @@ func hookPreCommitDolt(beadsDir, worktreeRoot string) int {
if err != nil { if err != nil {
fmt.Fprintf(os.Stderr, "Warning: could not get Dolt commit: %v\n", err) fmt.Fprintf(os.Stderr, "Warning: could not get Dolt commit: %v\n", err)
// Fall back to full export without commit tracking // Fall back to full export without commit tracking
return doExportAndSaveState(ctx, beadsDir, worktreeRoot, "") doExportAndSaveState(ctx, beadsDir, worktreeRoot, "")
return 0
} }
// Check if we've already exported for this Dolt commit (idempotency) // Check if we've already exported for this Dolt commit (idempotency)
@@ -465,17 +467,18 @@ func hookPreCommitDolt(beadsDir, worktreeRoot string) int {
} }
} }
return doExportAndSaveState(ctx, beadsDir, worktreeRoot, currentDoltCommit) doExportAndSaveState(ctx, beadsDir, worktreeRoot, currentDoltCommit)
return 0
} }
// doExportAndSaveState performs the export and saves state. Shared by main path and fallback. // doExportAndSaveState performs the export and saves state. Shared by main path and fallback.
func doExportAndSaveState(ctx context.Context, beadsDir, worktreeRoot, doltCommit string) int { func doExportAndSaveState(ctx context.Context, beadsDir, worktreeRoot, doltCommit string) {
jsonlPath := filepath.Join(beadsDir, "issues.jsonl") jsonlPath := filepath.Join(beadsDir, "issues.jsonl")
// Export to JSONL // Export to JSONL
if err := runJSONLExport(); err != nil { if err := runJSONLExport(); err != nil {
fmt.Fprintf(os.Stderr, "Warning: could not export to JSONL: %v\n", err) fmt.Fprintf(os.Stderr, "Warning: could not export to JSONL: %v\n", err)
return 0 return
} }
// Stage JSONL files for git commit // Stage JSONL files for git commit
@@ -493,8 +496,6 @@ func doExportAndSaveState(ctx context.Context, beadsDir, worktreeRoot, doltCommi
if err := saveExportState(beadsDir, worktreeRoot, state); err != nil { if err := saveExportState(beadsDir, worktreeRoot, state); err != nil {
fmt.Fprintf(os.Stderr, "Warning: could not save export state: %v\n", err) fmt.Fprintf(os.Stderr, "Warning: could not save export state: %v\n", err)
} }
return 0
} }
// hasDoltChanges checks if there are any changes between two Dolt commits. // hasDoltChanges checks if there are any changes between two Dolt commits.
@@ -622,7 +623,7 @@ func hookPostMergeDolt(beadsDir string) int {
fmt.Fprintf(os.Stderr, "Warning: could not open database: %v\n", err) fmt.Fprintf(os.Stderr, "Warning: could not open database: %v\n", err)
return 0 return 0
} }
defer store.Close() defer func() { _ = store.Close() }()
// Check if Dolt store supports version control operations // Check if Dolt store supports version control operations
doltStore, ok := store.(interface { doltStore, ok := store.(interface {
@@ -662,7 +663,7 @@ func hookPostMergeDolt(beadsDir string) int {
// Import JSONL to the import branch // Import JSONL to the import branch
jsonlPath := filepath.Join(beadsDir, "issues.jsonl") jsonlPath := filepath.Join(beadsDir, "issues.jsonl")
if err := importFromJSONLToStore(ctx, store, jsonlPath); err != nil { if err := importFromJSONLToStore(store, jsonlPath); err != nil {
fmt.Fprintf(os.Stderr, "Warning: could not import JSONL: %v\n", err) fmt.Fprintf(os.Stderr, "Warning: could not import JSONL: %v\n", err)
// Checkout back to original branch // Checkout back to original branch
_ = doltStore.Checkout(ctx, currentBranch) _ = doltStore.Checkout(ctx, currentBranch)
@@ -830,7 +831,9 @@ func hookPostCheckout(args []string) int {
// importFromJSONLToStore imports issues from JSONL to a store. // importFromJSONLToStore imports issues from JSONL to a store.
// This is a placeholder - the actual implementation should use the store's methods. // This is a placeholder - the actual implementation should use the store's methods.
func importFromJSONLToStore(ctx context.Context, store interface{}, jsonlPath string) error { func importFromJSONLToStore(store interface{}, jsonlPath string) error {
_ = store
_ = jsonlPath
// Use bd sync --import-only for now // Use bd sync --import-only for now
// TODO: Implement direct store import // TODO: Implement direct store import
cmd := exec.Command("bd", "sync", "--import-only", "--no-git-history", "--no-daemon") cmd := exec.Command("bd", "sync", "--import-only", "--no-git-history", "--no-daemon")

View File

@@ -913,6 +913,7 @@ type SyncConflictRecord struct {
// LoadSyncConflictState loads the sync conflict state from disk. // LoadSyncConflictState loads the sync conflict state from disk.
func LoadSyncConflictState(beadsDir string) (*SyncConflictState, error) { func LoadSyncConflictState(beadsDir string) (*SyncConflictState, error) {
path := filepath.Join(beadsDir, "sync_conflicts.json") path := filepath.Join(beadsDir, "sync_conflicts.json")
// #nosec G304 -- path is derived from the workspace .beads directory
data, err := os.ReadFile(path) data, err := os.ReadFile(path)
if err != nil { if err != nil {
if os.IsNotExist(err) { if os.IsNotExist(err) {
@@ -1012,7 +1013,7 @@ func resolveSyncConflicts(ctx context.Context, jsonlPath string, strategy config
// Handle manual strategy with interactive resolution // Handle manual strategy with interactive resolution
if strategy == config.ConflictStrategyManual { if strategy == config.ConflictStrategyManual {
return resolveSyncConflictsManually(ctx, jsonlPath, beadsDir, conflictState, baseMap, localMap, remoteMap, baseIssues, localIssues, remoteIssues) return resolveSyncConflictsManually(ctx, jsonlPath, beadsDir, conflictState, baseMap, localMap, remoteMap)
} }
resolved := 0 resolved := 0
@@ -1090,8 +1091,7 @@ func resolveSyncConflicts(ctx context.Context, jsonlPath string, strategy config
// resolveSyncConflictsManually handles manual conflict resolution with interactive prompts. // resolveSyncConflictsManually handles manual conflict resolution with interactive prompts.
func resolveSyncConflictsManually(ctx context.Context, jsonlPath, beadsDir string, conflictState *SyncConflictState, func resolveSyncConflictsManually(ctx context.Context, jsonlPath, beadsDir string, conflictState *SyncConflictState,
baseMap, localMap, remoteMap map[string]*beads.Issue, baseMap, localMap, remoteMap map[string]*beads.Issue) error {
baseIssues, localIssues, remoteIssues []*beads.Issue) error {
// Build interactive conflicts list // Build interactive conflicts list
var interactiveConflicts []InteractiveConflict var interactiveConflicts []InteractiveConflict

View File

@@ -160,15 +160,15 @@ func displayConflictDiff(conflict InteractiveConflict) {
// Description (show truncated if different) // Description (show truncated if different)
if local.Description != remote.Description { if local.Description != remote.Description {
fmt.Printf(" %s\n", ui.RenderAccent("description:")) fmt.Printf(" %s\n", ui.RenderAccent("description:"))
fmt.Printf(" %s %s\n", ui.RenderMuted("local:"), truncateText(local.Description, 60)) fmt.Printf(" %s %s\n", ui.RenderMuted("local:"), truncateText(local.Description))
fmt.Printf(" %s %s\n", ui.RenderAccent("remote:"), truncateText(remote.Description, 60)) fmt.Printf(" %s %s\n", ui.RenderAccent("remote:"), truncateText(remote.Description))
} }
// Notes (show truncated if different) // Notes (show truncated if different)
if local.Notes != remote.Notes { if local.Notes != remote.Notes {
fmt.Printf(" %s\n", ui.RenderAccent("notes:")) fmt.Printf(" %s\n", ui.RenderAccent("notes:"))
fmt.Printf(" %s %s\n", ui.RenderMuted("local:"), truncateText(local.Notes, 60)) fmt.Printf(" %s %s\n", ui.RenderMuted("local:"), truncateText(local.Notes))
fmt.Printf(" %s %s\n", ui.RenderAccent("remote:"), truncateText(remote.Notes, 60)) fmt.Printf(" %s %s\n", ui.RenderAccent("remote:"), truncateText(remote.Notes))
} }
// Labels // Labels
@@ -371,9 +371,11 @@ func valueOrNone(s string) string {
return s return s
} }
// truncateText truncates a string to maxLen runes (not bytes) for proper UTF-8 handling. const truncateTextMaxLen = 60
// truncateText truncates a string to a fixed max length (runes, not bytes) for proper UTF-8 handling.
// Replaces newlines with spaces for single-line display. // Replaces newlines with spaces for single-line display.
func truncateText(s string, maxLen int) string { func truncateText(s string) string {
if s == "" { if s == "" {
return "(empty)" return "(empty)"
} }
@@ -383,14 +385,14 @@ func truncateText(s string, maxLen int) string {
// Count runes, not bytes, for proper UTF-8 handling // Count runes, not bytes, for proper UTF-8 handling
runeCount := utf8.RuneCountInString(s) runeCount := utf8.RuneCountInString(s)
if runeCount <= maxLen { if runeCount <= truncateTextMaxLen {
return s return s
} }
// Truncate by runes // Truncate by runes
runes := []rune(s) runes := []rune(s)
if maxLen <= 3 { if truncateTextMaxLen <= 3 {
return "..." return "..."
} }
return string(runes[:maxLen-3]) + "..." return string(runes[:truncateTextMaxLen-3]) + "..."
} }

View File

@@ -10,78 +10,37 @@ import (
func TestTruncateText(t *testing.T) { func TestTruncateText(t *testing.T) {
tests := []struct { tests := []struct {
name string name string
input string input string
maxLen int want string
want string
}{ }{
{ {
name: "empty string", name: "empty string",
input: "", input: "",
maxLen: 10, want: "(empty)",
want: "(empty)",
}, },
{ {
name: "short string", name: "short string",
input: "hello", input: "hello",
maxLen: 10, want: "hello",
want: "hello",
}, },
{ {
name: "exact length", name: "newlines replaced",
input: "0123456789", input: "line1\nline2\r\nline3",
maxLen: 10, want: "line1 line2 line3",
want: "0123456789",
}, },
{ {
name: "truncated", name: "truncated at fixed max",
input: "this is a very long string", input: strings.Repeat("a", truncateTextMaxLen+10),
maxLen: 15, want: strings.Repeat("a", truncateTextMaxLen-3) + "...",
want: "this is a ve...",
},
{
name: "newlines replaced",
input: "line1\nline2\nline3",
maxLen: 30,
want: "line1 line2 line3",
},
{
name: "very short max",
input: "hello world",
maxLen: 3,
want: "...",
},
{
name: "UTF-8 characters preserved",
input: "Hello 世界This is a test",
maxLen: 12,
want: "Hello 世界!...",
},
{
name: "UTF-8 exact length",
input: "日本語テスト",
maxLen: 6,
want: "日本語テスト",
},
{
name: "UTF-8 truncate",
input: "日本語テストです",
maxLen: 6,
want: "日本語...",
},
{
name: "emoji handling",
input: "Hello 🌍🌎🌏 World",
maxLen: 12,
want: "Hello 🌍🌎🌏...",
}, },
} }
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
got := truncateText(tt.input, tt.maxLen) got := truncateText(tt.input)
if got != tt.want { if got != tt.want {
t.Errorf("truncateText(%q, %d) = %q, want %q", tt.input, tt.maxLen, got, tt.want) t.Errorf("truncateText(%q) = %q, want %q", tt.input, got, tt.want)
} }
}) })
} }

View File

@@ -21,7 +21,7 @@ var ConfigWarningWriter io.Writer = os.Stderr
// logConfigWarning logs a warning message if ConfigWarnings is enabled. // logConfigWarning logs a warning message if ConfigWarnings is enabled.
func logConfigWarning(format string, args ...interface{}) { func logConfigWarning(format string, args ...interface{}) {
if ConfigWarnings && ConfigWarningWriter != nil { if ConfigWarnings && ConfigWarningWriter != nil {
fmt.Fprintf(ConfigWarningWriter, format, args...) _, _ = fmt.Fprintf(ConfigWarningWriter, format, args...)
} }
} }

View File

@@ -140,7 +140,7 @@ func findJSONLPath(beadsDir string) string {
func acquireBootstrapLock(lockPath string, timeout time.Duration) (*os.File, error) { func acquireBootstrapLock(lockPath string, timeout time.Duration) (*os.File, error) {
// Create lock file // Create lock file
// #nosec G304 - controlled path // #nosec G304 - controlled path
f, err := os.OpenFile(lockPath, os.O_CREATE|os.O_RDWR, 0644) f, err := os.OpenFile(lockPath, os.O_CREATE|os.O_RDWR, 0600)
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to create lock file: %w", err) return nil, fmt.Errorf("failed to create lock file: %w", err)
} }