/{cmd,internal}: fix lint issues
This commit is contained in:
@@ -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")
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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]) + "..."
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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...)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user