package doctor import ( "os" "path/filepath" "strings" ) // GitignoreTemplate is the canonical .beads/.gitignore content const GitignoreTemplate = `# SQLite databases *.db *.db?* *.db-journal *.db-wal *.db-shm # Daemon runtime files daemon.lock daemon.log daemon.pid bd.sock sync-state.json # Local version tracking (prevents upgrade notification spam after git ops) .local_version # Legacy database files db.sqlite bd.db # Merge artifacts (temporary files from 3-way merge) beads.base.jsonl beads.base.meta.json beads.left.jsonl beads.left.meta.json beads.right.jsonl beads.right.meta.json # NOTE: Do NOT add negation patterns (e.g., !issues.jsonl) here. # They would override fork protection in .git/info/exclude, allowing # contributors to accidentally commit upstream issue databases. # The JSONL files (issues.jsonl, interactions.jsonl) and config files # are tracked by git by default since no pattern above ignores them. ` // requiredPatterns are patterns that MUST be in .beads/.gitignore var requiredPatterns = []string{ "beads.base.jsonl", "beads.left.jsonl", "beads.right.jsonl", "beads.base.meta.json", "beads.left.meta.json", "beads.right.meta.json", "*.db?*", } // CheckGitignore checks if .beads/.gitignore is up to date func CheckGitignore() DoctorCheck { gitignorePath := filepath.Join(".beads", ".gitignore") // Check if file exists content, err := os.ReadFile(gitignorePath) // #nosec G304 -- path is hardcoded if err != nil { return DoctorCheck{ Name: "Gitignore", Status: "warning", Message: ".beads/.gitignore not found", Fix: "Run: bd init (safe to re-run) or bd doctor --fix", } } // Check for required patterns contentStr := string(content) var missing []string for _, pattern := range requiredPatterns { if !strings.Contains(contentStr, pattern) { missing = append(missing, pattern) } } if len(missing) > 0 { return DoctorCheck{ Name: "Gitignore", Status: "warning", Message: "Outdated .beads/.gitignore (missing merge artifact patterns)", Detail: "Missing: " + strings.Join(missing, ", "), Fix: "Run: bd doctor --fix or bd init (safe to re-run)", } } return DoctorCheck{ Name: "Gitignore", Status: "ok", Message: "Up to date", } } // FixGitignore updates .beads/.gitignore to the current template func FixGitignore() error { gitignorePath := filepath.Join(".beads", ".gitignore") // If file exists and is read-only, fix permissions first if info, err := os.Stat(gitignorePath); err == nil { if info.Mode().Perm()&0200 == 0 { // No write permission for owner if err := os.Chmod(gitignorePath, 0600); err != nil { return err } } } // Write canonical template with secure file permissions if err := os.WriteFile(gitignorePath, []byte(GitignoreTemplate), 0600); err != nil { return err } // Ensure permissions are set correctly (some systems respect umask) if err := os.Chmod(gitignorePath, 0600); err != nil { return err } return nil }