fix(sync): use sync-branch worktree for --full --no-pull (#1183)

* fix(sync): use sync-branch worktree for --full --no-pull (#1173)

Bug 1: PullFromSyncBranch was copying uncommitted worktree changes to
main repo when remoteAhead==0. This corrupted the 3-way merge because
local changes appeared as remote changes. Fixed by copying only the
committed state from HEAD instead of the working directory.

Bug 2: doExportOnlySync was checking main repo for changes via
gitHasBeadsChanges, but when sync-branch is configured, changes go to
the worktree, not main. Fixed by detecting sync-branch config and using
CommitToSyncBranch which operates on the worktree.

Fixes #1173

* refactor(sync): consolidate sync-branch detection and commit/push logic

Extract repeated patterns into reusable helpers:

- SyncBranchContext struct: holds branch name and repo root
- getSyncBranchContext(): detects sync-branch config from store
- commitAndPushBeads(): handles both sync-branch and regular git workflows

This eliminates duplicated sync-branch detection code (was in 3 places)
and the duplicated commit/push conditional logic (was in 2 places).

Net reduction of ~20 lines while improving maintainability.

* fix: remove unused bool return from commitAndPushBeads
This commit is contained in:
John Zila
2026-01-20 16:06:22 -06:00
committed by GitHub
parent d929c8f974
commit f336e669e9
2 changed files with 127 additions and 83 deletions

View File

@@ -272,8 +272,12 @@ func PullFromSyncBranch(ctx context.Context, repoRoot, syncBranch, jsonlPath str
// Case 1: Already up to date (remote has nothing new)
if remoteAhead == 0 {
result.Pulled = true
// Still copy JSONL in case worktree has uncommitted changes
if err := copyJSONLToMainRepo(worktreePath, jsonlRelPath, jsonlPath); err != nil {
// GH#1173: Do NOT copy uncommitted worktree changes to main repo.
// The worktree may have uncommitted changes from previous exports that
// haven't been committed yet. Copying those to main would make local
// data appear as "remote" data, corrupting the 3-way merge.
// Instead, copy only the COMMITTED state from the worktree.
if err := copyCommittedJSONLToMainRepo(ctx, worktreePath, jsonlRelPath, jsonlPath); err != nil {
return nil, err
}
return result, nil
@@ -655,6 +659,35 @@ func extractJSONLFromCommit(ctx context.Context, worktreePath, commit, filePath
return output, nil
}
// copyCommittedJSONLToMainRepo copies the COMMITTED JSONL from worktree to main repo.
// GH#1173: This extracts the file from HEAD rather than the working directory,
// ensuring uncommitted local changes don't corrupt the 3-way merge.
func copyCommittedJSONLToMainRepo(ctx context.Context, worktreePath, jsonlRelPath, jsonlPath string) error {
// GH#785: Handle bare repo worktrees
normalizedRelPath := normalizeBeadsRelPath(jsonlRelPath)
// Extract the committed JSONL from HEAD
data, err := extractJSONLFromCommit(ctx, worktreePath, "HEAD", normalizedRelPath)
if err != nil {
// File might not exist in HEAD yet (first sync), nothing to copy
return nil
}
if err := os.WriteFile(jsonlPath, data, 0600); err != nil {
return fmt.Errorf("failed to write main JSONL: %w", err)
}
// Also copy committed metadata.json if it exists
beadsDir := filepath.Dir(jsonlPath)
metadataRelPath := filepath.Join(filepath.Dir(normalizedRelPath), "metadata.json")
if metaData, err := extractJSONLFromCommit(ctx, worktreePath, "HEAD", metadataRelPath); err == nil {
dstPath := filepath.Join(beadsDir, "metadata.json")
_ = os.WriteFile(dstPath, metaData, 0600) // Best effort
}
return nil
}
// copyJSONLToMainRepo copies JSONL and related files from worktree to main repo.
func copyJSONLToMainRepo(worktreePath, jsonlRelPath, jsonlPath string) error {
// GH#785: Handle bare repo worktrees where jsonlRelPath might include the