cmux link sync push: copy repos that borrow objects (alternates) to Cloud

Merged
TE
3d ago
+344-8

Leo picked a project and a Cloud machine in a DEV build, and the send was refused with:

Couldn't copy the folder to leo-cloud: /Users/leoli/Projects/cmux borrows objects from another repository (objects/info/alternates); clone it without --shared or --reference first

Leo: "chatgpt for chatgpt work we can select project etc / we wanna have that be possible too". Selecting a project for Cloud has to work for any local project, and a repo with alternates is common: git clone --shared and --reference make them, and so do worktree mirrors.

What changed

cmux link sync push (the copy behind Move to Cloud and cloud.startChat, in cmux-tui/crates/cmux-tui/src/link/sync_cli/local.rs) no longer refuses these repos. The copy now stands on its own:

  • The object folders a repo borrows from (objects/info/alternates) are followed, including their own alternates (up to git's 5 levels, absolute or relative paths, C-quoted lines too). Their object files (loose objects, packs with their .idx/.rev/.promisor/.bitmap, commit-graph layers) go into the copy's .git/objects/. If the repo and a borrowed store hold a file of the same name, the repo's own copy wins (same hash, same data).
  • The alternates file itself never goes, since its paths point at folders on this Mac. Neither do the borrowed stores' own indexes that cover only their objects (multi-pack-index, info/packs, the commit-graph chain file, info/commit-graph), nor tmp_* or .lock files.
  • Pack names are stable, so the existing per-file manifest still dedupes. A second push with no changes writes nothing, which the test checks.
  • Partial (blobless) clones keep working: a borrowed promisor pack brings its .promisor marker along.

I didn't use a bundle: git bundle create on a blobless partial clone lazily fetches every missing blob. I also didn't clone from origin on the machine: the sync talks to the machine only through the daemon's fs-v1 file ops, which can't run git there.

Logic is in Rust only. No Swift changes.

Tests

  • tests/link_sync.rs::a_repo_that_borrows_objects_arrives_standing_alone: a chain of git clone --shared repos (copy -> mid -> app, with mid's link rewritten to a relative path), with a commit in each and an unstaged edit. It checks that:
    • the push writes no alternates file;
    • a second push writes nothing;
    • after both borrowed repos are deleted, git fsck --full passes on the machine's copy, HEAD, history (--all), stash and status match, and a 13 MiB blob that only the farthest repo held is there.
  • Unit tests for alternates-line unquoting and for the skip rules on borrowed stores.

Follow-up (not in this PR)

The copy still sends the whole object store. On a big local store (Leo's ~/Projects/cmux has 83 GB of local objects) that means hashing and uploading all of it on the first push. Sending only what the machine can't fetch from origin needs a git step on the machine, which needs a machine-side exec path beyond fs-v1.

Agent session: agent session

šŸ¤– Generated with Claude Code