feat: detect uncommitted JSONL changes before sync (GH#885, bd-vd8e)
Add pre-flight safety check to detect when a previous sync exported but failed before commit, leaving JSONL in an inconsistent state. - Add gitHasUncommittedBeadsChanges() helper in sync_git.go - Call in sync pre-flight checks after merge/rebase check - If uncommitted changes detected, force re-export to reconcile state This catches the failure mode early before it compounds across worktrees. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
committed by
Steve Yegge
parent
7b90678afe
commit
44a5c3a0ec
@@ -163,6 +163,21 @@ Use --merge to merge the sync branch back to main branch.`,
|
|||||||
FatalErrorWithHint("unmerged paths or merge in progress", "resolve conflicts, run 'bd import' if needed, then 'bd sync' again")
|
FatalErrorWithHint("unmerged paths or merge in progress", "resolve conflicts, run 'bd import' if needed, then 'bd sync' again")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GH#885: Preflight check for uncommitted JSONL changes
|
||||||
|
// This detects when a previous sync exported but failed before commit,
|
||||||
|
// leaving the JSONL in an inconsistent state across worktrees.
|
||||||
|
if hasUncommitted, err := gitHasUncommittedBeadsChanges(ctx); err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "Warning: failed to check for uncommitted changes: %v\n", err)
|
||||||
|
} else if hasUncommitted {
|
||||||
|
fmt.Println("→ Detected uncommitted JSONL changes (possible incomplete sync)")
|
||||||
|
fmt.Println("→ Re-exporting from database to reconcile state...")
|
||||||
|
// Force a fresh export to ensure JSONL matches current DB state
|
||||||
|
if err := exportToJSONL(ctx, jsonlPath); err != nil {
|
||||||
|
FatalError("re-exporting to reconcile state: %v", err)
|
||||||
|
}
|
||||||
|
fmt.Println("✓ State reconciled")
|
||||||
|
}
|
||||||
|
|
||||||
// GH#638: Check sync.branch BEFORE upstream check
|
// GH#638: Check sync.branch BEFORE upstream check
|
||||||
// When sync.branch is configured, we should use worktree-based sync even if
|
// When sync.branch is configured, we should use worktree-based sync even if
|
||||||
// the current branch has no upstream (e.g., detached HEAD in jj, git worktrees)
|
// the current branch has no upstream (e.g., detached HEAD in jj, git worktrees)
|
||||||
|
|||||||
@@ -447,6 +447,45 @@ func restoreBeadsDirFromBranch(ctx context.Context) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// gitHasUncommittedBeadsChanges checks if .beads/issues.jsonl has uncommitted changes.
|
||||||
|
// This detects the failure mode where a previous sync exported but failed before commit.
|
||||||
|
// Returns true if the JSONL file has staged or unstaged changes (M or A status).
|
||||||
|
// GH#885: Pre-flight safety check to detect incomplete sync operations.
|
||||||
|
func gitHasUncommittedBeadsChanges(ctx context.Context) (bool, error) {
|
||||||
|
beadsDir := beads.FindBeadsDir()
|
||||||
|
if beadsDir == "" {
|
||||||
|
return false, nil // No beads dir, nothing to check
|
||||||
|
}
|
||||||
|
|
||||||
|
jsonlPath := filepath.Join(beadsDir, "issues.jsonl")
|
||||||
|
|
||||||
|
// Check git status for the JSONL file specifically
|
||||||
|
cmd := exec.CommandContext(ctx, "git", "status", "--porcelain", jsonlPath) //nolint:gosec // G204: jsonlPath from internal beads.FindBeadsDir()
|
||||||
|
output, err := cmd.Output()
|
||||||
|
if err != nil {
|
||||||
|
return false, fmt.Errorf("git status failed: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse status output - look for modified/added files
|
||||||
|
// Format: XY filename where X=staged, Y=unstaged
|
||||||
|
// M = modified, A = added, ? = untracked
|
||||||
|
statusLine := strings.TrimSpace(string(output))
|
||||||
|
if statusLine == "" {
|
||||||
|
return false, nil // No changes
|
||||||
|
}
|
||||||
|
|
||||||
|
// Any status (M, A, MM, AM, etc.) indicates uncommitted changes
|
||||||
|
if len(statusLine) >= 2 {
|
||||||
|
x, y := statusLine[0], statusLine[1]
|
||||||
|
// Check for modifications (staged or unstaged)
|
||||||
|
if x == 'M' || x == 'A' || y == 'M' || y == 'A' {
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
|
||||||
// getDefaultBranch returns the default branch name (main or master) for origin remote
|
// getDefaultBranch returns the default branch name (main or master) for origin remote
|
||||||
// Checks remote HEAD first, then falls back to checking if main/master exist
|
// Checks remote HEAD first, then falls back to checking if main/master exist
|
||||||
func getDefaultBranch(ctx context.Context) string {
|
func getDefaultBranch(ctx context.Context) string {
|
||||||
|
|||||||
Reference in New Issue
Block a user