Annotate gosec-safe file accesses
This commit is contained in:
@@ -14,20 +14,20 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
compactDryRun bool
|
compactDryRun bool
|
||||||
compactTier int
|
compactTier int
|
||||||
compactAll bool
|
compactAll bool
|
||||||
compactID string
|
compactID string
|
||||||
compactForce bool
|
compactForce bool
|
||||||
compactBatch int
|
compactBatch int
|
||||||
compactWorkers int
|
compactWorkers int
|
||||||
compactStats bool
|
compactStats bool
|
||||||
compactAnalyze bool
|
compactAnalyze bool
|
||||||
compactApply bool
|
compactApply bool
|
||||||
compactAuto bool
|
compactAuto bool
|
||||||
compactSummary string
|
compactSummary string
|
||||||
compactActor string
|
compactActor string
|
||||||
compactLimit int
|
compactLimit int
|
||||||
)
|
)
|
||||||
|
|
||||||
var compactCmd = &cobra.Command{
|
var compactCmd = &cobra.Command{
|
||||||
@@ -762,6 +762,7 @@ func runCompactApply(ctx context.Context, store *sqlite.SQLiteStorage) {
|
|||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
// #nosec G304 -- summary file path provided explicitly by operator
|
||||||
summaryBytes, err = os.ReadFile(compactSummary)
|
summaryBytes, err = os.ReadFile(compactSummary)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Fprintf(os.Stderr, "Error: failed to read summary file: %v\n", err)
|
fmt.Fprintf(os.Stderr, "Error: failed to read summary file: %v\n", err)
|
||||||
|
|||||||
@@ -13,13 +13,13 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/fatih/color"
|
"github.com/fatih/color"
|
||||||
|
_ "github.com/ncruces/go-sqlite3/driver"
|
||||||
|
_ "github.com/ncruces/go-sqlite3/embed"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
"github.com/steveyegge/beads/cmd/bd/doctor"
|
"github.com/steveyegge/beads/cmd/bd/doctor"
|
||||||
"github.com/steveyegge/beads/internal/beads"
|
"github.com/steveyegge/beads/internal/beads"
|
||||||
"github.com/steveyegge/beads/internal/configfile"
|
"github.com/steveyegge/beads/internal/configfile"
|
||||||
"github.com/steveyegge/beads/internal/daemon"
|
"github.com/steveyegge/beads/internal/daemon"
|
||||||
_ "github.com/ncruces/go-sqlite3/driver"
|
|
||||||
_ "github.com/ncruces/go-sqlite3/embed"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// Status constants for doctor checks
|
// Status constants for doctor checks
|
||||||
@@ -148,7 +148,7 @@ func applyFixes(result doctorResult) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func runDiagnostics(path string) doctorResult{
|
func runDiagnostics(path string) doctorResult {
|
||||||
result := doctorResult{
|
result := doctorResult{
|
||||||
Path: path,
|
Path: path,
|
||||||
CLIVersion: Version,
|
CLIVersion: Version,
|
||||||
@@ -293,7 +293,7 @@ func checkInstallation(path string) doctorCheck {
|
|||||||
|
|
||||||
func checkDatabaseVersion(path string) doctorCheck {
|
func checkDatabaseVersion(path string) doctorCheck {
|
||||||
beadsDir := filepath.Join(path, ".beads")
|
beadsDir := filepath.Join(path, ".beads")
|
||||||
|
|
||||||
// Check metadata.json first for custom database name
|
// Check metadata.json first for custom database name
|
||||||
var dbPath string
|
var dbPath string
|
||||||
if cfg, err := configfile.Load(beadsDir); err == nil && cfg != nil && cfg.Database != "" {
|
if cfg, err := configfile.Load(beadsDir); err == nil && cfg != nil && cfg.Database != "" {
|
||||||
@@ -379,7 +379,7 @@ func checkDatabaseVersion(path string) doctorCheck {
|
|||||||
|
|
||||||
func checkIDFormat(path string) doctorCheck {
|
func checkIDFormat(path string) doctorCheck {
|
||||||
beadsDir := filepath.Join(path, ".beads")
|
beadsDir := filepath.Join(path, ".beads")
|
||||||
|
|
||||||
// Check metadata.json first for custom database name
|
// Check metadata.json first for custom database name
|
||||||
var dbPath string
|
var dbPath string
|
||||||
if cfg, err := configfile.Load(beadsDir); err == nil && cfg != nil && cfg.Database != "" {
|
if cfg, err := configfile.Load(beadsDir); err == nil && cfg != nil && cfg.Database != "" {
|
||||||
@@ -668,7 +668,7 @@ func printDiagnostics(result doctorResult) {
|
|||||||
|
|
||||||
func checkMultipleDatabases(path string) doctorCheck {
|
func checkMultipleDatabases(path string) doctorCheck {
|
||||||
beadsDir := filepath.Join(path, ".beads")
|
beadsDir := filepath.Join(path, ".beads")
|
||||||
|
|
||||||
// Find all .db files (excluding backups and vc.db)
|
// Find all .db files (excluding backups and vc.db)
|
||||||
files, err := filepath.Glob(filepath.Join(beadsDir, "*.db"))
|
files, err := filepath.Glob(filepath.Join(beadsDir, "*.db"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -1032,7 +1032,7 @@ func countJSONLIssues(jsonlPath string) (int, map[string]int, error) {
|
|||||||
|
|
||||||
func checkPermissions(path string) doctorCheck {
|
func checkPermissions(path string) doctorCheck {
|
||||||
beadsDir := filepath.Join(path, ".beads")
|
beadsDir := filepath.Join(path, ".beads")
|
||||||
|
|
||||||
// Check if .beads/ is writable
|
// Check if .beads/ is writable
|
||||||
testFile := filepath.Join(beadsDir, ".doctor-test-write")
|
testFile := filepath.Join(beadsDir, ".doctor-test-write")
|
||||||
if err := os.WriteFile(testFile, []byte("test"), 0600); err != nil {
|
if err := os.WriteFile(testFile, []byte("test"), 0600); err != nil {
|
||||||
@@ -1190,9 +1190,9 @@ func checkGitHooks(path string) doctorCheck {
|
|||||||
|
|
||||||
// Recommended hooks and their purposes
|
// Recommended hooks and their purposes
|
||||||
recommendedHooks := map[string]string{
|
recommendedHooks := map[string]string{
|
||||||
"pre-commit": "Flushes pending bd changes to JSONL before commit",
|
"pre-commit": "Flushes pending bd changes to JSONL before commit",
|
||||||
"post-merge": "Imports updated JSONL after git pull/merge",
|
"post-merge": "Imports updated JSONL after git pull/merge",
|
||||||
"pre-push": "Exports database to JSONL before push",
|
"pre-push": "Exports database to JSONL before push",
|
||||||
}
|
}
|
||||||
|
|
||||||
hooksDir := filepath.Join(gitDir, "hooks")
|
hooksDir := filepath.Join(gitDir, "hooks")
|
||||||
@@ -1240,7 +1240,7 @@ func checkGitHooks(path string) doctorCheck {
|
|||||||
|
|
||||||
func checkSchemaCompatibility(path string) doctorCheck {
|
func checkSchemaCompatibility(path string) doctorCheck {
|
||||||
beadsDir := filepath.Join(path, ".beads")
|
beadsDir := filepath.Join(path, ".beads")
|
||||||
|
|
||||||
// Check metadata.json first for custom database name
|
// Check metadata.json first for custom database name
|
||||||
var dbPath string
|
var dbPath string
|
||||||
if cfg, err := configfile.Load(beadsDir); err == nil && cfg != nil && cfg.Database != "" {
|
if cfg, err := configfile.Load(beadsDir); err == nil && cfg != nil && cfg.Database != "" {
|
||||||
@@ -1277,18 +1277,22 @@ func checkSchemaCompatibility(path string) doctorCheck {
|
|||||||
// This is a simplified version since we can't import the internal package directly
|
// This is a simplified version since we can't import the internal package directly
|
||||||
// Check all critical tables and columns
|
// Check all critical tables and columns
|
||||||
criticalChecks := map[string][]string{
|
criticalChecks := map[string][]string{
|
||||||
"issues": {"id", "title", "content_hash", "external_ref", "compacted_at"},
|
"issues": {"id", "title", "content_hash", "external_ref", "compacted_at"},
|
||||||
"dependencies": {"issue_id", "depends_on_id", "type"},
|
"dependencies": {"issue_id", "depends_on_id", "type"},
|
||||||
"child_counters": {"parent_id", "last_child"},
|
"child_counters": {"parent_id", "last_child"},
|
||||||
"export_hashes": {"issue_id", "content_hash"},
|
"export_hashes": {"issue_id", "content_hash"},
|
||||||
}
|
}
|
||||||
|
|
||||||
var missingElements []string
|
var missingElements []string
|
||||||
for table, columns := range criticalChecks {
|
for table, columns := range criticalChecks {
|
||||||
// Try to query all columns
|
// Try to query all columns
|
||||||
query := fmt.Sprintf("SELECT %s FROM %s LIMIT 0", strings.Join(columns, ", "), table)
|
query := fmt.Sprintf(
|
||||||
|
"SELECT %s FROM %s LIMIT 0",
|
||||||
|
strings.Join(columns, ", "),
|
||||||
|
table,
|
||||||
|
) // #nosec G201 -- table/column names sourced from hardcoded map
|
||||||
_, err := db.Exec(query)
|
_, err := db.Exec(query)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
errMsg := err.Error()
|
errMsg := err.Error()
|
||||||
if strings.Contains(errMsg, "no such table") {
|
if strings.Contains(errMsg, "no such table") {
|
||||||
@@ -1296,7 +1300,7 @@ func checkSchemaCompatibility(path string) doctorCheck {
|
|||||||
} else if strings.Contains(errMsg, "no such column") {
|
} else if strings.Contains(errMsg, "no such column") {
|
||||||
// Find which columns are missing
|
// Find which columns are missing
|
||||||
for _, col := range columns {
|
for _, col := range columns {
|
||||||
colQuery := fmt.Sprintf("SELECT %s FROM %s LIMIT 0", col, table)
|
colQuery := fmt.Sprintf("SELECT %s FROM %s LIMIT 0", col, table) // #nosec G201 -- names come from static schema definition
|
||||||
if _, colErr := db.Exec(colQuery); colErr != nil && strings.Contains(colErr.Error(), "no such column") {
|
if _, colErr := db.Exec(colQuery); colErr != nil && strings.Contains(colErr.Error(), "no such column") {
|
||||||
missingElements = append(missingElements, fmt.Sprintf("%s.%s", table, col))
|
missingElements = append(missingElements, fmt.Sprintf("%s.%s", table, col))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -188,6 +188,7 @@ func collectDatabaseStats(dbPath string) map[string]string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func startCPUProfile(path string) error {
|
func startCPUProfile(path string) error {
|
||||||
|
// #nosec G304 -- profile path supplied by CLI flag in trusted environment
|
||||||
f, err := os.Create(path)
|
f, err := os.Create(path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
|||||||
@@ -74,6 +74,7 @@ func CheckGitHooks() []HookStatus {
|
|||||||
|
|
||||||
// getHookVersion extracts the version from a hook file
|
// getHookVersion extracts the version from a hook file
|
||||||
func getHookVersion(path string) (string, error) {
|
func getHookVersion(path string) (string, error) {
|
||||||
|
// #nosec G304 -- hook path constrained to .git/hooks directory
|
||||||
file, err := os.Open(path)
|
file, err := os.Open(path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
@@ -293,6 +294,7 @@ func installHooks(embeddedHooks map[string]string, force bool) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Write hook file
|
// Write hook file
|
||||||
|
// #nosec G306 -- git hooks must be executable for Git to run them
|
||||||
if err := os.WriteFile(hookPath, []byte(hookContent), 0755); err != nil {
|
if err := os.WriteFile(hookPath, []byte(hookContent), 0755); err != nil {
|
||||||
return fmt.Errorf("failed to write %s: %w", hookName, err)
|
return fmt.Errorf("failed to write %s: %w", hookName, err)
|
||||||
}
|
}
|
||||||
|
|||||||
173
cmd/bd/import.go
173
cmd/bd/import.go
@@ -42,14 +42,14 @@ NOTE: Import requires direct database access and does not work with daemon mode.
|
|||||||
fmt.Fprintf(os.Stderr, "Error: failed to create database directory: %v\n", err)
|
fmt.Fprintf(os.Stderr, "Error: failed to create database directory: %v\n", err)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Import requires direct database access due to complex transaction handling
|
// Import requires direct database access due to complex transaction handling
|
||||||
// and collision detection. Force direct mode regardless of daemon state.
|
// and collision detection. Force direct mode regardless of daemon state.
|
||||||
if daemonClient != nil {
|
if daemonClient != nil {
|
||||||
debug.Logf("Debug: import command forcing direct mode (closes daemon connection)\n")
|
debug.Logf("Debug: import command forcing direct mode (closes daemon connection)\n")
|
||||||
_ = daemonClient.Close()
|
_ = daemonClient.Close()
|
||||||
daemonClient = nil
|
daemonClient = nil
|
||||||
|
|
||||||
var err error
|
var err error
|
||||||
store, err = sqlite.New(dbPath)
|
store, err = sqlite.New(dbPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -58,7 +58,7 @@ NOTE: Import requires direct database access and does not work with daemon mode.
|
|||||||
}
|
}
|
||||||
defer func() { _ = store.Close() }()
|
defer func() { _ = store.Close() }()
|
||||||
}
|
}
|
||||||
|
|
||||||
// We'll check if database needs initialization after reading the JSONL
|
// We'll check if database needs initialization after reading the JSONL
|
||||||
// so we can detect the prefix from the imported issues
|
// so we can detect the prefix from the imported issues
|
||||||
|
|
||||||
@@ -96,78 +96,78 @@ NOTE: Import requires direct database access and does not work with daemon mode.
|
|||||||
lineNum := 0
|
lineNum := 0
|
||||||
|
|
||||||
for scanner.Scan() {
|
for scanner.Scan() {
|
||||||
lineNum++
|
lineNum++
|
||||||
rawLine := scanner.Bytes()
|
rawLine := scanner.Bytes()
|
||||||
line := string(rawLine)
|
line := string(rawLine)
|
||||||
|
|
||||||
// Skip empty lines
|
// Skip empty lines
|
||||||
if line == "" {
|
if line == "" {
|
||||||
continue
|
continue
|
||||||
}
|
|
||||||
|
|
||||||
// Detect git conflict markers in raw bytes (before JSON decoding)
|
|
||||||
// This prevents false positives when issue content contains these strings
|
|
||||||
trimmed := bytes.TrimSpace(rawLine)
|
|
||||||
if bytes.HasPrefix(trimmed, []byte("<<<<<<< ")) ||
|
|
||||||
bytes.Equal(trimmed, []byte("=======")) ||
|
|
||||||
bytes.HasPrefix(trimmed, []byte(">>>>>>> ")) {
|
|
||||||
fmt.Fprintf(os.Stderr, "Git conflict markers detected in JSONL file (line %d)\n", lineNum)
|
|
||||||
fmt.Fprintf(os.Stderr, "→ Attempting automatic 3-way merge...\n\n")
|
|
||||||
|
|
||||||
// Attempt automatic merge using bd merge command
|
|
||||||
if err := attemptAutoMerge(input); err != nil {
|
|
||||||
fmt.Fprintf(os.Stderr, "Error: Automatic merge failed: %v\n\n", err)
|
|
||||||
fmt.Fprintf(os.Stderr, "To resolve manually:\n")
|
|
||||||
fmt.Fprintf(os.Stderr, " git checkout --ours .beads/issues.jsonl && bd import -i .beads/issues.jsonl\n")
|
|
||||||
fmt.Fprintf(os.Stderr, " git checkout --theirs .beads/issues.jsonl && bd import -i .beads/issues.jsonl\n\n")
|
|
||||||
fmt.Fprintf(os.Stderr, "For advanced field-level merging, see: https://github.com/neongreen/mono/tree/main/beads-merge\n")
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Fprintf(os.Stderr, "✓ Automatic merge successful\n")
|
// Detect git conflict markers in raw bytes (before JSON decoding)
|
||||||
fmt.Fprintf(os.Stderr, "→ Restarting import with merged JSONL...\n\n")
|
// This prevents false positives when issue content contains these strings
|
||||||
|
trimmed := bytes.TrimSpace(rawLine)
|
||||||
|
if bytes.HasPrefix(trimmed, []byte("<<<<<<< ")) ||
|
||||||
|
bytes.Equal(trimmed, []byte("=======")) ||
|
||||||
|
bytes.HasPrefix(trimmed, []byte(">>>>>>> ")) {
|
||||||
|
fmt.Fprintf(os.Stderr, "Git conflict markers detected in JSONL file (line %d)\n", lineNum)
|
||||||
|
fmt.Fprintf(os.Stderr, "→ Attempting automatic 3-way merge...\n\n")
|
||||||
|
|
||||||
// Re-open the input file to read the merged content
|
// Attempt automatic merge using bd merge command
|
||||||
if input != "" {
|
if err := attemptAutoMerge(input); err != nil {
|
||||||
// Close current file handle
|
fmt.Fprintf(os.Stderr, "Error: Automatic merge failed: %v\n\n", err)
|
||||||
if in != os.Stdin {
|
fmt.Fprintf(os.Stderr, "To resolve manually:\n")
|
||||||
_ = in.Close()
|
fmt.Fprintf(os.Stderr, " git checkout --ours .beads/issues.jsonl && bd import -i .beads/issues.jsonl\n")
|
||||||
}
|
fmt.Fprintf(os.Stderr, " git checkout --theirs .beads/issues.jsonl && bd import -i .beads/issues.jsonl\n\n")
|
||||||
|
fmt.Fprintf(os.Stderr, "For advanced field-level merging, see: https://github.com/neongreen/mono/tree/main/beads-merge\n")
|
||||||
// Re-open the merged file
|
|
||||||
// #nosec G304 - user-provided file path is intentional
|
|
||||||
f, err := os.Open(input)
|
|
||||||
if err != nil {
|
|
||||||
fmt.Fprintf(os.Stderr, "Error reopening merged file: %v\n", err)
|
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
defer func() {
|
|
||||||
if err := f.Close(); err != nil {
|
fmt.Fprintf(os.Stderr, "✓ Automatic merge successful\n")
|
||||||
fmt.Fprintf(os.Stderr, "Warning: failed to close input file: %v\n", err)
|
fmt.Fprintf(os.Stderr, "→ Restarting import with merged JSONL...\n\n")
|
||||||
|
|
||||||
|
// Re-open the input file to read the merged content
|
||||||
|
if input != "" {
|
||||||
|
// Close current file handle
|
||||||
|
if in != os.Stdin {
|
||||||
|
_ = in.Close()
|
||||||
}
|
}
|
||||||
}()
|
|
||||||
in = f
|
// Re-open the merged file
|
||||||
scanner = bufio.NewScanner(in)
|
// #nosec G304 - user-provided file path is intentional
|
||||||
allIssues = nil // Reset issues list
|
f, err := os.Open(input)
|
||||||
lineNum = 0 // Reset line counter
|
if err != nil {
|
||||||
continue // Restart parsing from beginning
|
fmt.Fprintf(os.Stderr, "Error reopening merged file: %v\n", err)
|
||||||
} else {
|
os.Exit(1)
|
||||||
// Can't retry stdin - should not happen since git conflicts only in files
|
}
|
||||||
fmt.Fprintf(os.Stderr, "Error: Cannot retry merge from stdin\n")
|
defer func() {
|
||||||
|
if err := f.Close(); err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "Warning: failed to close input file: %v\n", err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
in = f
|
||||||
|
scanner = bufio.NewScanner(in)
|
||||||
|
allIssues = nil // Reset issues list
|
||||||
|
lineNum = 0 // Reset line counter
|
||||||
|
continue // Restart parsing from beginning
|
||||||
|
} else {
|
||||||
|
// Can't retry stdin - should not happen since git conflicts only in files
|
||||||
|
fmt.Fprintf(os.Stderr, "Error: Cannot retry merge from stdin\n")
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse JSON
|
||||||
|
var issue types.Issue
|
||||||
|
if err := json.Unmarshal([]byte(line), &issue); err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "Error parsing line %d: %v\n", lineNum, err)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// Parse JSON
|
allIssues = append(allIssues, &issue)
|
||||||
var issue types.Issue
|
|
||||||
if err := json.Unmarshal([]byte(line), &issue); err != nil {
|
|
||||||
fmt.Fprintf(os.Stderr, "Error parsing line %d: %v\n", lineNum, err)
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
allIssues = append(allIssues, &issue)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := scanner.Err(); err != nil {
|
if err := scanner.Err(); err != nil {
|
||||||
fmt.Fprintf(os.Stderr, "Error reading input: %v\n", err)
|
fmt.Fprintf(os.Stderr, "Error reading input: %v\n", err)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
@@ -190,12 +190,12 @@ NOTE: Import requires direct database access and does not work with daemon mode.
|
|||||||
detectedPrefix = filepath.Base(cwd)
|
detectedPrefix = filepath.Base(cwd)
|
||||||
}
|
}
|
||||||
detectedPrefix = strings.TrimRight(detectedPrefix, "-")
|
detectedPrefix = strings.TrimRight(detectedPrefix, "-")
|
||||||
|
|
||||||
if err := store.SetConfig(initCtx, "issue_prefix", detectedPrefix); err != nil {
|
if err := store.SetConfig(initCtx, "issue_prefix", detectedPrefix); err != nil {
|
||||||
fmt.Fprintf(os.Stderr, "Error: failed to set issue prefix: %v\n", err)
|
fmt.Fprintf(os.Stderr, "Error: failed to set issue prefix: %v\n", err)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Fprintf(os.Stderr, "✓ Initialized database with prefix '%s' (detected from issues)\n", detectedPrefix)
|
fmt.Fprintf(os.Stderr, "✓ Initialized database with prefix '%s' (detected from issues)\n", detectedPrefix)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -233,7 +233,7 @@ NOTE: Import requires direct database access and does not work with daemon mode.
|
|||||||
fmt.Fprintf(os.Stderr, "\nOr use 'bd rename-prefix' after import to fix the database.\n")
|
fmt.Fprintf(os.Stderr, "\nOr use 'bd rename-prefix' after import to fix the database.\n")
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if it's a collision error
|
// Check if it's a collision error
|
||||||
if result != nil && len(result.CollisionIDs) > 0 {
|
if result != nil && len(result.CollisionIDs) > 0 {
|
||||||
// Print collision report before exiting
|
// Print collision report before exiting
|
||||||
@@ -259,7 +259,7 @@ NOTE: Import requires direct database access and does not work with daemon mode.
|
|||||||
}
|
}
|
||||||
fmt.Fprintf(os.Stderr, "\nUse --rename-on-import to automatically fix prefixes during import.\n")
|
fmt.Fprintf(os.Stderr, "\nUse --rename-on-import to automatically fix prefixes during import.\n")
|
||||||
}
|
}
|
||||||
|
|
||||||
if result.Collisions > 0 {
|
if result.Collisions > 0 {
|
||||||
fmt.Fprintf(os.Stderr, "\n=== Collision Detection Report ===\n")
|
fmt.Fprintf(os.Stderr, "\n=== Collision Detection Report ===\n")
|
||||||
fmt.Fprintf(os.Stderr, "COLLISIONS DETECTED: %d\n", result.Collisions)
|
fmt.Fprintf(os.Stderr, "COLLISIONS DETECTED: %d\n", result.Collisions)
|
||||||
@@ -393,7 +393,7 @@ NOTE: Import requires direct database access and does not work with daemon mode.
|
|||||||
// If jsonlPath is empty or can't be read, falls back to time.Now().
|
// If jsonlPath is empty or can't be read, falls back to time.Now().
|
||||||
func touchDatabaseFile(dbPath, jsonlPath string) error {
|
func touchDatabaseFile(dbPath, jsonlPath string) error {
|
||||||
targetTime := time.Now()
|
targetTime := time.Now()
|
||||||
|
|
||||||
// If we have the JSONL path, use max(JSONL mtime, now) to handle clock skew
|
// If we have the JSONL path, use max(JSONL mtime, now) to handle clock skew
|
||||||
if jsonlPath != "" {
|
if jsonlPath != "" {
|
||||||
if info, err := os.Stat(jsonlPath); err == nil {
|
if info, err := os.Stat(jsonlPath); err == nil {
|
||||||
@@ -403,7 +403,7 @@ func touchDatabaseFile(dbPath, jsonlPath string) error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Best-effort touch - don't fail import if this doesn't work
|
// Best-effort touch - don't fail import if this doesn't work
|
||||||
return os.Chtimes(dbPath, targetTime, targetTime)
|
return os.Chtimes(dbPath, targetTime, targetTime)
|
||||||
}
|
}
|
||||||
@@ -418,17 +418,17 @@ func checkUncommittedChanges(filePath string, result *ImportResult) {
|
|||||||
|
|
||||||
// Get the directory containing the file to use as git working directory
|
// Get the directory containing the file to use as git working directory
|
||||||
workDir := filepath.Dir(filePath)
|
workDir := filepath.Dir(filePath)
|
||||||
|
|
||||||
// Use git diff to check if working tree differs from HEAD
|
// Use git diff to check if working tree differs from HEAD
|
||||||
cmd := fmt.Sprintf("git diff --quiet HEAD %s", filePath)
|
cmd := fmt.Sprintf("git diff --quiet HEAD %s", filePath)
|
||||||
exitCode, _ := runGitCommand(cmd, workDir)
|
exitCode, _ := runGitCommand(cmd, workDir)
|
||||||
|
|
||||||
// Exit code 0 = no changes, 1 = changes exist, >1 = error
|
// Exit code 0 = no changes, 1 = changes exist, >1 = error
|
||||||
if exitCode == 1 {
|
if exitCode == 1 {
|
||||||
// Get line counts for context
|
// Get line counts for context
|
||||||
workingTreeLines := countLines(filePath)
|
workingTreeLines := countLines(filePath)
|
||||||
headLines := countLinesInGitHEAD(filePath, workDir)
|
headLines := countLinesInGitHEAD(filePath, workDir)
|
||||||
|
|
||||||
fmt.Fprintf(os.Stderr, "\n⚠️ Warning: .beads/issues.jsonl has uncommitted changes\n")
|
fmt.Fprintf(os.Stderr, "\n⚠️ Warning: .beads/issues.jsonl has uncommitted changes\n")
|
||||||
fmt.Fprintf(os.Stderr, " Working tree: %d lines\n", workingTreeLines)
|
fmt.Fprintf(os.Stderr, " Working tree: %d lines\n", workingTreeLines)
|
||||||
if headLines > 0 {
|
if headLines > 0 {
|
||||||
@@ -466,7 +466,7 @@ func countLines(filePath string) int {
|
|||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
defer func() { _ = f.Close() }()
|
defer func() { _ = f.Close() }()
|
||||||
|
|
||||||
scanner := bufio.NewScanner(f)
|
scanner := bufio.NewScanner(f)
|
||||||
lines := 0
|
lines := 0
|
||||||
for scanner.Scan() {
|
for scanner.Scan() {
|
||||||
@@ -484,24 +484,24 @@ func countLinesInGitHEAD(filePath string, workDir string) int {
|
|||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
gitRoot := strings.TrimSpace(gitRootOutput)
|
gitRoot := strings.TrimSpace(gitRootOutput)
|
||||||
|
|
||||||
// Make filePath relative to git root
|
// Make filePath relative to git root
|
||||||
absPath, err := filepath.Abs(filePath)
|
absPath, err := filepath.Abs(filePath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
relPath, err := filepath.Rel(gitRoot, absPath)
|
relPath, err := filepath.Rel(gitRoot, absPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
cmd := fmt.Sprintf("git show HEAD:%s 2>/dev/null | wc -l", relPath)
|
cmd := fmt.Sprintf("git show HEAD:%s 2>/dev/null | wc -l", relPath)
|
||||||
exitCode, output := runGitCommand(cmd, workDir)
|
exitCode, output := runGitCommand(cmd, workDir)
|
||||||
if exitCode != 0 {
|
if exitCode != 0 {
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
var lines int
|
var lines int
|
||||||
_, err = fmt.Sscanf(strings.TrimSpace(output), "%d", &lines)
|
_, err = fmt.Sscanf(strings.TrimSpace(output), "%d", &lines)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -518,7 +518,7 @@ func attemptAutoMerge(conflictedPath string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Get git repository root
|
// Get git repository root
|
||||||
gitRootCmd := exec.Command("git", "rev-parse", "--show-toplevel")
|
gitRootCmd := exec.Command("git", "rev-parse", "--show-toplevel") // #nosec G204 -- fixed git invocation for repo root discovery
|
||||||
gitRootOutput, err := gitRootCmd.Output()
|
gitRootOutput, err := gitRootCmd.Output()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("not in a git repository: %w", err)
|
return fmt.Errorf("not in a git repository: %w", err)
|
||||||
@@ -553,7 +553,7 @@ func attemptAutoMerge(conflictedPath string) error {
|
|||||||
outputPath := filepath.Join(tmpDir, "merged.jsonl")
|
outputPath := filepath.Join(tmpDir, "merged.jsonl")
|
||||||
|
|
||||||
// Extract base version (merge-base)
|
// Extract base version (merge-base)
|
||||||
baseCmd := exec.Command("git", "show", fmt.Sprintf(":1:%s", relPath))
|
baseCmd := exec.Command("git", "show", fmt.Sprintf(":1:%s", relPath)) // #nosec G204 -- relPath limited to files tracked in current repo
|
||||||
baseCmd.Dir = gitRoot
|
baseCmd.Dir = gitRoot
|
||||||
baseContent, err := baseCmd.Output()
|
baseContent, err := baseCmd.Output()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -566,7 +566,7 @@ func attemptAutoMerge(conflictedPath string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Extract left version (ours/HEAD)
|
// Extract left version (ours/HEAD)
|
||||||
leftCmd := exec.Command("git", "show", fmt.Sprintf(":2:%s", relPath))
|
leftCmd := exec.Command("git", "show", fmt.Sprintf(":2:%s", relPath)) // #nosec G204 -- relPath limited to files tracked in current repo
|
||||||
leftCmd.Dir = gitRoot
|
leftCmd.Dir = gitRoot
|
||||||
leftContent, err := leftCmd.Output()
|
leftContent, err := leftCmd.Output()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -577,7 +577,7 @@ func attemptAutoMerge(conflictedPath string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Extract right version (theirs/MERGE_HEAD)
|
// Extract right version (theirs/MERGE_HEAD)
|
||||||
rightCmd := exec.Command("git", "show", fmt.Sprintf(":3:%s", relPath))
|
rightCmd := exec.Command("git", "show", fmt.Sprintf(":3:%s", relPath)) // #nosec G204 -- relPath limited to files tracked in current repo
|
||||||
rightCmd.Dir = gitRoot
|
rightCmd.Dir = gitRoot
|
||||||
rightContent, err := rightCmd.Output()
|
rightContent, err := rightCmd.Output()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -594,7 +594,7 @@ func attemptAutoMerge(conflictedPath string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Invoke bd merge command
|
// Invoke bd merge command
|
||||||
mergeCmd := exec.Command(exe, "merge", outputPath, basePath, leftPath, rightPath)
|
mergeCmd := exec.Command(exe, "merge", outputPath, basePath, leftPath, rightPath) // #nosec G204 -- executes current bd binary for deterministic merge
|
||||||
mergeOutput, err := mergeCmd.CombinedOutput()
|
mergeOutput, err := mergeCmd.CombinedOutput()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// Check exit code - bd merge returns 1 if there are conflicts, 2 for errors
|
// Check exit code - bd merge returns 1 if there are conflicts, 2 for errors
|
||||||
@@ -608,6 +608,7 @@ func attemptAutoMerge(conflictedPath string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Merge succeeded - copy merged result back to original file
|
// Merge succeeded - copy merged result back to original file
|
||||||
|
// #nosec G304 -- merged output created earlier in this function
|
||||||
mergedContent, err := os.ReadFile(outputPath)
|
mergedContent, err := os.ReadFile(outputPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to read merged output: %w", err)
|
return fmt.Errorf("failed to read merged output: %w", err)
|
||||||
@@ -618,7 +619,7 @@ func attemptAutoMerge(conflictedPath string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Stage the resolved file
|
// Stage the resolved file
|
||||||
stageCmd := exec.Command("git", "add", relPath)
|
stageCmd := exec.Command("git", "add", relPath) // #nosec G204 -- relPath constrained to file within current repo
|
||||||
stageCmd.Dir = gitRoot
|
stageCmd.Dir = gitRoot
|
||||||
if err := stageCmd.Run(); err != nil {
|
if err := stageCmd.Run(); err != nil {
|
||||||
// Non-fatal - user can stage manually
|
// Non-fatal - user can stage manually
|
||||||
@@ -634,7 +635,7 @@ func detectPrefixFromIssues(issues []*types.Issue) string {
|
|||||||
if len(issues) == 0 {
|
if len(issues) == 0 {
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
// Count prefix occurrences
|
// Count prefix occurrences
|
||||||
prefixCounts := make(map[string]int)
|
prefixCounts := make(map[string]int)
|
||||||
for _, issue := range issues {
|
for _, issue := range issues {
|
||||||
@@ -644,7 +645,7 @@ func detectPrefixFromIssues(issues []*types.Issue) string {
|
|||||||
prefixCounts[issue.ID[:idx]]++
|
prefixCounts[issue.ID[:idx]]++
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Find most common prefix
|
// Find most common prefix
|
||||||
maxCount := 0
|
maxCount := 0
|
||||||
commonPrefix := ""
|
commonPrefix := ""
|
||||||
@@ -654,7 +655,7 @@ func detectPrefixFromIssues(issues []*types.Issue) string {
|
|||||||
commonPrefix = prefix
|
commonPrefix = prefix
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return commonPrefix
|
return commonPrefix
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -965,6 +965,7 @@ func createConfigYaml(beadsDir string, noDbMode bool) error {
|
|||||||
|
|
||||||
// readFirstIssueFromJSONL reads the first issue from a JSONL file
|
// readFirstIssueFromJSONL reads the first issue from a JSONL file
|
||||||
func readFirstIssueFromJSONL(path string) (*types.Issue, error) {
|
func readFirstIssueFromJSONL(path string) (*types.Issue, error) {
|
||||||
|
// #nosec G304 -- helper reads JSONL file chosen by current bd command
|
||||||
file, err := os.Open(path)
|
file, err := os.Open(path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to open JSONL file: %w", err)
|
return nil, fmt.Errorf("failed to open JSONL file: %w", err)
|
||||||
|
|||||||
@@ -122,6 +122,7 @@ func runContributorWizard(ctx context.Context, store storage.Storage) error {
|
|||||||
|
|
||||||
// Create issues.jsonl
|
// Create issues.jsonl
|
||||||
jsonlPath := filepath.Join(beadsDir, "beads.jsonl")
|
jsonlPath := filepath.Join(beadsDir, "beads.jsonl")
|
||||||
|
// #nosec G306 -- planning repo JSONL must be shareable across collaborators
|
||||||
if err := os.WriteFile(jsonlPath, []byte{}, 0644); err != nil {
|
if err := os.WriteFile(jsonlPath, []byte{}, 0644); err != nil {
|
||||||
return fmt.Errorf("failed to create issues.jsonl: %w", err)
|
return fmt.Errorf("failed to create issues.jsonl: %w", err)
|
||||||
}
|
}
|
||||||
@@ -144,6 +145,7 @@ Issues here are automatically created when working on forked repositories.
|
|||||||
|
|
||||||
Created by: bd init --contributor
|
Created by: bd init --contributor
|
||||||
`)
|
`)
|
||||||
|
// #nosec G306 -- README should be world-readable
|
||||||
if err := os.WriteFile(readmePath, []byte(readmeContent), 0644); err != nil {
|
if err := os.WriteFile(readmePath, []byte(readmeContent), 0644); err != nil {
|
||||||
fmt.Fprintf(os.Stderr, "Warning: failed to create README: %v\n", err)
|
fmt.Fprintf(os.Stderr, "Warning: failed to create README: %v\n", err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -299,7 +299,7 @@ func findCandidateIssues(ctx context.Context, db *sql.DB, p migrateIssuesParams)
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Build query
|
// Build query
|
||||||
query := "SELECT id FROM issues WHERE " + strings.Join(conditions, " AND ")
|
query := "SELECT id FROM issues WHERE " + strings.Join(conditions, " AND ") // #nosec G202 -- query fragments are constant strings with parameter placeholders
|
||||||
|
|
||||||
rows, err := db.QueryContext(ctx, query, args...)
|
rows, err := db.QueryContext(ctx, query, args...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -499,7 +499,7 @@ func countCrossRepoEdges(ctx context.Context, db *sql.DB, migrationSet []string)
|
|||||||
incomingQuery := fmt.Sprintf(`
|
incomingQuery := fmt.Sprintf(`
|
||||||
SELECT COUNT(*) FROM dependencies
|
SELECT COUNT(*) FROM dependencies
|
||||||
WHERE depends_on_id IN (%s)
|
WHERE depends_on_id IN (%s)
|
||||||
AND issue_id NOT IN (%s)`, inClause, inClause)
|
AND issue_id NOT IN (%s)`, inClause, inClause) // #nosec G201 -- inClause generated from sanitized placeholders
|
||||||
|
|
||||||
var incoming int
|
var incoming int
|
||||||
if err := db.QueryRowContext(ctx, incomingQuery, append(args, args...)...).Scan(&incoming); err != nil {
|
if err := db.QueryRowContext(ctx, incomingQuery, append(args, args...)...).Scan(&incoming); err != nil {
|
||||||
@@ -510,7 +510,7 @@ func countCrossRepoEdges(ctx context.Context, db *sql.DB, migrationSet []string)
|
|||||||
outgoingQuery := fmt.Sprintf(`
|
outgoingQuery := fmt.Sprintf(`
|
||||||
SELECT COUNT(*) FROM dependencies
|
SELECT COUNT(*) FROM dependencies
|
||||||
WHERE issue_id IN (%s)
|
WHERE issue_id IN (%s)
|
||||||
AND depends_on_id NOT IN (%s)`, inClause, inClause)
|
AND depends_on_id NOT IN (%s)`, inClause, inClause) // #nosec G201 -- inClause generated from sanitized placeholders
|
||||||
|
|
||||||
var outgoing int
|
var outgoing int
|
||||||
if err := db.QueryRowContext(ctx, outgoingQuery, append(args, args...)...).Scan(&outgoing); err != nil {
|
if err := db.QueryRowContext(ctx, outgoingQuery, append(args, args...)...).Scan(&outgoing); err != nil {
|
||||||
@@ -665,6 +665,7 @@ func executeMigration(ctx context.Context, db *sql.DB, migrationSet []string, to
|
|||||||
}
|
}
|
||||||
|
|
||||||
func loadIDsFromFile(path string) ([]string, error) {
|
func loadIDsFromFile(path string) ([]string, error) {
|
||||||
|
// #nosec G304 -- file path supplied explicitly via CLI flag
|
||||||
data, err := os.ReadFile(path)
|
data, err := os.ReadFile(path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|||||||
@@ -75,6 +75,7 @@ func isMCPActive() bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
settingsPath := filepath.Join(home, ".claude/settings.json")
|
settingsPath := filepath.Join(home, ".claude/settings.json")
|
||||||
|
// #nosec G304 -- settings path derived from user home directory
|
||||||
data, err := os.ReadFile(settingsPath)
|
data, err := os.ReadFile(settingsPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false
|
return false
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ var showCmd = &cobra.Command{
|
|||||||
Run: func(cmd *cobra.Command, args []string) {
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
jsonOutput, _ := cmd.Flags().GetBool("json")
|
jsonOutput, _ := cmd.Flags().GetBool("json")
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
|
|
||||||
// Resolve partial IDs first
|
// Resolve partial IDs first
|
||||||
var resolvedIDs []string
|
var resolvedIDs []string
|
||||||
if daemonClient != nil {
|
if daemonClient != nil {
|
||||||
@@ -45,7 +45,7 @@ var showCmd = &cobra.Command{
|
|||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// If daemon is running, use RPC
|
// If daemon is running, use RPC
|
||||||
if daemonClient != nil {
|
if daemonClient != nil {
|
||||||
allDetails := []interface{}{}
|
allDetails := []interface{}{}
|
||||||
@@ -381,7 +381,7 @@ var updateCmd = &cobra.Command{
|
|||||||
}
|
}
|
||||||
|
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
|
|
||||||
// Resolve partial IDs first
|
// Resolve partial IDs first
|
||||||
var resolvedIDs []string
|
var resolvedIDs []string
|
||||||
if daemonClient != nil {
|
if daemonClient != nil {
|
||||||
@@ -402,7 +402,7 @@ var updateCmd = &cobra.Command{
|
|||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// If daemon is running, use RPC
|
// If daemon is running, use RPC
|
||||||
if daemonClient != nil {
|
if daemonClient != nil {
|
||||||
updatedIssues := []*types.Issue{}
|
updatedIssues := []*types.Issue{}
|
||||||
@@ -434,7 +434,7 @@ var updateCmd = &cobra.Command{
|
|||||||
if acceptanceCriteria, ok := updates["acceptance_criteria"].(string); ok {
|
if acceptanceCriteria, ok := updates["acceptance_criteria"].(string); ok {
|
||||||
updateArgs.AcceptanceCriteria = &acceptanceCriteria
|
updateArgs.AcceptanceCriteria = &acceptanceCriteria
|
||||||
}
|
}
|
||||||
if externalRef, ok := updates["external_ref"].(string); ok { // NEW: Map external_ref
|
if externalRef, ok := updates["external_ref"].(string); ok { // NEW: Map external_ref
|
||||||
updateArgs.ExternalRef = &externalRef
|
updateArgs.ExternalRef = &externalRef
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -464,12 +464,12 @@ var updateCmd = &cobra.Command{
|
|||||||
// Direct mode
|
// Direct mode
|
||||||
updatedIssues := []*types.Issue{}
|
updatedIssues := []*types.Issue{}
|
||||||
for _, id := range resolvedIDs {
|
for _, id := range resolvedIDs {
|
||||||
if err := store.UpdateIssue(ctx, id, updates, actor); err != nil {
|
if err := store.UpdateIssue(ctx, id, updates, actor); err != nil {
|
||||||
fmt.Fprintf(os.Stderr, "Error updating %s: %v\n", id, err)
|
fmt.Fprintf(os.Stderr, "Error updating %s: %v\n", id, err)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
if jsonOutput {
|
if jsonOutput {
|
||||||
issue, _ := store.GetIssue(ctx, id)
|
issue, _ := store.GetIssue(ctx, id)
|
||||||
if issue != nil {
|
if issue != nil {
|
||||||
updatedIssues = append(updatedIssues, issue)
|
updatedIssues = append(updatedIssues, issue)
|
||||||
@@ -508,7 +508,7 @@ Examples:
|
|||||||
Run: func(cmd *cobra.Command, args []string) {
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
id := args[0]
|
id := args[0]
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
|
|
||||||
// Resolve partial ID if in direct mode
|
// Resolve partial ID if in direct mode
|
||||||
if daemonClient == nil {
|
if daemonClient == nil {
|
||||||
fullID, err := utils.ResolvePartialID(ctx, store, id)
|
fullID, err := utils.ResolvePartialID(ctx, store, id)
|
||||||
@@ -625,6 +625,7 @@ Examples:
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Read the edited content
|
// Read the edited content
|
||||||
|
// #nosec G304 -- tmpPath was created earlier in this function
|
||||||
editedContent, err := os.ReadFile(tmpPath)
|
editedContent, err := os.ReadFile(tmpPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Fprintf(os.Stderr, "Error reading edited file: %v\n", err)
|
fmt.Fprintf(os.Stderr, "Error reading edited file: %v\n", err)
|
||||||
@@ -699,7 +700,7 @@ var closeCmd = &cobra.Command{
|
|||||||
jsonOutput, _ := cmd.Flags().GetBool("json")
|
jsonOutput, _ := cmd.Flags().GetBool("json")
|
||||||
|
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
|
|
||||||
// Resolve partial IDs first
|
// Resolve partial IDs first
|
||||||
var resolvedIDs []string
|
var resolvedIDs []string
|
||||||
if daemonClient != nil {
|
if daemonClient != nil {
|
||||||
|
|||||||
@@ -306,6 +306,7 @@ func (sm *SnapshotManager) writeMetadata(path string, meta snapshotMetadata) err
|
|||||||
|
|
||||||
// Use process-specific temp file for atomic write
|
// Use process-specific temp file for atomic write
|
||||||
tempPath := fmt.Sprintf("%s.%d.tmp", path, os.Getpid())
|
tempPath := fmt.Sprintf("%s.%d.tmp", path, os.Getpid())
|
||||||
|
// #nosec G306 -- metadata is shared across repo users and must stay readable
|
||||||
if err := os.WriteFile(tempPath, data, 0644); err != nil {
|
if err := os.WriteFile(tempPath, data, 0644); err != nil {
|
||||||
return fmt.Errorf("failed to write metadata temp file: %w", err)
|
return fmt.Errorf("failed to write metadata temp file: %w", err)
|
||||||
}
|
}
|
||||||
@@ -315,6 +316,7 @@ func (sm *SnapshotManager) writeMetadata(path string, meta snapshotMetadata) err
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (sm *SnapshotManager) readMetadata(path string) (*snapshotMetadata, error) {
|
func (sm *SnapshotManager) readMetadata(path string) (*snapshotMetadata, error) {
|
||||||
|
// #nosec G304 -- metadata lives under .beads and path is derived internally
|
||||||
data, err := os.ReadFile(path)
|
data, err := os.ReadFile(path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if os.IsNotExist(err) {
|
if os.IsNotExist(err) {
|
||||||
@@ -360,6 +362,7 @@ func (sm *SnapshotManager) validateMetadata(meta *snapshotMetadata, currentCommi
|
|||||||
func (sm *SnapshotManager) buildIDToLineMap(path string) (map[string]string, error) {
|
func (sm *SnapshotManager) buildIDToLineMap(path string) (map[string]string, error) {
|
||||||
result := make(map[string]string)
|
result := make(map[string]string)
|
||||||
|
|
||||||
|
// #nosec G304 -- snapshot file lives in .beads/snapshots and path is derived internally
|
||||||
f, err := os.Open(path)
|
f, err := os.Open(path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if os.IsNotExist(err) {
|
if os.IsNotExist(err) {
|
||||||
@@ -397,6 +400,7 @@ func (sm *SnapshotManager) buildIDToLineMap(path string) (map[string]string, err
|
|||||||
func (sm *SnapshotManager) buildIDSet(path string) (map[string]bool, error) {
|
func (sm *SnapshotManager) buildIDSet(path string) (map[string]bool, error) {
|
||||||
result := make(map[string]bool)
|
result := make(map[string]bool)
|
||||||
|
|
||||||
|
// #nosec G304 -- snapshot file path derived from internal state
|
||||||
f, err := os.Open(path)
|
f, err := os.Open(path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if os.IsNotExist(err) {
|
if os.IsNotExist(err) {
|
||||||
@@ -443,12 +447,14 @@ func (sm *SnapshotManager) jsonEquals(a, b string) bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (sm *SnapshotManager) copyFile(src, dst string) error {
|
func (sm *SnapshotManager) copyFile(src, dst string) error {
|
||||||
|
// #nosec G304 -- snapshot copy only touches files inside .beads/snapshots
|
||||||
sourceFile, err := os.Open(src)
|
sourceFile, err := os.Open(src)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
defer sourceFile.Close()
|
defer sourceFile.Close()
|
||||||
|
|
||||||
|
// #nosec G304 -- snapshot copy only writes files inside .beads/snapshots
|
||||||
destFile, err := os.Create(dst)
|
destFile, err := os.Create(dst)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
|||||||
@@ -32,13 +32,13 @@ type StatusSummary struct {
|
|||||||
|
|
||||||
// RecentActivitySummary represents activity from git history
|
// RecentActivitySummary represents activity from git history
|
||||||
type RecentActivitySummary struct {
|
type RecentActivitySummary struct {
|
||||||
HoursTracked int `json:"hours_tracked"`
|
HoursTracked int `json:"hours_tracked"`
|
||||||
CommitCount int `json:"commit_count"`
|
CommitCount int `json:"commit_count"`
|
||||||
IssuesCreated int `json:"issues_created"`
|
IssuesCreated int `json:"issues_created"`
|
||||||
IssuesClosed int `json:"issues_closed"`
|
IssuesClosed int `json:"issues_closed"`
|
||||||
IssuesUpdated int `json:"issues_updated"`
|
IssuesUpdated int `json:"issues_updated"`
|
||||||
IssuesReopened int `json:"issues_reopened"`
|
IssuesReopened int `json:"issues_reopened"`
|
||||||
TotalChanges int `json:"total_changes"`
|
TotalChanges int `json:"total_changes"`
|
||||||
}
|
}
|
||||||
|
|
||||||
var statusCmd = &cobra.Command{
|
var statusCmd = &cobra.Command{
|
||||||
@@ -168,8 +168,8 @@ func getGitActivity(hours int) *RecentActivitySummary {
|
|||||||
|
|
||||||
// Run git log to get patches for the last N hours
|
// Run git log to get patches for the last N hours
|
||||||
since := fmt.Sprintf("%d hours ago", hours)
|
since := fmt.Sprintf("%d hours ago", hours)
|
||||||
cmd := exec.Command("git", "log", "--since="+since, "--numstat", "--pretty=format:%H", ".beads/beads.jsonl")
|
cmd := exec.Command("git", "log", "--since="+since, "--numstat", "--pretty=format:%H", ".beads/beads.jsonl") // #nosec G204 -- bounded arguments for local git history inspection
|
||||||
|
|
||||||
output, err := cmd.Output()
|
output, err := cmd.Output()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// Git log failed (might not be a git repo or no commits)
|
// Git log failed (might not be a git repo or no commits)
|
||||||
@@ -178,63 +178,63 @@ func getGitActivity(hours int) *RecentActivitySummary {
|
|||||||
|
|
||||||
scanner := bufio.NewScanner(strings.NewReader(string(output)))
|
scanner := bufio.NewScanner(strings.NewReader(string(output)))
|
||||||
commitCount := 0
|
commitCount := 0
|
||||||
|
|
||||||
for scanner.Scan() {
|
for scanner.Scan() {
|
||||||
line := scanner.Text()
|
line := scanner.Text()
|
||||||
|
|
||||||
// Empty lines separate commits
|
// Empty lines separate commits
|
||||||
if line == "" {
|
if line == "" {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
// Commit hash line
|
// Commit hash line
|
||||||
if !strings.Contains(line, "\t") {
|
if !strings.Contains(line, "\t") {
|
||||||
commitCount++
|
commitCount++
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
// numstat line format: "additions\tdeletions\tfilename"
|
// numstat line format: "additions\tdeletions\tfilename"
|
||||||
parts := strings.Split(line, "\t")
|
parts := strings.Split(line, "\t")
|
||||||
if len(parts) < 3 {
|
if len(parts) < 3 {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
// For JSONL files, each added line is a new/updated issue
|
// For JSONL files, each added line is a new/updated issue
|
||||||
// We need to analyze the actual diff to understand what changed
|
// We need to analyze the actual diff to understand what changed
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get detailed diff to analyze changes
|
// Get detailed diff to analyze changes
|
||||||
cmd = exec.Command("git", "log", "--since="+since, "-p", ".beads/beads.jsonl")
|
cmd = exec.Command("git", "log", "--since="+since, "-p", ".beads/beads.jsonl") // #nosec G204 -- bounded arguments for local git history inspection
|
||||||
output, err = cmd.Output()
|
output, err = cmd.Output()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
scanner = bufio.NewScanner(strings.NewReader(string(output)))
|
scanner = bufio.NewScanner(strings.NewReader(string(output)))
|
||||||
for scanner.Scan() {
|
for scanner.Scan() {
|
||||||
line := scanner.Text()
|
line := scanner.Text()
|
||||||
|
|
||||||
// Look for added lines in diff (lines starting with +)
|
// Look for added lines in diff (lines starting with +)
|
||||||
if !strings.HasPrefix(line, "+") || strings.HasPrefix(line, "+++") {
|
if !strings.HasPrefix(line, "+") || strings.HasPrefix(line, "+++") {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove the + prefix
|
// Remove the + prefix
|
||||||
jsonLine := strings.TrimPrefix(line, "+")
|
jsonLine := strings.TrimPrefix(line, "+")
|
||||||
|
|
||||||
// Skip empty lines
|
// Skip empty lines
|
||||||
if strings.TrimSpace(jsonLine) == "" {
|
if strings.TrimSpace(jsonLine) == "" {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
// Try to parse as issue JSON
|
// Try to parse as issue JSON
|
||||||
var issue types.Issue
|
var issue types.Issue
|
||||||
if err := json.Unmarshal([]byte(jsonLine), &issue); err != nil {
|
if err := json.Unmarshal([]byte(jsonLine), &issue); err != nil {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
activity.TotalChanges++
|
activity.TotalChanges++
|
||||||
|
|
||||||
// Analyze the change type based on timestamps and status
|
// Analyze the change type based on timestamps and status
|
||||||
// Created recently if created_at is close to now
|
// Created recently if created_at is close to now
|
||||||
if time.Since(issue.CreatedAt) < time.Duration(hours)*time.Hour {
|
if time.Since(issue.CreatedAt) < time.Duration(hours)*time.Hour {
|
||||||
@@ -253,7 +253,7 @@ func getGitActivity(hours int) *RecentActivitySummary {
|
|||||||
activity.IssuesUpdated++
|
activity.IssuesUpdated++
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
activity.CommitCount = commitCount
|
activity.CommitCount = commitCount
|
||||||
return activity
|
return activity
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -118,7 +118,7 @@ func Merge3Way(outputPath, basePath, leftPath, rightPath string, debug bool) err
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Open output file for writing
|
// Open output file for writing
|
||||||
outFile, err := os.Create(outputPath)
|
outFile, err := os.Create(outputPath) // #nosec G304 -- outputPath provided by CLI flag but sanitized earlier
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("error creating output file: %w", err)
|
return fmt.Errorf("error creating output file: %w", err)
|
||||||
}
|
}
|
||||||
@@ -150,7 +150,8 @@ func Merge3Way(outputPath, basePath, leftPath, rightPath string, debug bool) err
|
|||||||
if err := outFile.Sync(); err != nil {
|
if err := outFile.Sync(); err != nil {
|
||||||
fmt.Fprintf(os.Stderr, "Warning: failed to sync output file: %v\n", err)
|
fmt.Fprintf(os.Stderr, "Warning: failed to sync output file: %v\n", err)
|
||||||
}
|
}
|
||||||
if content, err := os.ReadFile(outputPath); err == nil { // #nosec G304 -- debug output reads file created earlier in same function
|
// #nosec G304 -- debug output reads file created earlier in same function
|
||||||
|
if content, err := os.ReadFile(outputPath); err == nil {
|
||||||
lines := 0
|
lines := 0
|
||||||
fmt.Fprintf(os.Stderr, "Output file preview (first 10 lines):\n")
|
fmt.Fprintf(os.Stderr, "Output file preview (first 10 lines):\n")
|
||||||
for _, line := range splitLines(string(content)) {
|
for _, line := range splitLines(string(content)) {
|
||||||
|
|||||||
@@ -403,6 +403,7 @@ func exportToJSONL(ctx context.Context, store storage.Storage, path string) erro
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Write to JSONL file
|
// Write to JSONL file
|
||||||
|
// #nosec G304 -- fixture exports to deterministic file controlled by tests
|
||||||
f, err := os.Create(path)
|
f, err := os.Create(path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to create JSONL file: %w", err)
|
return fmt.Errorf("failed to create JSONL file: %w", err)
|
||||||
@@ -422,6 +423,7 @@ func exportToJSONL(ctx context.Context, store storage.Storage, path string) erro
|
|||||||
// importFromJSONL imports issues from a JSONL file
|
// importFromJSONL imports issues from a JSONL file
|
||||||
func importFromJSONL(ctx context.Context, store storage.Storage, path string) error {
|
func importFromJSONL(ctx context.Context, store storage.Storage, path string) error {
|
||||||
// Read JSONL file
|
// Read JSONL file
|
||||||
|
// #nosec G304 -- fixture imports from deterministic file created earlier in test
|
||||||
data, err := os.ReadFile(path)
|
data, err := os.ReadFile(path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to read JSONL file: %w", err)
|
return fmt.Errorf("failed to read JSONL file: %w", err)
|
||||||
|
|||||||
Reference in New Issue
Block a user