feat(doctor): add redirect configuration health checks
Add three new bd doctor checks for redirect configuration health: 1. CheckRedirectTargetValid - Verifies redirect target exists and contains a valid beads database 2. CheckRedirectTargetSyncWorktree - Verifies redirect target has beads-sync worktree when using sync-branch mode 3. CheckNoVestigialSyncWorktrees - Detects unused .beads-sync worktrees in redirected repos that waste space These checks help diagnose redirect configuration issues in multi-clone setups like Gas Town polecats and crew workspaces. Closes bd-b88x3 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -450,6 +450,21 @@ func runDiagnostics(path string) doctorResult {
|
||||
result.Checks = append(result.Checks, redirectTrackingCheck)
|
||||
// Don't fail overall check for redirect tracking, just warn
|
||||
|
||||
// Check 14c: redirect target validity (target exists and has valid db)
|
||||
redirectTargetCheck := convertWithCategory(doctor.CheckRedirectTargetValid(), doctor.CategoryGit)
|
||||
result.Checks = append(result.Checks, redirectTargetCheck)
|
||||
// Don't fail overall check for redirect target, just warn
|
||||
|
||||
// Check 14d: redirect target sync worktree (target has beads-sync if needed)
|
||||
redirectTargetSyncCheck := convertWithCategory(doctor.CheckRedirectTargetSyncWorktree(), doctor.CategoryGit)
|
||||
result.Checks = append(result.Checks, redirectTargetSyncCheck)
|
||||
// Don't fail overall check for redirect target sync, just warn
|
||||
|
||||
// Check 14e: vestigial sync worktrees (unused worktrees in redirected repos)
|
||||
vestigialWorktreesCheck := convertWithCategory(doctor.CheckNoVestigialSyncWorktrees(), doctor.CategoryGit)
|
||||
result.Checks = append(result.Checks, vestigialWorktreesCheck)
|
||||
// Don't fail overall check for vestigial worktrees, just warn
|
||||
|
||||
// Check 15: Git merge driver configuration
|
||||
mergeDriverCheck := convertWithCategory(doctor.CheckMergeDriver(path), doctor.CategoryGit)
|
||||
result.Checks = append(result.Checks, mergeDriverCheck)
|
||||
|
||||
@@ -271,6 +271,255 @@ func FixRedirectTracking() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// CheckRedirectTargetValid verifies that the redirect target exists and has a valid beads database.
|
||||
// This catches cases where the redirect points to a non-existent directory or one without a database.
|
||||
func CheckRedirectTargetValid() DoctorCheck {
|
||||
redirectPath := filepath.Join(".beads", "redirect")
|
||||
|
||||
// Check if redirect file exists
|
||||
data, err := os.ReadFile(redirectPath) // #nosec G304 - path is hardcoded
|
||||
if os.IsNotExist(err) {
|
||||
return DoctorCheck{
|
||||
Name: "Redirect Target Valid",
|
||||
Status: StatusOK,
|
||||
Message: "No redirect configured",
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
return DoctorCheck{
|
||||
Name: "Redirect Target Valid",
|
||||
Status: StatusWarning,
|
||||
Message: "Cannot read redirect file",
|
||||
Detail: err.Error(),
|
||||
}
|
||||
}
|
||||
|
||||
// Parse redirect target
|
||||
target := strings.TrimSpace(string(data))
|
||||
if target == "" {
|
||||
return DoctorCheck{
|
||||
Name: "Redirect Target Valid",
|
||||
Status: StatusWarning,
|
||||
Message: "Redirect file is empty",
|
||||
Fix: "Remove the empty redirect file or add a valid path",
|
||||
}
|
||||
}
|
||||
|
||||
// Resolve the redirect path relative to the parent of .beads
|
||||
cwd, err := os.Getwd()
|
||||
if err != nil {
|
||||
return DoctorCheck{
|
||||
Name: "Redirect Target Valid",
|
||||
Status: StatusWarning,
|
||||
Message: "Cannot determine current directory",
|
||||
}
|
||||
}
|
||||
|
||||
resolvedTarget := filepath.Clean(filepath.Join(cwd, target))
|
||||
|
||||
// Check if target directory exists
|
||||
info, err := os.Stat(resolvedTarget)
|
||||
if os.IsNotExist(err) {
|
||||
return DoctorCheck{
|
||||
Name: "Redirect Target Valid",
|
||||
Status: StatusError,
|
||||
Message: "Redirect target does not exist",
|
||||
Detail: fmt.Sprintf("Target: %s", resolvedTarget),
|
||||
Fix: "Fix the redirect path or create the target directory",
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
return DoctorCheck{
|
||||
Name: "Redirect Target Valid",
|
||||
Status: StatusWarning,
|
||||
Message: "Cannot access redirect target",
|
||||
Detail: err.Error(),
|
||||
}
|
||||
}
|
||||
if !info.IsDir() {
|
||||
return DoctorCheck{
|
||||
Name: "Redirect Target Valid",
|
||||
Status: StatusError,
|
||||
Message: "Redirect target is not a directory",
|
||||
Detail: fmt.Sprintf("Target: %s", resolvedTarget),
|
||||
}
|
||||
}
|
||||
|
||||
// Check for valid beads database in target
|
||||
dbPath := filepath.Join(resolvedTarget, "beads.db")
|
||||
if _, err := os.Stat(dbPath); os.IsNotExist(err) {
|
||||
// Also check for any .db file
|
||||
matches, _ := filepath.Glob(filepath.Join(resolvedTarget, "*.db"))
|
||||
if len(matches) == 0 {
|
||||
return DoctorCheck{
|
||||
Name: "Redirect Target Valid",
|
||||
Status: StatusWarning,
|
||||
Message: "Redirect target has no beads database",
|
||||
Detail: fmt.Sprintf("Target: %s", resolvedTarget),
|
||||
Fix: "Run 'bd init' in the target directory or check redirect path",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return DoctorCheck{
|
||||
Name: "Redirect Target Valid",
|
||||
Status: StatusOK,
|
||||
Message: fmt.Sprintf("Redirect target valid: %s", resolvedTarget),
|
||||
}
|
||||
}
|
||||
|
||||
// CheckRedirectTargetSyncWorktree verifies that the redirect target has a working beads-sync worktree.
|
||||
// This is important for repos using sync-branch mode with redirects.
|
||||
func CheckRedirectTargetSyncWorktree() DoctorCheck {
|
||||
redirectPath := filepath.Join(".beads", "redirect")
|
||||
|
||||
// Check if redirect file exists
|
||||
data, err := os.ReadFile(redirectPath) // #nosec G304 - path is hardcoded
|
||||
if os.IsNotExist(err) {
|
||||
return DoctorCheck{
|
||||
Name: "Redirect Target Sync",
|
||||
Status: StatusOK,
|
||||
Message: "No redirect configured",
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
return DoctorCheck{
|
||||
Name: "Redirect Target Sync",
|
||||
Status: StatusOK, // Don't warn if we can't read - other check handles that
|
||||
Message: "N/A (cannot read redirect)",
|
||||
}
|
||||
}
|
||||
|
||||
target := strings.TrimSpace(string(data))
|
||||
if target == "" {
|
||||
return DoctorCheck{
|
||||
Name: "Redirect Target Sync",
|
||||
Status: StatusOK,
|
||||
Message: "N/A (empty redirect)",
|
||||
}
|
||||
}
|
||||
|
||||
// Resolve the target path
|
||||
cwd, err := os.Getwd()
|
||||
if err != nil {
|
||||
return DoctorCheck{
|
||||
Name: "Redirect Target Sync",
|
||||
Status: StatusOK,
|
||||
Message: "N/A (cannot determine cwd)",
|
||||
}
|
||||
}
|
||||
|
||||
resolvedTarget := filepath.Clean(filepath.Join(cwd, target))
|
||||
|
||||
// Check if the target has a sync-branch configured in config.yaml
|
||||
configPath := filepath.Join(resolvedTarget, "config.yaml")
|
||||
configData, err := os.ReadFile(configPath) // #nosec G304 - constructed from known path
|
||||
if err != nil {
|
||||
// No config.yaml means no sync-branch, which is fine
|
||||
return DoctorCheck{
|
||||
Name: "Redirect Target Sync",
|
||||
Status: StatusOK,
|
||||
Message: "N/A (target not using sync-branch mode)",
|
||||
}
|
||||
}
|
||||
|
||||
// Simple check for sync-branch in config
|
||||
if !strings.Contains(string(configData), "sync-branch:") {
|
||||
return DoctorCheck{
|
||||
Name: "Redirect Target Sync",
|
||||
Status: StatusOK,
|
||||
Message: "N/A (target not using sync-branch mode)",
|
||||
}
|
||||
}
|
||||
|
||||
// Target uses sync-branch - check for beads-sync worktree in the repo containing the target
|
||||
// The target is inside a .beads dir, so the repo is the parent of .beads
|
||||
targetRepoRoot := filepath.Dir(resolvedTarget)
|
||||
|
||||
// Check for beads-sync worktree
|
||||
worktreePath := filepath.Join(targetRepoRoot, ".beads-sync")
|
||||
if _, err := os.Stat(worktreePath); os.IsNotExist(err) {
|
||||
return DoctorCheck{
|
||||
Name: "Redirect Target Sync",
|
||||
Status: StatusWarning,
|
||||
Message: "Redirect target missing beads-sync worktree",
|
||||
Detail: fmt.Sprintf("Expected worktree at: %s", worktreePath),
|
||||
Fix: fmt.Sprintf("Run 'bd sync' in %s to create the worktree", targetRepoRoot),
|
||||
}
|
||||
}
|
||||
|
||||
return DoctorCheck{
|
||||
Name: "Redirect Target Sync",
|
||||
Status: StatusOK,
|
||||
Message: "Redirect target has beads-sync worktree",
|
||||
}
|
||||
}
|
||||
|
||||
// CheckNoVestigialSyncWorktrees detects beads-sync worktrees in redirected repos that are unused.
|
||||
// When a repo uses .beads/redirect, it doesn't need its own beads-sync worktree since
|
||||
// sync operations happen in the redirect target. These vestigial worktrees waste space.
|
||||
func CheckNoVestigialSyncWorktrees() DoctorCheck {
|
||||
redirectPath := filepath.Join(".beads", "redirect")
|
||||
|
||||
// Check if redirect file exists
|
||||
if _, err := os.Stat(redirectPath); os.IsNotExist(err) {
|
||||
// No redirect - this check doesn't apply
|
||||
return DoctorCheck{
|
||||
Name: "Vestigial Sync Worktrees",
|
||||
Status: StatusOK,
|
||||
Message: "N/A (no redirect configured)",
|
||||
}
|
||||
}
|
||||
|
||||
// Check for local .beads-sync worktree
|
||||
cwd, err := os.Getwd()
|
||||
if err != nil {
|
||||
return DoctorCheck{
|
||||
Name: "Vestigial Sync Worktrees",
|
||||
Status: StatusOK,
|
||||
Message: "N/A (cannot determine cwd)",
|
||||
}
|
||||
}
|
||||
|
||||
// Walk up to find git root
|
||||
gitRoot := cwd
|
||||
for {
|
||||
if _, err := os.Stat(filepath.Join(gitRoot, ".git")); err == nil {
|
||||
break
|
||||
}
|
||||
parent := filepath.Dir(gitRoot)
|
||||
if parent == gitRoot {
|
||||
// Reached filesystem root, not in a git repo
|
||||
return DoctorCheck{
|
||||
Name: "Vestigial Sync Worktrees",
|
||||
Status: StatusOK,
|
||||
Message: "N/A (not in git repository)",
|
||||
}
|
||||
}
|
||||
gitRoot = parent
|
||||
}
|
||||
|
||||
// Check for .beads-sync worktree
|
||||
syncWorktreePath := filepath.Join(gitRoot, ".beads-sync")
|
||||
if _, err := os.Stat(syncWorktreePath); os.IsNotExist(err) {
|
||||
// No local worktree - good
|
||||
return DoctorCheck{
|
||||
Name: "Vestigial Sync Worktrees",
|
||||
Status: StatusOK,
|
||||
Message: "No vestigial sync worktrees found",
|
||||
}
|
||||
}
|
||||
|
||||
// Found a local .beads-sync but we have a redirect - this is vestigial
|
||||
return DoctorCheck{
|
||||
Name: "Vestigial Sync Worktrees",
|
||||
Status: StatusWarning,
|
||||
Message: "Vestigial .beads-sync worktree found",
|
||||
Detail: fmt.Sprintf("This repo uses redirect but has unused worktree at: %s", syncWorktreePath),
|
||||
Fix: fmt.Sprintf("Remove with: rm -rf %s", syncWorktreePath),
|
||||
}
|
||||
}
|
||||
|
||||
// CheckSyncBranchGitignore checks if git index flags are set on issues.jsonl when sync.branch is configured.
|
||||
// Without these flags, the file appears modified in git status even though changes go to the sync branch.
|
||||
// GH#797, GH#801, GH#870.
|
||||
|
||||
Reference in New Issue
Block a user