Bug
On first sync of a large repo with an active stash watch on another machine, sync fails with "Remote main moved during push" and exhausts all 5 retries.
Cause
First sync has no local snapshot, so fetch() downloads all blob metadata and file content via GraphQL + raw API calls. This makes the window between fetch() and push() very wide. An active watcher consistently lands a commit during this window. Each retry repeats the full expensive fetch, so the window stays wide across all attempts.
Subsequent syncs are unaffected — they diff against the local snapshot and only fetch the delta, keeping the window small. If a normal sync does hit this, the next attempt self-heals.
Proposed fix
Split first sync (empty local snapshot) into two passes:
Pass 1 — pull only:
fetch() — download remote state (expensive, but no push follows)
reconcile() — compute mutations
- Skip push — don't push snapshot.json yet
apply() — write remote files to local disk
saveSnapshot() — write local snapshot only
Pass 2 — push snapshot (immediate, automatic):
fetch() — fast, local snapshot now matches remote, most files skip
reconcile() — only snapshot.json needs pushing
push() — tiny payload, tiny contention window
saveSnapshot() — update local snapshot
Only triggers when local snapshot is empty at sync start. All other syncs unchanged.
Bug
On first sync of a large repo with an active
stash watchon another machine, sync fails with "Remote main moved during push" and exhausts all 5 retries.Cause
First sync has no local snapshot, so
fetch()downloads all blob metadata and file content via GraphQL + raw API calls. This makes the window betweenfetch()andpush()very wide. An active watcher consistently lands a commit during this window. Each retry repeats the full expensive fetch, so the window stays wide across all attempts.Subsequent syncs are unaffected — they diff against the local snapshot and only fetch the delta, keeping the window small. If a normal sync does hit this, the next attempt self-heals.
Proposed fix
Split first sync (empty local snapshot) into two passes:
Pass 1 — pull only:
fetch()— download remote state (expensive, but no push follows)reconcile()— compute mutationsapply()— write remote files to local disksaveSnapshot()— write local snapshot onlyPass 2 — push snapshot (immediate, automatic):
fetch()— fast, local snapshot now matches remote, most files skipreconcile()— only snapshot.json needs pushingpush()— tiny payload, tiny contention windowsaveSnapshot()— update local snapshotOnly triggers when local snapshot is empty at sync start. All other syncs unchanged.