diff --git a/cmd/bd/hook.go b/cmd/bd/hook.go index c7ee0210..a0e246d8 100644 --- a/cmd/bd/hook.go +++ b/cmd/bd/hook.go @@ -429,13 +429,14 @@ func hookPreCommitDolt(beadsDir, worktreeRoot string) int { fmt.Fprintf(os.Stderr, "Warning: could not open database: %v\n", err) return 0 } - defer store.Close() + defer func() { _ = store.Close() }() // Check if store supports versioned operations (required for Dolt) vs, ok := storage.AsVersioned(store) if !ok { // Fall back to full export if not versioned - return doExportAndSaveState(ctx, beadsDir, worktreeRoot, "") + doExportAndSaveState(ctx, beadsDir, worktreeRoot, "") + return 0 } // Get current Dolt commit hash @@ -443,7 +444,8 @@ func hookPreCommitDolt(beadsDir, worktreeRoot string) int { if err != nil { fmt.Fprintf(os.Stderr, "Warning: could not get Dolt commit: %v\n", err) // 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) @@ -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. -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") // Export to JSONL if err := runJSONLExport(); err != nil { fmt.Fprintf(os.Stderr, "Warning: could not export to JSONL: %v\n", err) - return 0 + return } // 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 { 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. @@ -622,7 +623,7 @@ func hookPostMergeDolt(beadsDir string) int { fmt.Fprintf(os.Stderr, "Warning: could not open database: %v\n", err) return 0 } - defer store.Close() + defer func() { _ = store.Close() }() // Check if Dolt store supports version control operations doltStore, ok := store.(interface { @@ -662,7 +663,7 @@ func hookPostMergeDolt(beadsDir string) int { // Import JSONL to the import branch 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) // Checkout back to original branch _ = doltStore.Checkout(ctx, currentBranch) @@ -830,7 +831,9 @@ func hookPostCheckout(args []string) int { // importFromJSONLToStore imports issues from JSONL to a store. // 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 // TODO: Implement direct store import cmd := exec.Command("bd", "sync", "--import-only", "--no-git-history", "--no-daemon") diff --git a/cmd/bd/sync.go b/cmd/bd/sync.go index f1ddf050..906ce565 100644 --- a/cmd/bd/sync.go +++ b/cmd/bd/sync.go @@ -913,6 +913,7 @@ type SyncConflictRecord struct { // LoadSyncConflictState loads the sync conflict state from disk. func LoadSyncConflictState(beadsDir string) (*SyncConflictState, error) { path := filepath.Join(beadsDir, "sync_conflicts.json") + // #nosec G304 -- path is derived from the workspace .beads directory data, err := os.ReadFile(path) if err != nil { if os.IsNotExist(err) { @@ -1012,7 +1013,7 @@ func resolveSyncConflicts(ctx context.Context, jsonlPath string, strategy config // Handle manual strategy with interactive resolution 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 @@ -1090,8 +1091,7 @@ func resolveSyncConflicts(ctx context.Context, jsonlPath string, strategy config // resolveSyncConflictsManually handles manual conflict resolution with interactive prompts. func resolveSyncConflictsManually(ctx context.Context, jsonlPath, beadsDir string, conflictState *SyncConflictState, - baseMap, localMap, remoteMap map[string]*beads.Issue, - baseIssues, localIssues, remoteIssues []*beads.Issue) error { + baseMap, localMap, remoteMap map[string]*beads.Issue) error { // Build interactive conflicts list var interactiveConflicts []InteractiveConflict diff --git a/cmd/bd/sync_manual.go b/cmd/bd/sync_manual.go index 61ac04a8..ed4c7342 100644 --- a/cmd/bd/sync_manual.go +++ b/cmd/bd/sync_manual.go @@ -160,15 +160,15 @@ func displayConflictDiff(conflict InteractiveConflict) { // Description (show truncated if different) if local.Description != remote.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.RenderAccent("remote:"), truncateText(remote.Description, 60)) + fmt.Printf(" %s %s\n", ui.RenderMuted("local:"), truncateText(local.Description)) + fmt.Printf(" %s %s\n", ui.RenderAccent("remote:"), truncateText(remote.Description)) } // Notes (show truncated if different) if local.Notes != remote.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.RenderAccent("remote:"), truncateText(remote.Notes, 60)) + fmt.Printf(" %s %s\n", ui.RenderMuted("local:"), truncateText(local.Notes)) + fmt.Printf(" %s %s\n", ui.RenderAccent("remote:"), truncateText(remote.Notes)) } // Labels @@ -371,9 +371,11 @@ func valueOrNone(s string) string { 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. -func truncateText(s string, maxLen int) string { +func truncateText(s string) string { if s == "" { return "(empty)" } @@ -383,14 +385,14 @@ func truncateText(s string, maxLen int) string { // Count runes, not bytes, for proper UTF-8 handling runeCount := utf8.RuneCountInString(s) - if runeCount <= maxLen { + if runeCount <= truncateTextMaxLen { return s } // Truncate by runes runes := []rune(s) - if maxLen <= 3 { + if truncateTextMaxLen <= 3 { return "..." } - return string(runes[:maxLen-3]) + "..." + return string(runes[:truncateTextMaxLen-3]) + "..." } diff --git a/cmd/bd/sync_manual_test.go b/cmd/bd/sync_manual_test.go index 4bc32a29..7ddbc43b 100644 --- a/cmd/bd/sync_manual_test.go +++ b/cmd/bd/sync_manual_test.go @@ -10,78 +10,37 @@ import ( func TestTruncateText(t *testing.T) { tests := []struct { - name string - input string - maxLen int - want string + name string + input string + want string }{ { - name: "empty string", - input: "", - maxLen: 10, - want: "(empty)", + name: "empty string", + input: "", + want: "(empty)", }, { - name: "short string", - input: "hello", - maxLen: 10, - want: "hello", + name: "short string", + input: "hello", + want: "hello", }, { - name: "exact length", - input: "0123456789", - maxLen: 10, - want: "0123456789", + name: "newlines replaced", + input: "line1\nline2\r\nline3", + want: "line1 line2 line3", }, { - name: "truncated", - input: "this is a very long string", - maxLen: 15, - 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 🌍🌎🌏...", + name: "truncated at fixed max", + input: strings.Repeat("a", truncateTextMaxLen+10), + want: strings.Repeat("a", truncateTextMaxLen-3) + "...", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - got := truncateText(tt.input, tt.maxLen) + got := truncateText(tt.input) 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) } }) } diff --git a/internal/config/sync.go b/internal/config/sync.go index b1fe6dc3..50661df6 100644 --- a/internal/config/sync.go +++ b/internal/config/sync.go @@ -21,7 +21,7 @@ var ConfigWarningWriter io.Writer = os.Stderr // logConfigWarning logs a warning message if ConfigWarnings is enabled. func logConfigWarning(format string, args ...interface{}) { if ConfigWarnings && ConfigWarningWriter != nil { - fmt.Fprintf(ConfigWarningWriter, format, args...) + _, _ = fmt.Fprintf(ConfigWarningWriter, format, args...) } } diff --git a/internal/storage/dolt/bootstrap.go b/internal/storage/dolt/bootstrap.go index 6eba7522..a482018b 100644 --- a/internal/storage/dolt/bootstrap.go +++ b/internal/storage/dolt/bootstrap.go @@ -140,7 +140,7 @@ func findJSONLPath(beadsDir string) string { func acquireBootstrapLock(lockPath string, timeout time.Duration) (*os.File, error) { // Create lock file // #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 { return nil, fmt.Errorf("failed to create lock file: %w", err) }