Files
beads/cmd/bd/thanks.go
Ryan 3c08e5eb9d DOCTOR IMPROVEMENTS: visual improvements/grouping + add comprehensive tests + fix gosec warnings (#656)
* test(doctor): add comprehensive tests for fix and check functions

Add edge case tests, e2e tests, and improve test coverage for:
- database_test.go: database integrity and sync checks
- git_test.go: git hooks, merge driver, sync branch tests
- gitignore_test.go: gitignore validation
- prefix_test.go: ID prefix handling
- fix/fix_test.go: fix operations
- fix/e2e_test.go: end-to-end fix scenarios
- fix/fix_edge_cases_test.go: edge case handling

* docs: add testing philosophy and anti-patterns guide

- Create TESTING_PHILOSOPHY.md covering test pyramid, priority matrix,
  what NOT to test, and 5 anti-patterns with code examples
- Add cross-reference from README_TESTING.md
- Document beads-specific guidance (well-covered areas vs gaps)
- Include target metrics (test-to-code ratio, execution time targets)

* chore: revert .beads/ to upstream/main state

* refactor(doctor): add category grouping and Ayu theme colors

- Add Category field to DoctorCheck for organizing checks by type
- Define category constants: Core, Git, Runtime, Data, Integration, Metadata
- Update thanks command to use shared Ayu color palette from internal/ui
- Simplify test fixtures by removing redundant test cases

* fix(doctor): prevent test fork bomb and fix test failures

- Add ErrTestBinary guard in getBdBinary() to prevent tests from
  recursively executing the test binary when calling bd subcommands
- Update claude_test.go to use new check names (CLI Availability,
  Prime Documentation)
- Fix syncbranch test path comparison by resolving symlinks
  (/var vs /private/var on macOS)
- Fix permissions check to use exact comparison instead of bitmask
- Fix UntrackedJSONL to use git commit --only to preserve staged changes
- Fix MergeDriver edge case test by making both .git dir and config
  read-only
- Add skipIfTestBinary helper for E2E tests that need real bd binary

* test(doctor): skip read-only config test in CI environments

GitHub Actions containers may have CAP_DAC_OVERRIDE or similar
capabilities that allow writing to read-only files, causing
the test to fail. Skip the test when CI=true or GITHUB_ACTIONS=true.
2025-12-20 03:10:06 -08:00

271 lines
6.7 KiB
Go

package main
import (
"fmt"
"sort"
"github.com/charmbracelet/lipgloss"
"github.com/spf13/cobra"
"github.com/steveyegge/beads/internal/ui"
)
// lipgloss styles for the thanks page using Ayu theme
var (
thanksTitleStyle = lipgloss.NewStyle().
Bold(true).
Foreground(ui.ColorWarn)
thanksSubtitleStyle = lipgloss.NewStyle().
Foreground(ui.ColorMuted)
thanksSectionStyle = lipgloss.NewStyle().
Foreground(ui.ColorAccent).
Bold(true)
thanksNameStyle = lipgloss.NewStyle().
Foreground(ui.ColorPass)
thanksLabelStyle = lipgloss.NewStyle().
Foreground(ui.ColorWarn)
thanksDimStyle = lipgloss.NewStyle().
Foreground(ui.ColorMuted)
)
// thanksBoxStyle returns a box style with dynamic width
func thanksBoxStyle(width int) lipgloss.Style {
return lipgloss.NewStyle().
BorderStyle(lipgloss.DoubleBorder()).
BorderForeground(ui.ColorMuted).
Padding(1, 4).
Width(width - 4).
Align(lipgloss.Center)
}
// Static list of human contributors to the beads project.
// To update: run `git shortlog -sn --all` in the beads repo.
// Map of contributor name -> commit count, sorted by contribution count descending.
var beadsContributors = map[string]int{
"Steve Yegge": 2959,
"matt wilkie": 64,
"Ryan Snodgrass": 43,
"Travis Cline": 9,
"David Laing": 7,
"Ryan Newton": 6,
"Joshua Shanks": 6,
"Daan van Etten": 5,
"Augustinas Malinauskas": 4,
"Matteo Landi": 4,
"Baishampayan Ghose": 4,
"Charles P. Cross": 4,
"Abhinav Gupta": 3,
"Brian Williams": 3,
"Marco Del Pin": 3,
"Willi Ballenthin": 3,
"Ben Lovell": 2,
"Ben Madore": 2,
"Dane Bertram": 2,
"Dennis Schön": 2,
"Troy Gaines": 2,
"Zoe Gagnon": 2,
"Peter Schilling": 2,
"Adam Spiers": 1,
"Aodhan Hayter": 1,
"Assim Elhammouti": 1,
"Bryce Roche": 1,
"Caleb Leak": 1,
"David Birks": 1,
"Dean Giberson": 1,
"Eli": 1,
"Graeme Foster": 1,
"Gurdas Nijor": 1,
"Jimmy Stridh": 1,
"Joel Klabo": 1,
"Johannes Zillmann": 1,
"John Lam": 1,
"Jonathan Berger": 1,
"Joshua Park": 1,
"Juan Vargas": 1,
"Kasper Zutterman": 1,
"Kris Hansen": 1,
"Logan Thomas": 1,
"Lon Lundgren": 1,
"Mark Wotton": 1,
"Markus Flür": 1,
"Michael Shuffett": 1,
"Midworld Kim": 1,
"Nikolai Prokoschenko": 1,
"Peter Loron": 1,
"Rod Davenport": 1,
"Serhii": 1,
"Shaun Cutts": 1,
"Sophie Smithburg": 1,
"Tim Haasdyk": 1,
"Travis Lyons": 1,
"Yaakov Nemoy": 1,
"Yunsik Kim": 1,
"Zachary Rosen": 1,
}
var thanksCmd = &cobra.Command{
Use: "thanks",
Short: "Thank the human contributors to beads",
Long: `Display a thank you page listing all human contributors to the beads project.
Examples:
bd thanks # Show thank you page
`,
Run: func(cmd *cobra.Command, args []string) {
printThanksPage()
},
}
// getContributorsSorted returns contributors sorted by commit count descending
func getContributorsSorted() []string {
type kv struct {
name string
commits int
}
var sorted []kv
for name, commits := range beadsContributors {
sorted = append(sorted, kv{name, commits})
}
sort.Slice(sorted, func(i, j int) bool {
return sorted[i].commits > sorted[j].commits
})
names := make([]string, len(sorted))
for i, kv := range sorted {
names[i] = kv.name
}
return names
}
// printThanksPage displays the thank you page
func printThanksPage() {
fmt.Println()
// get sorted contributors and split into top 20 and rest
allContributors := getContributorsSorted()
topN := 20
if topN > len(allContributors) {
topN = len(allContributors)
}
topContributors := allContributors[:topN]
additionalContributors := allContributors[topN:]
// calculate content width based on featured contributors columns
contentWidth := calculateColumnsWidth(topContributors, 4) + 4 // +4 for indent
// build header content with styled text
title := thanksTitleStyle.Render("THANK YOU!")
subtitle := thanksSubtitleStyle.Render("To all the humans who contributed to beads")
header := title + "\n\n" + subtitle
// render header in a bordered box matching content width
fmt.Println(thanksBoxStyle(contentWidth).Render(header))
fmt.Println()
// print featured contributors section
fmt.Println(thanksSectionStyle.Render(" Featured Contributors"))
fmt.Println()
printThanksColumns(topContributors, 4)
// print additional contributors with line wrapping
if len(additionalContributors) > 0 {
fmt.Println()
fmt.Println(thanksSectionStyle.Render(" Additional Contributors"))
fmt.Println()
printThanksWrappedList("", additionalContributors, contentWidth)
}
fmt.Println()
}
// calculateColumnsWidth returns the total width needed for displaying names in columns
func calculateColumnsWidth(names []string, cols int) int {
if len(names) == 0 {
return 0
}
maxWidth := 0
for _, name := range names {
if len(name) > maxWidth {
maxWidth = len(name)
}
}
if maxWidth > 20 {
maxWidth = 20
}
colWidth := maxWidth + 2
return colWidth * cols
}
// printThanksColumns prints names in n columns, sorted horizontally by input order
func printThanksColumns(names []string, cols int) {
if len(names) == 0 {
return
}
// find max width for alignment
maxWidth := 0
for _, name := range names {
if len(name) > maxWidth {
maxWidth = len(name)
}
}
if maxWidth > 20 {
maxWidth = 20
}
colWidth := maxWidth + 2
// print in rows, reading left to right
for i := 0; i < len(names); i += cols {
fmt.Print(" ")
for j := 0; j < cols && i+j < len(names); j++ {
name := names[i+j]
if len(name) > 20 {
name = name[:17] + "..."
}
padded := fmt.Sprintf("%-*s", colWidth, name)
fmt.Print(thanksNameStyle.Render(padded))
}
fmt.Println()
}
}
// printThanksWrappedList prints a list with word wrapping at name boundaries
func printThanksWrappedList(label string, names []string, maxWidth int) {
indent := " "
fmt.Print(indent)
lineLen := len(indent)
if label != "" {
fmt.Print(thanksLabelStyle.Render(label) + " ")
lineLen += len(label) + 1
}
for i, name := range names {
suffix := ", "
if i == len(names)-1 {
suffix = ""
}
entry := name + suffix
if lineLen+len(entry) > maxWidth && lineLen > len(indent) {
fmt.Println()
fmt.Print(indent)
lineLen = len(indent)
}
fmt.Print(thanksDimStyle.Render(entry))
lineLen += len(entry)
}
fmt.Println()
}
func init() {
rootCmd.AddCommand(thanksCmd)
}