Skip to content

sync --push --ref: remote root is tracked per workspace, not per ref, so a second named ref can never be created #9

Description

@santoshkumarradha

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions