Summary
After a workspace has pushed once, pushing to any new named ref always fails:
Error: paired remote lost its published head; refusing to recreate it without re-pairing
Named refs look like the intended mechanism for concurrent publishers (#3), but a workspace can only ever create one of them.
Reproducer
furrow watch --no-daemon
furrow remote add ssh://host --name proj
furrow sync --push --ref one # ok
furrow sync --push --ref two # Error: paired remote lost its published head
Diagnosis
sync_push_on reads a single per-workspace sync state:
let expected = self.read_sync_state()?.map(|state| state.remote_root);
let report = sync::push_on(&self.store, snapshot, config, expected, takeover, remote)?;
self.write_sync_state(parse_id(&report.root)?)?;
push_on then guards with:
} else {
anyhow::ensure!(
expected_remote_root.is_none(),
"paired remote lost its published head; refusing to recreate it without re-pairing"
);
head_key is per-ref, but SyncState is not. So for a brand-new ref the head legitimately does not exist, while expected is Some(root) left over from a push to a different ref, and the guard fires. The guard is correct in intent — it is protecting against a vanished head — but it is reading another ref's bookmark.
push_snapshot_to_ref already sidesteps this by passing None, with a comment noting it is what lets furrow work publish a result ref without contending. That confirms per-ref roots are the intended model; sync --push --ref just does not have them.
Second consequence
sync_pull_on writes the same global state:
self.write_sync_state(pulled.root)?;
So pulling a result ref also overwrites the push bookmark. Pushing to the original ref afterwards fails with remote workspace changed since this machine last synchronized; pull before pushing, even though nothing changed it. A publish-and-collect loop over two refs therefore breaks on the second cycle.
Suggested fix
Key SyncState by ref name (with the current value as the entry for the default ref), so expected is only ever compared against the ref it came from.
Workaround
Use exactly one named ref per workspace for pushing, and treat every other ref as write-only via furrow work. That is what I ended up doing, at the cost of not being able to pin each queued job to its own input snapshot.
Environment
macOS 15 arm64 and Ubuntu 24.04 aarch64, furrow 0.1.0, ssh:// remote.
Summary
After a workspace has pushed once, pushing to any new named ref always fails:
Named refs look like the intended mechanism for concurrent publishers (#3), but a workspace can only ever create one of them.
Reproducer
Diagnosis
sync_push_onreads a single per-workspace sync state:push_onthen guards with:head_keyis per-ref, butSyncStateis not. So for a brand-new ref the head legitimately does not exist, whileexpectedisSome(root)left over from a push to a different ref, and the guard fires. The guard is correct in intent — it is protecting against a vanished head — but it is reading another ref's bookmark.push_snapshot_to_refalready sidesteps this by passingNone, with a comment noting it is what letsfurrow workpublish a result ref without contending. That confirms per-ref roots are the intended model;sync --push --refjust does not have them.Second consequence
sync_pull_onwrites the same global state:So pulling a result ref also overwrites the push bookmark. Pushing to the original ref afterwards fails with
remote workspace changed since this machine last synchronized; pull before pushing, even though nothing changed it. A publish-and-collect loop over two refs therefore breaks on the second cycle.Suggested fix
Key
SyncStateby ref name (with the current value as the entry for the default ref), soexpectedis only ever compared against the ref it came from.Workaround
Use exactly one named ref per workspace for pushing, and treat every other ref as write-only via
furrow work. That is what I ended up doing, at the cost of not being able to pin each queued job to its own input snapshot.Environment
macOS 15 arm64 and Ubuntu 24.04 aarch64, furrow 0.1.0,
ssh://remote.