Import beads' UX design system into gastown: - Add internal/ui/ package with Ayu theme colors and semantic styling - styles.go: AdaptiveColor definitions for light/dark mode - terminal.go: TTY detection, NO_COLOR/CLICOLOR support - markdown.go: Glamour rendering with agent mode bypass - pager.go: Smart paging with GT_PAGER support - Add colorized help output (internal/cmd/help.go) - Group headers in accent color - Command names styled for scannability - Flag types and defaults muted - Add gt thanks command (internal/cmd/thanks.go) - Contributor display with same logic as bd thanks - Styled with Ayu theme colors - Update gt doctor to match bd doctor UX - Category grouping (Core, Infrastructure, Rig, Patrol, etc.) - Semantic icons (✓ ⚠ ✖) with Ayu colors - Tree connectors for detail lines - Summary line with pass/warn/fail counts - Warnings section at end with numbered issues - Migrate existing styles to use ui package - internal/style/style.go uses ui.ColorPass etc. - internal/tui/feed/styles.go uses ui package colors Co-Authored-By: SageOx <ox@sageox.ai>
163 lines
4.5 KiB
Go
163 lines
4.5 KiB
Go
package doctor
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
)
|
|
|
|
// PreCheckoutHookCheck verifies that the pre-checkout hook is installed in the
|
|
// town root to prevent accidental branch switches.
|
|
type PreCheckoutHookCheck struct {
|
|
FixableCheck
|
|
hookMissing bool // Cached during Run for use in Fix
|
|
}
|
|
|
|
// NewPreCheckoutHookCheck creates a new pre-checkout hook check.
|
|
func NewPreCheckoutHookCheck() *PreCheckoutHookCheck {
|
|
return &PreCheckoutHookCheck{
|
|
FixableCheck: FixableCheck{
|
|
BaseCheck: BaseCheck{
|
|
CheckName: "pre-checkout-hook",
|
|
CheckDescription: "Verify pre-checkout hook prevents branch switches",
|
|
CheckCategory: CategoryHooks,
|
|
},
|
|
},
|
|
}
|
|
}
|
|
|
|
// PreCheckoutHookScript is the expected content marker for our hook.
|
|
const preCheckoutHookMarker = "Gas Town pre-checkout hook"
|
|
|
|
// preCheckoutHookScript is the full hook script content.
|
|
// This matches the script in cmd/gitinit.go.
|
|
const preCheckoutHookScript = `#!/bin/bash
|
|
# Gas Town pre-checkout hook
|
|
# Prevents accidental branch switches in the town root (HQ).
|
|
# The town root must stay on main to avoid breaking gt commands.
|
|
|
|
# Only check branch checkouts (not file checkouts)
|
|
# $3 is 1 for file checkout, 0 for branch checkout
|
|
if [ "$3" = "1" ]; then
|
|
exit 0
|
|
fi
|
|
|
|
# Get the target branch name
|
|
TARGET_BRANCH=$(git rev-parse --abbrev-ref "$2" 2>/dev/null)
|
|
|
|
# Allow checkout to main or master
|
|
if [ "$TARGET_BRANCH" = "main" ] || [ "$TARGET_BRANCH" = "master" ]; then
|
|
exit 0
|
|
fi
|
|
|
|
# Get current branch
|
|
CURRENT_BRANCH=$(git branch --show-current)
|
|
|
|
# If already not on main, allow (might be fixing the situation)
|
|
if [ "$CURRENT_BRANCH" != "main" ] && [ "$CURRENT_BRANCH" != "master" ]; then
|
|
exit 0
|
|
fi
|
|
|
|
# Block the checkout with a warning
|
|
echo ""
|
|
echo "⚠️ BLOCKED: Town root must stay on main branch"
|
|
echo ""
|
|
echo " You're trying to switch from '$CURRENT_BRANCH' to '$TARGET_BRANCH'"
|
|
echo " in the Gas Town HQ directory."
|
|
echo ""
|
|
echo " The town root (~/gt) should always be on main. Switching branches"
|
|
echo " can break gt commands (missing rigs.json, wrong configs, etc.)."
|
|
echo ""
|
|
echo " If you really need to switch branches, you can:"
|
|
echo " 1. Temporarily rename .git/hooks/pre-checkout"
|
|
echo " 2. Do your work"
|
|
echo " 3. Switch back to main"
|
|
echo " 4. Restore the hook"
|
|
echo ""
|
|
exit 1
|
|
`
|
|
|
|
// Run checks if the pre-checkout hook is installed.
|
|
func (c *PreCheckoutHookCheck) Run(ctx *CheckContext) *CheckResult {
|
|
gitDir := filepath.Join(ctx.TownRoot, ".git")
|
|
|
|
// Check if town root is a git repo
|
|
if _, err := os.Stat(gitDir); os.IsNotExist(err) {
|
|
return &CheckResult{
|
|
Name: c.Name(),
|
|
Status: StatusOK,
|
|
Message: "Town root is not a git repository (skipped)",
|
|
}
|
|
}
|
|
|
|
hookPath := filepath.Join(gitDir, "hooks", "pre-checkout")
|
|
|
|
// Check if hook exists
|
|
content, err := os.ReadFile(hookPath)
|
|
if os.IsNotExist(err) {
|
|
c.hookMissing = true
|
|
return &CheckResult{
|
|
Name: c.Name(),
|
|
Status: StatusWarning,
|
|
Message: "Pre-checkout hook not installed",
|
|
Details: []string{
|
|
"The pre-checkout hook prevents accidental branch switches in the town root",
|
|
"Without it, a git checkout in ~/gt could switch to a polecat branch",
|
|
"This can break gt commands (missing rigs.json, wrong configs)",
|
|
},
|
|
FixHint: "Run 'gt doctor --fix' or 'gt git-init' to install the hook",
|
|
}
|
|
}
|
|
|
|
if err != nil {
|
|
return &CheckResult{
|
|
Name: c.Name(),
|
|
Status: StatusError,
|
|
Message: fmt.Sprintf("Failed to read pre-checkout hook: %v", err),
|
|
}
|
|
}
|
|
|
|
// Check if it's our hook
|
|
if !strings.Contains(string(content), preCheckoutHookMarker) {
|
|
return &CheckResult{
|
|
Name: c.Name(),
|
|
Status: StatusWarning,
|
|
Message: "Pre-checkout hook exists but is not Gas Town's",
|
|
Details: []string{
|
|
"A pre-checkout hook exists but doesn't contain the Gas Town marker",
|
|
"Consider adding branch protection manually or replacing it",
|
|
},
|
|
}
|
|
}
|
|
|
|
return &CheckResult{
|
|
Name: c.Name(),
|
|
Status: StatusOK,
|
|
Message: "Pre-checkout hook installed",
|
|
}
|
|
}
|
|
|
|
// Fix installs the pre-checkout hook.
|
|
func (c *PreCheckoutHookCheck) Fix(ctx *CheckContext) error {
|
|
if !c.hookMissing {
|
|
return nil
|
|
}
|
|
|
|
hooksDir := filepath.Join(ctx.TownRoot, ".git", "hooks")
|
|
|
|
// Ensure hooks directory exists
|
|
if err := os.MkdirAll(hooksDir, 0755); err != nil {
|
|
return fmt.Errorf("creating hooks directory: %w", err)
|
|
}
|
|
|
|
hookPath := filepath.Join(hooksDir, "pre-checkout")
|
|
|
|
// Install the hook
|
|
if err := os.WriteFile(hookPath, []byte(preCheckoutHookScript), 0755); err != nil {
|
|
return fmt.Errorf("writing hook: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|