fix(sync): handle redirect + sync-branch incompatibility (bd-wayc3)
When a crew worker's .beads/ is redirected to another repo, bd sync now detects this and skips all git operations (sync-branch worktree manipulation). Instead, it just exports to JSONL and lets the target repo's owner handle the git sync. Changes: - sync.go: Detect redirect early, skip git operations when active - beads.go: Update GetRedirectInfo() to check git repo even when BEADS_DIR is pre-set (findLocalBdsDirInRepo helper) - validation.go: Add doctor check for redirect + sync-branch conflict - doctor.go: Register new check, remove undefined CheckMisclassifiedWisps Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> Executed-By: beads/crew/dave Rig: beads Role: crew
This commit is contained in:
committed by
Steve Yegge
parent
ff67b88ea9
commit
3298c45e4b
2526
.beads/issues.jsonl
2526
.beads/issues.jsonl
File diff suppressed because one or more lines are too long
@@ -465,7 +465,12 @@ func runDiagnostics(path string) doctorResult {
|
|||||||
result.Checks = append(result.Checks, vestigialWorktreesCheck)
|
result.Checks = append(result.Checks, vestigialWorktreesCheck)
|
||||||
// Don't fail overall check for vestigial worktrees, just warn
|
// Don't fail overall check for vestigial worktrees, just warn
|
||||||
|
|
||||||
// Check 14f: last-touched file tracking (runtime state shouldn't be committed)
|
// Check 14f: redirect + sync-branch conflict (bd-wayc3)
|
||||||
|
redirectSyncBranchCheck := convertDoctorCheck(doctor.CheckRedirectSyncBranchConflict(path))
|
||||||
|
result.Checks = append(result.Checks, redirectSyncBranchCheck)
|
||||||
|
// Don't fail overall check for redirect+sync-branch conflict, just warn
|
||||||
|
|
||||||
|
// Check 14g: last-touched file tracking (runtime state shouldn't be committed)
|
||||||
lastTouchedTrackingCheck := convertWithCategory(doctor.CheckLastTouchedNotTracked(), doctor.CategoryGit)
|
lastTouchedTrackingCheck := convertWithCategory(doctor.CheckLastTouchedNotTracked(), doctor.CategoryGit)
|
||||||
result.Checks = append(result.Checks, lastTouchedTrackingCheck)
|
result.Checks = append(result.Checks, lastTouchedTrackingCheck)
|
||||||
// Don't fail overall check for last-touched tracking, just warn
|
// Don't fail overall check for last-touched tracking, just warn
|
||||||
@@ -577,10 +582,9 @@ func runDiagnostics(path string) doctorResult {
|
|||||||
result.Checks = append(result.Checks, staleMQFilesCheck)
|
result.Checks = append(result.Checks, staleMQFilesCheck)
|
||||||
// Don't fail overall check for legacy MQ files, just warn
|
// Don't fail overall check for legacy MQ files, just warn
|
||||||
|
|
||||||
// Check 26d: Misclassified wisps (wisp-patterned IDs without ephemeral flag)
|
// Note: Check 26d (misclassified wisps) was referenced but never implemented.
|
||||||
misclassifiedWispsCheck := convertDoctorCheck(doctor.CheckMisclassifiedWisps(path))
|
// The commit f703237c added importer-based auto-detection instead.
|
||||||
result.Checks = append(result.Checks, misclassifiedWispsCheck)
|
// Removing the undefined reference to fix build.
|
||||||
// Don't fail overall check for misclassified wisps, just warn
|
|
||||||
|
|
||||||
// Check 27: Expired tombstones (maintenance)
|
// Check 27: Expired tombstones (maintenance)
|
||||||
tombstonesExpiredCheck := convertDoctorCheck(doctor.CheckExpiredTombstones(path))
|
tombstonesExpiredCheck := convertDoctorCheck(doctor.CheckExpiredTombstones(path))
|
||||||
|
|||||||
@@ -391,6 +391,79 @@ func CheckChildParentDependencies(path string) DoctorCheck {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CheckRedirectSyncBranchConflict detects when both redirect and sync-branch are configured.
|
||||||
|
// This is a configuration error: redirect means "my database is elsewhere (I'm a client)",
|
||||||
|
// while sync-branch means "I own my database and sync it myself". These are mutually exclusive.
|
||||||
|
// bd-wayc3: Added to detect incompatible configuration before sync fails.
|
||||||
|
func CheckRedirectSyncBranchConflict(path string) DoctorCheck {
|
||||||
|
beadsDir := filepath.Join(path, ".beads")
|
||||||
|
|
||||||
|
// Check if redirect file exists
|
||||||
|
redirectFile := filepath.Join(beadsDir, beads.RedirectFileName)
|
||||||
|
if _, err := os.Stat(redirectFile); os.IsNotExist(err) {
|
||||||
|
return DoctorCheck{
|
||||||
|
Name: "Redirect + Sync-Branch",
|
||||||
|
Status: StatusOK,
|
||||||
|
Message: "No redirect configured",
|
||||||
|
Category: CategoryData,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Redirect exists - check if sync-branch is also configured
|
||||||
|
// Read config.yaml directly since we need to check the local config, not the resolved one
|
||||||
|
configPath := filepath.Join(beadsDir, "config.yaml")
|
||||||
|
data, err := os.ReadFile(configPath) // #nosec G304 - path constructed safely
|
||||||
|
if err != nil {
|
||||||
|
// No config file - no conflict possible
|
||||||
|
return DoctorCheck{
|
||||||
|
Name: "Redirect + Sync-Branch",
|
||||||
|
Status: StatusOK,
|
||||||
|
Message: "Redirect active (no local config)",
|
||||||
|
Category: CategoryData,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse sync-branch from config.yaml (simple line-based parsing)
|
||||||
|
// Handles: sync-branch: value, sync-branch: "value", sync-branch: 'value'
|
||||||
|
// Also handles trailing comments: sync-branch: value # comment
|
||||||
|
configStr := string(data)
|
||||||
|
for _, line := range strings.Split(configStr, "\n") {
|
||||||
|
line = strings.TrimSpace(line)
|
||||||
|
// Skip comments
|
||||||
|
if strings.HasPrefix(line, "#") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if strings.HasPrefix(line, "sync-branch:") {
|
||||||
|
value := strings.TrimPrefix(line, "sync-branch:")
|
||||||
|
// Remove trailing comment if present
|
||||||
|
if idx := strings.Index(value, "#"); idx != -1 {
|
||||||
|
value = value[:idx]
|
||||||
|
}
|
||||||
|
value = strings.TrimSpace(value)
|
||||||
|
// Remove quotes if present
|
||||||
|
value = strings.Trim(value, `"'`)
|
||||||
|
if value != "" {
|
||||||
|
// Found both redirect and sync-branch - conflict!
|
||||||
|
return DoctorCheck{
|
||||||
|
Name: "Redirect + Sync-Branch",
|
||||||
|
Status: StatusWarning,
|
||||||
|
Message: fmt.Sprintf("Redirect active but sync-branch=%q configured", value),
|
||||||
|
Detail: "Redirect and sync-branch are mutually exclusive. Redirected clones should not have sync-branch.",
|
||||||
|
Fix: "Remove sync-branch from config.yaml (set to empty string or delete the line)",
|
||||||
|
Category: CategoryData,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return DoctorCheck{
|
||||||
|
Name: "Redirect + Sync-Branch",
|
||||||
|
Status: StatusOK,
|
||||||
|
Message: "Redirect active (no sync-branch conflict)",
|
||||||
|
Category: CategoryData,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// CheckGitConflicts detects git conflict markers in JSONL file.
|
// CheckGitConflicts detects git conflict markers in JSONL file.
|
||||||
func CheckGitConflicts(path string) DoctorCheck {
|
func CheckGitConflicts(path string) DoctorCheck {
|
||||||
// Follow redirect to resolve actual beads directory (bd-tvus fix)
|
// Follow redirect to resolve actual beads directory (bd-tvus fix)
|
||||||
|
|||||||
@@ -208,6 +208,36 @@ Use --merge to merge the sync branch back to main branch.`,
|
|||||||
}
|
}
|
||||||
hasSyncBranchConfig := syncBranchName != ""
|
hasSyncBranchConfig := syncBranchName != ""
|
||||||
|
|
||||||
|
// bd-wayc3: Check for redirect + sync-branch incompatibility
|
||||||
|
// Redirect and sync-branch are mutually exclusive:
|
||||||
|
// - Redirect says: "My database is in another repo (I am a client)"
|
||||||
|
// - Sync-branch says: "I own my database and sync it myself via worktree"
|
||||||
|
// When redirect is active, the sync-branch worktree operations fail because
|
||||||
|
// the beads files are in a different git repo than the current working directory.
|
||||||
|
redirectInfo := beads.GetRedirectInfo()
|
||||||
|
if redirectInfo.IsRedirected {
|
||||||
|
if hasSyncBranchConfig {
|
||||||
|
fmt.Printf("⚠️ Redirect active (-> %s), skipping sync-branch operations\n", redirectInfo.TargetDir)
|
||||||
|
fmt.Println(" Hint: Redirected clones should not have sync-branch configured")
|
||||||
|
fmt.Println(" The owner of the target .beads directory handles sync-branch")
|
||||||
|
} else {
|
||||||
|
fmt.Printf("→ Redirect active (-> %s)\n", redirectInfo.TargetDir)
|
||||||
|
}
|
||||||
|
// For redirected clones, just do import/export - skip all git operations
|
||||||
|
// The target repo's owner (e.g., mayor) handles git commit/push via sync-branch
|
||||||
|
if dryRun {
|
||||||
|
fmt.Println("→ [DRY RUN] Would export to JSONL (redirected clone, git operations skipped)")
|
||||||
|
fmt.Println("✓ Dry run complete (no changes made)")
|
||||||
|
} else {
|
||||||
|
fmt.Println("→ Exporting to JSONL (redirected clone, skipping git operations)...")
|
||||||
|
if err := exportToJSONL(ctx, jsonlPath); err != nil {
|
||||||
|
FatalError("exporting: %v", err)
|
||||||
|
}
|
||||||
|
fmt.Println("✓ Export complete (target repo owner handles git sync)")
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// Preflight: check for upstream tracking
|
// Preflight: check for upstream tracking
|
||||||
// If no upstream, automatically switch to --from-main mode (gt-ick9: ephemeral branch support)
|
// If no upstream, automatically switch to --from-main mode (gt-ick9: ephemeral branch support)
|
||||||
// GH#638: Skip this fallback if sync.branch is explicitly configured
|
// GH#638: Skip this fallback if sync.branch is explicitly configured
|
||||||
|
|||||||
@@ -106,26 +106,42 @@ type RedirectInfo struct {
|
|||||||
// GetRedirectInfo checks if the current beads directory is redirected.
|
// GetRedirectInfo checks if the current beads directory is redirected.
|
||||||
// It searches for the local .beads/ directory and checks if it contains a redirect file.
|
// It searches for the local .beads/ directory and checks if it contains a redirect file.
|
||||||
// Returns RedirectInfo with IsRedirected=true if a redirect is active.
|
// Returns RedirectInfo with IsRedirected=true if a redirect is active.
|
||||||
|
//
|
||||||
|
// bd-wayc3: This function now also checks the git repo's local .beads directory even when
|
||||||
|
// BEADS_DIR is set. This handles the case where BEADS_DIR is pre-set to the redirect target
|
||||||
|
// (e.g., by shell environment or tooling), but we still need to detect that a redirect exists.
|
||||||
func GetRedirectInfo() RedirectInfo {
|
func GetRedirectInfo() RedirectInfo {
|
||||||
info := RedirectInfo{}
|
// First, always check the git repo's local .beads directory for redirects
|
||||||
|
// This handles the case where BEADS_DIR is pre-set to the redirect target
|
||||||
// Find the local .beads directory without following redirects
|
if localBeadsDir := findLocalBdsDirInRepo(); localBeadsDir != "" {
|
||||||
localBeadsDir := findLocalBeadsDir()
|
if info := checkRedirectInDir(localBeadsDir); info.IsRedirected {
|
||||||
if localBeadsDir == "" {
|
return info
|
||||||
return info
|
}
|
||||||
}
|
}
|
||||||
info.LocalDir = localBeadsDir
|
|
||||||
|
// Fall back to original logic for non-git-repo cases
|
||||||
|
if localBeadsDir := findLocalBeadsDir(); localBeadsDir != "" {
|
||||||
|
return checkRedirectInDir(localBeadsDir)
|
||||||
|
}
|
||||||
|
|
||||||
|
return RedirectInfo{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// checkRedirectInDir checks if a beads directory has a redirect file and returns redirect info.
|
||||||
|
// Returns RedirectInfo with IsRedirected=true if a valid redirect exists.
|
||||||
|
func checkRedirectInDir(beadsDir string) RedirectInfo {
|
||||||
|
info := RedirectInfo{LocalDir: beadsDir}
|
||||||
|
|
||||||
// Check if this directory has a redirect file
|
// Check if this directory has a redirect file
|
||||||
redirectFile := filepath.Join(localBeadsDir, RedirectFileName)
|
redirectFile := filepath.Join(beadsDir, RedirectFileName)
|
||||||
if _, err := os.Stat(redirectFile); err != nil {
|
if _, err := os.Stat(redirectFile); err != nil {
|
||||||
// No redirect file
|
// No redirect file
|
||||||
return info
|
return info
|
||||||
}
|
}
|
||||||
|
|
||||||
// There's a redirect - find the target
|
// There's a redirect - find the target
|
||||||
targetDir := FollowRedirect(localBeadsDir)
|
targetDir := FollowRedirect(beadsDir)
|
||||||
if targetDir == localBeadsDir {
|
if targetDir == beadsDir {
|
||||||
// Redirect file exists but failed to resolve (invalid target)
|
// Redirect file exists but failed to resolve (invalid target)
|
||||||
return info
|
return info
|
||||||
}
|
}
|
||||||
@@ -135,6 +151,24 @@ func GetRedirectInfo() RedirectInfo {
|
|||||||
return info
|
return info
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// findLocalBdsDirInRepo finds the .beads directory relative to the git repo root.
|
||||||
|
// This ignores BEADS_DIR to find the "true local" .beads for redirect detection.
|
||||||
|
// bd-wayc3: Added to detect redirects even when BEADS_DIR is pre-set.
|
||||||
|
func findLocalBdsDirInRepo() string {
|
||||||
|
// Get git repo root
|
||||||
|
repoRoot := git.GetRepoRoot()
|
||||||
|
if repoRoot == "" {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
beadsDir := filepath.Join(repoRoot, ".beads")
|
||||||
|
if info, err := os.Stat(beadsDir); err == nil && info.IsDir() {
|
||||||
|
return beadsDir
|
||||||
|
}
|
||||||
|
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
// findLocalBeadsDir finds the local .beads directory without following redirects.
|
// findLocalBeadsDir finds the local .beads directory without following redirects.
|
||||||
// This is used to detect if a redirect is configured.
|
// This is used to detect if a redirect is configured.
|
||||||
func findLocalBeadsDir() string {
|
func findLocalBeadsDir() string {
|
||||||
|
|||||||
Reference in New Issue
Block a user