From 09fd82c7437b6478fa8d03c8166508b84b8fa9a6 Mon Sep 17 00:00:00 2001 From: japabu Date: Sat, 10 Oct 2026 12:12:18 +0200 Subject: [PATCH 1/5] A missing fork or nested commit in the primary is fetched as bootstrap fetches one; a move in flight is finished After the fork's LLVM pin moved to ToyOSOrg's repository, the primary's fork checkout refused with `git fetch origin `, which failed: the owner's `src/llvm-project` was made when `.gitmodules` named rust-lang's, so its `origin` lacks the commit. The build already fetches pinned submodule commits through the admitted `git submodule update` row, and rustc's bootstrap does exactly that on every LLVM build: `git submodule sync`, then `git submodule update --init`, depth 1 where `.gitmodules` says `shallow = true`. So the primary's fork checkout now does the same where it lacks the pinned commit, its own (from the superproject) or a nested submodule's (from the fork). `sync` reads the URL from the moved checkout's `.gitmodules`, so a stale `origin` (one made before the pin's tree named ToyOSOrg's repository) is cured at its root. The primary's refusal arms go. Because git's update reads a nested submodule's URL and commit from the fork's worktree and index, the fork now moves before its nested submodules, and `HEAD` can no longer be the last step. A file in the fork's own git directory (`rev-parse --git-path toyos-fork-move`) says a move is in flight; the next build finishes it, and while it exists a nested submodule's commit alone is no work. The update's own fetch is told not to recurse into the fork's nested submodules: on-demand recursion would fetch each from the `origin` it had before its move, which in the owner's case is rust-lang's llvm-project. A linked worktree runs no `git submodule`: its nested checkout is a git worktree of the primary's, and `git submodule update` there writes its own path as `core.worktree` into the config the primary's shares (measured: git in the primary's `rust/` then fails with "cannot chdir"). A linked worktree lacking a nested commit is still refused, without a command. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_017cSFvbD35xJ2kGANVdm23C --- src/sysroot.rs | 257 +++++++++++++++++++++++++++++++++++++------------ 1 file changed, 193 insertions(+), 64 deletions(-) diff --git a/src/sysroot.rs b/src/sysroot.rs index 6f2111ed35d..0adde581136 100644 --- a/src/sysroot.rs +++ b/src/sysroot.rs @@ -502,8 +502,9 @@ fn pinned_fork(root: &Path) -> String { /// /// The primary's is its own `rust/`, used where it is not initialised yet, which /// whoever initialises it does at the pin. It is no workspace, so one whose -/// `HEAD` is not the pin is moved there; refused by name if it has local -/// changes, or does not hold the pinned commit. +/// `HEAD` is not the pin is moved there, refused by name if it has local +/// changes; a commit missing from it or from a nested submodule is fetched as +/// rustc's bootstrap fetches one ([`submodule_update`]). /// /// A linked worktree's `rust/` starts as the empty stub `git worktree add` /// leaves; it is made here, the first time it is needed, as a git worktree of @@ -516,9 +517,15 @@ fn pinned_fork(root: &Path) -> String { /// changes rather than moved out from under whoever made them. /// /// Every nested submodule checked out in it moves with it to the commit the pin -/// records there, refused by name where it does not hold that commit, or is at -/// a commit that neither the checkout's `HEAD` nor the pin records there, which -/// only its own `HEAD` may record. +/// records there, refused by name where it is at a commit that neither the +/// checkout's `HEAD` nor the pin records there, which only its own `HEAD` may +/// record — and in a linked worktree where it does not hold that commit, which +/// [`submodule_update`] may not fetch there. +/// +/// The checkout moves before its nested submodules, because git's update reads +/// their URLs and commits from the moved checkout's `.gitmodules` and index; a +/// move killed between the two is one [`moving`] says is in flight, which the +/// next finishes. /// /// Every build in a worktree asks this at once, so the making and the move are /// each decided and done under the worktree's lock held exclusively @@ -573,51 +580,38 @@ pub fn fork_checkout(root: &Path, lock: &mut Held) -> PathBuf { .current_dir(&fork) .status() .is_ok_and(|s| s.success()); - (head != pinned && !ahead).then_some(head) + (head != pinned && !ahead || moving(&fork).exists()).then_some(head) }, |head| { - if !holds(&fork, &pinned) { - match &primary { - Some(primary) => git_run(&fork, &["fetch", path_str(&primary.join("rust")), &pinned]), - None => panic!( - "{} is at {head}, and this tree pins the fork at {pinned}, which it does not hold: \ - `git -C {} fetch origin {pinned}` fetches it", - fork.display(), - fork.display(), - ), + if let Some(primary) = &primary { + if !holds(&fork, &pinned) { + git_run(&fork, &["fetch", path_str(&primary.join("rust")), &pinned]); } } - let recorded: Vec<(String, String)> = git_out(&fork, &["ls-tree", "-r", &pinned]) - .lines() - .filter_map(|entry| { - let (meta, path) = entry.split_once('\t')?; - match meta.split_whitespace().collect::>().as_slice() { - ["160000", "commit", commit] => Some((path.to_string(), commit.to_string())), - _ => None, - } - }) - .collect(); - let nested: Vec<(PathBuf, String)> = recorded - .iter() - .map(|(path, commit)| (fork.join(path), commit.clone())) - .filter(|(at, commit)| at.join(".git").exists() && git_out(at, &["rev-parse", "HEAD"]).trim() != commit) - .collect(); - for (at, commit) in &nested { - assert!( - holds(at, commit), - "{} does not hold {commit}, which the fork's {pinned} records there: \ - `git -C {} fetch origin {commit}` fetches it", - at.display(), - at.display(), - ); + // The primary's gets a pin it lacks with its move, so nothing it records is known yet. + let recorded = if holds(&fork, &pinned) { gitlinks(&fork, &pinned) } else { Vec::new() }; + if primary.is_some() { + for (path, commit) in &recorded { + let at = fork.join(path); + assert!( + !at.join(".git").exists() || holds(&at, commit), + "{} does not hold {commit}, which the fork's {pinned} records there, and a linked worktree's \ + fork checkout gets a commit only from the primary's", + at.display(), + ); + } } - // A nested submodule checked out at the commit the pin records there, and nothing more, is no work: a move - // killed before its last step leaves it so. One at any other commit may hold a commit nothing else records. + // A nested submodule checked out at the commit the pin records there, and nothing more, is no work, nor is + // one at any commit while a move is in flight. One at any other commit may hold a commit nothing else records. + let in_flight = moving(&fork).exists(); let status = git_out(&fork, &["status", "--porcelain=v2", "--ignore-submodules=none"]); let work: Vec = status .lines() .filter_map(|entry| { let path = entry.strip_prefix("1 .M SC.. ").and_then(|rest| rest.splitn(6, ' ').nth(5)); + if path.is_some() && in_flight { + return None; + } let Some((path, commit)) = path.and_then(|path| recorded.iter().find(|(p, _)| p == path)) else { return Some(entry.to_string()); }; @@ -636,17 +630,73 @@ pub fn fork_checkout(root: &Path, lock: &mut Held) -> PathBuf { fork.display(), work.join("\n"), ); - for (at, commit) in &nested { - git_run(at, &["checkout", "--detach", "-q", commit]); + let moving = moving(&fork); + fs::write(&moving, "").unwrap_or_else(|e| panic!("write {}: {e}", moving.display())); + match (&primary, holds(&fork, &pinned)) { + (_, true) => git_run(&fork, &["checkout", "--detach", "-q", &pinned]), + (None, false) => submodule_update(root, "rust"), + (Some(_), false) => unreachable!("a linked worktree's fork checkout fetched {pinned} above"), } - // Last, so a move killed before it is asked for again: `HEAD` alone decides. - git_run(&fork, &["checkout", "--detach", "-q", &pinned]); + for (path, commit) in gitlinks(&fork, "HEAD") { + let at = fork.join(&path); + if !at.join(".git").exists() || git_out(&at, &["rev-parse", "HEAD"]).trim() == commit { + continue; + } + match (&primary, holds(&at, &commit)) { + (_, true) => git_run(&at, &["checkout", "--detach", "-q", &commit]), + (None, false) => submodule_update(&fork, &path), + (Some(_), false) => unreachable!("{} lacks {commit}, which was refused above", at.display()), + } + } + fs::remove_file(&moving).unwrap_or_else(|e| panic!("remove {}: {e}", moving.display())); eprintln!("{} was at {head}, and this tree pins {pinned}: checked it out", fork.display()); }, ); fork } +/// The file in `fork`'s own git directory whose presence says a move of it to +/// its pin is in flight: the checkout may be at the pin while a nested +/// submodule is not yet. +fn moving(fork: &Path) -> PathBuf { + fork.join(git_out(fork, &["rev-parse", "--git-path", "toyos-fork-move"]).trim()) +} + +/// The submodules `tree` in `repo` records, by path, each with its commit. +fn gitlinks(repo: &Path, tree: &str) -> Vec<(String, String)> { + git_out(repo, &["ls-tree", "-r", tree]) + .lines() + .filter_map(|entry| { + let (meta, path) = entry.split_once('\t')?; + match meta.split_whitespace().collect::>().as_slice() { + ["160000", "commit", commit] => Some((path.to_string(), commit.to_string())), + _ => None, + } + }) + .collect() +} + +/// The submodule at `path` in `repo` checked out at the commit `repo`'s index +/// records there, fetched as rustc's bootstrap fetches its LLVM: `git submodule +/// sync`, so its `origin` is the URL `repo`'s `.gitmodules` records, then `git +/// submodule update --init`, one commit deep where `.gitmodules` declares it +/// `shallow`. Its own fetch recurses into no submodule of its own, whose +/// `origin` is synced only once it has moved. +/// +/// `repo` must be the primary's superproject or its own fork checkout, whose +/// submodules' git directories are their own: run in a linked worktree's fork +/// checkout, whose nested one is a git worktree of the primary's, `update` +/// writes that checkout's path as `core.worktree` into the config the +/// primary's shares, and git in the primary's fails. +fn submodule_update(repo: &Path, path: &str) { + let shallow = format!("submodule.{path}.shallow"); + let shallow = git_out(repo, &["config", "--file", ".gitmodules", "--type", "bool", "--default", "false", "--get", &shallow]); + git_run(repo, &["submodule", "sync", "--", path]); + let depth: &[&str] = if shallow.trim() == "true" { &["--depth", "1"] } else { &[] }; + let update = ["-c", "fetch.recurseSubmodules=false", "submodule", "update", "--init", "--checkout"]; + git_run(repo, &[&update[..], depth, &["--", path]].concat()); +} + /// Whether the repository at `dir` holds `commit`. fn holds(dir: &Path, commit: &str) -> bool { git_try(dir, &["cat-file", "-e", &format!("{commit}^{{commit}}")]).is_ok() @@ -1607,6 +1657,48 @@ mod tests { twelve_build(); } + /// **A linked worktree's fork checkout runs no `git submodule`**: one whose + /// pin records a nested commit no local repository holds, which only the + /// URL the pin's tree records holds, is refused by name and not moved, and + /// the primary's fork configuration is not written. + #[test] + fn a_linked_worktree_lacking_a_nested_commit_is_refused_and_the_primary_s_is_not_written() { + let base = TempDir::new("fork-linked-nested"); + let (primary, linked, _c1, c2) = two_pins(&base); + let mut lock = buildlock::shared(&linked, "a build"); + let fork = fork_checkout(&linked, &mut lock); + let theirs = primary.join("rust"); + let configs = || { + let modules = primary.join(".git/modules/rust"); + [modules.join("config"), modules.join("modules/library/backtrace/config")].map(|c| fs::read(c).unwrap()) + }; + // What git asks of a submodule's local path, and of no `https` URL. + git(&theirs.join("library/backtrace"), &["config", "protocol.file.allow", "always"]); + let before = configs(); + + let bt = base.join("backtrace-moved"); + git(&base, &["clone", "-q", path_str(&base.join("backtrace-src")), path_str(&bt)]); + write(&bt.join("lib.rs"), "pub fn trace_most() {}\n"); + git(&bt, &["commit", "-qam", "backtrace, moved"]); + let b3 = git(&bt, &["rev-parse", "HEAD"]); + let upstream = base.join("fork-src"); + git(&upstream, &["config", "--file", ".gitmodules", "submodule.library/backtrace.url", path_str(&bt)]); + git(&upstream, &["update-index", "--cacheinfo", &format!("160000,{b3},library/backtrace")]); + git(&upstream, &["add", ".gitmodules"]); + git(&upstream, &["commit", "-qm", "C5"]); + let c5 = git(&upstream, &["rev-parse", "HEAD"]); + git(&theirs, &["-c", "fetch.recurseSubmodules=false", "fetch", "-q", path_str(&upstream), &c5]); + git(&linked, &["update-index", "--cacheinfo", &format!("160000,{c5},rust")]); + git(&linked, &["commit", "-qm", "pins C5"]); + + let said = refusal(|| drop(fork_checkout(&linked, &mut lock))); + + assert_eq!(configs(), before, "the primary's fork configuration was written"); + git(&theirs, &["status", "--porcelain", "--ignore-submodules=none"]); + assert!(said.contains(&b3) && said.contains("linked worktree"), "{said}"); + assert_eq!(git(&fork, &["rev-parse", "HEAD"]), c2, "a checkout was moved over a nested commit it lacks"); + } + /// Where a linked worktree's `rust/` is a fork checkout that is not whole, /// [`crate::ensure_shallow_fork`] refuses, and git in the primary's fork /// still runs. @@ -1627,10 +1719,12 @@ mod tests { /// **The primary's fork checkout is the commit its tree pins**: one behind /// the pin, as a `git pull` leaves it, and one ahead of it are moved there, /// each nested submodule checked out in it with it; one whose nested - /// gitlink alone is moved is no work and moves too; one with local changes, - /// or pinned at a commit it or a nested submodule does not hold, is refused - /// by name and not moved; one not initialised yet is left to whoever - /// initialises it, and git is run in no other repository. + /// gitlink alone is moved is no work and moves too; one with local changes + /// is refused by name and not moved; a commit it or a nested submodule does + /// not hold is fetched from the URL its pin's tree records, whatever its + /// `origin` names; a move killed half-way is finished by the next; one not + /// initialised yet is left to whoever initialises it, and git is run in no + /// other repository. #[test] fn the_primary_s_fork_checkout_is_moved_to_its_pin() { let base = TempDir::new("fork-primary"); @@ -1670,27 +1764,62 @@ mod tests { assert_eq!(head(), c1, "a checkout with local changes was moved"); git(&fork, &["checkout", "-q", "--", "."]); - let upstream = base.join("fork-src"); - write(&upstream.join("library/std/src/lib.rs"), "pub fn c() {}\n"); - git(&upstream, &["commit", "-qam", "C3"]); - let c3 = git(&upstream, &["rev-parse", "HEAD"]); + // The owner's case: each checkout's `origin` names a repository that holds none of the commits to come, as one + // made before `.gitmodules` named the fork's own does, and only the URL the pin's tree records holds them. + let stale = base.join("stale"); + git(&base, &["init", "-q", "--bare", path_str(&stale)]); + git(&nested, &["push", "-q", path_str(&stale), "HEAD:refs/heads/stale-only"]); + for at in [&fork, &nested] { + git(at, &["remote", "set-url", "origin", path_str(&stale)]); + // What git asks of a submodule's local path, and of no `https` URL. + git(at, &["config", "protocol.file.allow", "always"]); + } + let origin = |at: &Path| git(at, &["remote", "get-url", "origin"]); + let worktree_of = |at: &Path| git(at, &["config", "core.worktree"]); + let worktrees = (worktree_of(&fork), worktree_of(&nested)); + + let moved = base.join("fork-moved"); + git(&base, &["clone", "-q", path_str(&base.join("fork-src")), path_str(&moved)]); + write(&moved.join("library/std/src/lib.rs"), "pub fn c() {}\n"); + git(&moved, &["commit", "-qam", "C3"]); + let c3 = git(&moved, &["rev-parse", "HEAD"]); + git(&primary, &["config", "--file", ".gitmodules", "submodule.rust.url", path_str(&moved)]); + git(&primary, &["add", ".gitmodules"]); pin(&c3); - let said = refusal(|| drop(fork_checkout(&primary, &mut lock))); - assert!(said.contains(&format!("`git -C {} fetch origin {c3}` fetches it", fork.display())), "{said}"); - assert_eq!(head(), c1, "a checkout was moved to a commit it does not hold"); + assert_eq!(fork_checkout(&primary, &mut lock), fork); + assert_eq!(head(), c3, "a pin only the URL its tree records holds was not fetched"); + assert_eq!(origin(&fork), path_str(&moved), "the fork checkout's origin is not the URL its pin's tree records"); - let bt = base.join("backtrace-src"); + let bt = base.join("backtrace-moved"); + git(&base, &["clone", "-q", path_str(&base.join("backtrace-src")), path_str(&bt)]); write(&bt.join("lib.rs"), "pub fn trace_most() {}\n"); - git(&bt, &["commit", "-qam", "backtrace, latest"]); + git(&bt, &["commit", "-qam", "backtrace, moved"]); let b3 = git(&bt, &["rev-parse", "HEAD"]); - git(&upstream.join("library/backtrace"), &["pull", "-q"]); - git(&upstream, &["commit", "-qam", "C4"]); - let c4 = git(&upstream, &["rev-parse", "HEAD"]); - git(&fork, &["-c", "fetch.recurseSubmodules=false", "fetch", "-q", path_str(&upstream), &c4]); + git(&moved, &["config", "--file", ".gitmodules", "submodule.library/backtrace.url", path_str(&bt)]); + git(&moved, &["config", "--file", ".gitmodules", "submodule.library/backtrace.shallow", "true"]); + git(&moved, &["update-index", "--cacheinfo", &format!("160000,{b3},library/backtrace")]); + git(&moved, &["add", ".gitmodules"]); + git(&moved, &["commit", "-qm", "C4"]); + let c4 = git(&moved, &["rev-parse", "HEAD"]); pin(&c4); - let said = refusal(|| drop(fork_checkout(&primary, &mut lock))); - assert!(said.contains(&format!("`git -C {} fetch origin {b3}` fetches it", nested.display())), "{said}"); - assert_eq!((head(), nested_head()), (c1.clone(), recorded(&c1)), "a move half-made while a nested commit was missing"); + assert_eq!(fork_checkout(&primary, &mut lock), fork); + assert_eq!((head(), nested_head()), (c4, b3), "a nested pin only its pin's URL holds was not fetched"); + assert_eq!(origin(&nested), path_str(&bt), "the nested checkout's origin is not the URL its pin's tree records"); + assert_eq!(git(&nested, &["rev-parse", "--is-shallow-repository"]), "true", "a shallow submodule was fetched whole"); + assert_eq!((worktree_of(&fork), worktree_of(&nested)), worktrees, "a git directory was given another worktree"); + assert!( + git_try(&nested, &["rev-parse", "--verify", "-q", "refs/remotes/origin/stale-only"]).is_err(), + "the fork's fetch recursed into a nested submodule, from the origin it had before its move" + ); + + // A move killed after the checkout moved and before its nested submodule did is finished by the next. + let b2 = recorded(&c3); + fs::write(moving(&fork), "").unwrap(); + git(&fork, &["checkout", "-q", "--detach", &c3]); + pin(&c3); + assert_eq!(fork_checkout(&primary, &mut lock), fork); + assert_eq!((head(), nested_head()), (c3, b2), "a move killed half-way was left half-made"); + assert!(!moving(&fork).exists(), "a finished move is still in flight"); fs::remove_dir_all(&fork).unwrap(); fs::create_dir(&fork).unwrap(); From d2ac64773841978a845eb26ca5f3f3d514caa340 Mon Sep 17 00:00:00 2001 From: japabu Date: Sat, 10 Oct 2026 12:54:40 +0200 Subject: [PATCH 2/5] A leftover move marker no longer moves a linked worktree's fork checkout that is ahead of its pin `(head != pinned && !ahead || moving)` parsed as `(head != pinned && !ahead) || moving`, so a marker left by a killed move overrode `ahead`: a commit the agent made in its fork checkout after the kill was checked out from under it to finish the move. The predicate is now `(head != pinned || moving) && !ahead`; `ahead` is false in the primary, whose resume of a killed move is unchanged. a_worktree_pinning_another_fork_commit_gets_its_own_checkout leaves a marker under a checkout ahead of its pin and asserts it stays there; under the old parse it reds, exit 101. The tracker's entry for the pre-existing `git submodule update --init library/backtrace` in a linked worktree's fork checkout carries what git 2.54.0 does there: at that arm's own state (an empty `library/backtrace`) it clones a git directory of the checkout's own and leaves the primary's configuration alone; over a `library/backtrace` that is a git worktree of the primary's nested repository it rewrites the primary's nested `core.worktree`, and git in the primary's `rust/` exits 128. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_017cSFvbD35xJ2kGANVdm23C --- ...ckout-runs-git-submodule-in-a-linked-worktree.md | 13 ++++++++++++- src/sysroot.rs | 7 ++++++- 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/issues/the-fork-checkout-runs-git-submodule-in-a-linked-worktree.md b/issues/the-fork-checkout-runs-git-submodule-in-a-linked-worktree.md index a37a9a3e683..e7e6316c3d1 100644 --- a/issues/the-fork-checkout-runs-git-submodule-in-a-linked-worktree.md +++ b/issues/the-fork-checkout-runs-git-submodule-in-a-linked-worktree.md @@ -15,7 +15,18 @@ worktree writes `core.worktree` into shared config and breaks git in the primary checkout's `rust/`. The orchestrator measured that for `git submodule update rust` in a linked worktree of the monorepo, which set the primary's `.git/modules/rust/config` `core.worktree` to a path that does not -exist; this arm is the same command one level down and is unmeasured. +exist. One level down, under git 2.54.0, the same command has two outcomes, +measured on `sysroot`'s `two_pins` fixture shape (pull request #831). Run where +this arm runs it, in a fork checkout fresh from `git worktree add` whose +`library/backtrace` is an empty directory, it clones a git directory of the +checkout's own, under the primary's `.git/modules/rust/worktrees//modules/`, +leaves the primary's nested configuration unchanged, and `git status` in the +primary's `rust/` exits 0. Run where that `library/backtrace` is already a +`git worktree add` of the primary's nested repository, as `fork_checkout` +otherwise makes it, it rewrites the primary's nested `core.worktree` to the +linked path, and `git status` in the primary's `rust/` exits 128. Nothing +keeps the command from the second state: what it is safe in is a property of +the directory git happens to find, and the rule stands on the command. `ensure_submodule` (`src/lib.rs`) runs `git submodule update --init library/backtrace` in the same fork checkout, from `sysroot::build_std` and diff --git a/src/sysroot.rs b/src/sysroot.rs index 0adde581136..80b61368d18 100644 --- a/src/sysroot.rs +++ b/src/sysroot.rs @@ -580,7 +580,7 @@ pub fn fork_checkout(root: &Path, lock: &mut Held) -> PathBuf { .current_dir(&fork) .status() .is_ok_and(|s| s.success()); - (head != pinned && !ahead || moving(&fork).exists()).then_some(head) + ((head != pinned || moving(&fork).exists()) && !ahead).then_some(head) }, |head| { if let Some(primary) = &primary { @@ -1583,6 +1583,11 @@ mod tests { let c3 = git(&fork, &["rev-parse", "HEAD"]); assert_eq!(fork_checkout(&linked, &mut lock), fork); assert_eq!(git(&fork, &["rev-parse", "HEAD"]), c3, "a checkout ahead of its pin was moved"); + // Even where a move of it was killed before the agent committed. + fs::write(moving(&fork), "").unwrap(); + assert_eq!(fork_checkout(&linked, &mut lock), fork); + assert_eq!(git(&fork, &["rev-parse", "HEAD"]), c3, "a checkout ahead of its pin was moved to finish a move"); + fs::remove_file(moving(&fork)).unwrap(); // A clean checkout behind what the tree pins is moved to the pin itself. git(&fork, &["checkout", "-q", &c1]); From 7ec2255b439145db01bcacb55012b326f069ab3e Mon Sep 17 00:00:00 2001 From: japabu Date: Sat, 10 Oct 2026 13:12:06 +0200 Subject: [PATCH 3/5] The fork checkout's move marker goes: the primary moves whatever is off its pin, and a linked worktree's is never moved The marker said a move was in flight so the next build would finish it, and its exemption let a nested commit through while it existed. Review round 3 found it wrong twice in linked worktrees: `is-ancestor` holds for a checkout at its pin, so a killed move there was never finished, and the empty marker exempted any nested commit, the agent's own included. The primary owns `rust/` and nothing commits in its nested checkouts, so it needs no state: wherever the fork or a checked-out nested submodule is off the commit the pin records, it runs the move again, and a nested submodule whose commit alone differs is no work. A move killed between the fork and its nested submodules is finished by the next build because the nested one is still off its pin. A linked worktree's checkout is never moved. One strictly ahead of its pin (`HEAD` is not the pin and the pin is its ancestor) is used as it stands; any other mismatch of the checkout or a nested submodule with the pin is refused by name, with the one command that moves it, fetching from the URL the tree records where the commit is missing. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_017cSFvbD35xJ2kGANVdm23C --- ...y-pin-a-rust-commit-no-repository-holds.md | 2 +- src/sysroot.rs | 332 +++++++++--------- 2 files changed, 159 insertions(+), 175 deletions(-) diff --git a/issues/seven-commits-of-mains-history-pin-a-rust-commit-no-repository-holds.md b/issues/seven-commits-of-mains-history-pin-a-rust-commit-no-repository-holds.md index 2f52d8cb545..d285b271e77 100644 --- a/issues/seven-commits-of-mains-history-pin-a-rust-commit-no-repository-holds.md +++ b/issues/seven-commits-of-mains-history-pin-a-rust-commit-no-repository-holds.md @@ -36,7 +36,7 @@ At `42e5fca73`: ## Read, not measured No check sees whether a commit a branch carries pins a `rust` commit the fork -holds. A worktree's build fetches its pin from the primary checkout's `rust/` +holds. A worktree's build takes its pin from the primary checkout's `rust/` (`sysroot::fork_checkout`), never from the fork, so a commit made there and never pushed builds green. diff --git a/src/sysroot.rs b/src/sysroot.rs index 80b61368d18..f088679d364 100644 --- a/src/sysroot.rs +++ b/src/sysroot.rs @@ -22,7 +22,7 @@ //! **Each worktree builds std in its own fork checkout.** The primary builds in its `rust/`, //! moved to the commit its tree pins; //! a linked worktree in its own `rust/`, made on first need as a git worktree of -//! the primary's fork repository at the commit this tree pins ([`fork_checkout`]). +//! the primary's fork repository at the commit this tree pins and never moved ([`fork_checkout`]). //! `library/std` names `toyos-abi` and `toyos` as `../../../` and each file of //! its ToyOS backend, `sdk/std`, by a `#[path]` as far up, so each checkout's //! std compiles against its own worktree's ABI and backend with nothing @@ -501,31 +501,25 @@ fn pinned_fork(root: &Path) -> String { /// The fork checkout `root`'s std is built in, at the commit `root`'s tree pins. /// /// The primary's is its own `rust/`, used where it is not initialised yet, which -/// whoever initialises it does at the pin. It is no workspace, so one whose -/// `HEAD` is not the pin is moved there, refused by name if it has local -/// changes; a commit missing from it or from a nested submodule is fetched as -/// rustc's bootstrap fetches one ([`submodule_update`]). +/// whoever initialises it does at the pin. It is no workspace and nothing +/// commits in its nested submodules, so one whose `HEAD`, or a nested +/// submodule checked out in it, is not at the commit the pin records is moved +/// there, refused by name if it has local changes; a commit missing from it or +/// from a nested submodule is fetched as rustc's bootstrap fetches one +/// ([`submodule_update`]). The checkout moves before its nested submodules, +/// because git's update reads their URLs and commits from the moved checkout's +/// `.gitmodules` and index; a move killed between the two leaves a nested +/// submodule off its pin, which the next moves. /// /// A linked worktree's `rust/` starts as the empty stub `git worktree add` /// leaves; it is made here, the first time it is needed, as a git worktree of /// the primary's fork repository at the pin, sharing its objects — and /// `library/backtrace` the same way from the primary's, or by git's own clone /// where the primary does not hold that commit. It is where an agent edits the -/// fork, so one ahead of the pin is used as it stands; one neither at the pin -/// nor ahead of it is moved there, fetching the commit from the primary's -/// repository first if it does not hold it, and refused by name if it has local -/// changes rather than moved out from under whoever made them. -/// -/// Every nested submodule checked out in it moves with it to the commit the pin -/// records there, refused by name where it is at a commit that neither the -/// checkout's `HEAD` nor the pin records there, which only its own `HEAD` may -/// record — and in a linked worktree where it does not hold that commit, which -/// [`submodule_update`] may not fetch there. -/// -/// The checkout moves before its nested submodules, because git's update reads -/// their URLs and commits from the moved checkout's `.gitmodules` and index; a -/// move killed between the two is one [`moving`] says is in flight, which the -/// next finishes. +/// fork, and is never moved: one strictly ahead of the pin is used as it +/// stands, and any other whose `HEAD`, or a nested submodule checked out in it, +/// is not at the commit the pin records is refused by name with the command +/// that moves it. /// /// Every build in a worktree asks this at once, so the making and the move are /// each decided and done under the worktree's lock held exclusively @@ -541,125 +535,119 @@ pub fn fork_checkout(root: &Path, lock: &mut Held) -> PathBuf { Owner::Elsewhere(primary) => Some(primary), }; let pinned = pinned_fork(root); - if let Some(primary) = &primary { + let head = || git_out(&fork, &["rev-parse", "HEAD"]).trim().to_string(); + let Some(primary) = primary else { lock.act_if( - "make the fork checkout", - || (!fork.join(".git").exists()).then_some(()), - |()| { - let stub = fs::read_dir(&fork).map_or(0, |d| d.count()); + "move the fork checkout to its pin", + || { + let head = head(); + (head != pinned || !misplaced(&fork, "HEAD").is_empty()).then_some(head) + }, + |head| { + // A nested submodule whose commit alone differs is no work: nothing commits there. + let status = git_out(&fork, &["status", "--porcelain=v2", "--ignore-submodules=none"]); + let work: Vec<&str> = status.lines().filter(|entry| !entry.starts_with("1 .M SC.. ")).collect(); assert!( - stub == 0, - "{} is neither a fork checkout nor the empty stub a worktree starts with", - fork.display() + work.is_empty(), + "{} is at {head} with uncommitted work, and this tree pins the fork at {pinned}: a build \ + here would compile a std this tree does not name, and moving the checkout would lose \ + that work.\n{}", + fork.display(), + work.join("\n"), ); - let _ = fs::remove_dir(&fork); - eprintln!("Making {} a fork checkout at {pinned} (a git worktree of the primary's)", fork.display()); - git_run(&primary.join("rust"), &["worktree", "add", "--detach", path_str(&fork), &pinned]); - let backtrace = git_out(&fork, &["ls-tree", "HEAD", "library/backtrace"]); - let commit = backtrace.split_whitespace().nth(2).unwrap_or_else(|| { - panic!("{} pins no library/backtrace: {backtrace:?}", fork.display()) - }); - let theirs = primary.join("rust/library/backtrace"); - let at = fork.join("library/backtrace"); - if holds(&theirs, commit) { - let _ = fs::remove_dir(&at); - git_run(&theirs, &["worktree", "add", "--detach", path_str(&at), commit]); + if holds(&fork, &pinned) { + git_run(&fork, &["checkout", "--detach", "-q", &pinned]); } else { - git_run(&fork, &["submodule", "update", "--init", "library/backtrace"]); + submodule_update(root, "rust"); } + for (path, _, commit) in misplaced(&fork, "HEAD") { + let at = fork.join(&path); + if holds(&at, &commit) { + git_run(&at, &["checkout", "--detach", "-q", &commit]); + } else { + submodule_update(&fork, &path); + } + } + eprintln!("{} was at {head}, and this tree pins {pinned}: checked it out", fork.display()); }, ); - } + return fork; + }; lock.act_if( - "move the fork checkout to its pin", - || { - let head = git_out(&fork, &["rev-parse", "HEAD"]).trim().to_string(); - let ahead = primary.is_some() - && Command::new("git") - .args(["merge-base", "--is-ancestor", &pinned, &head]) - .current_dir(&fork) - .status() - .is_ok_and(|s| s.success()); - ((head != pinned || moving(&fork).exists()) && !ahead).then_some(head) - }, - |head| { - if let Some(primary) = &primary { - if !holds(&fork, &pinned) { - git_run(&fork, &["fetch", path_str(&primary.join("rust")), &pinned]); - } - } - // The primary's gets a pin it lacks with its move, so nothing it records is known yet. - let recorded = if holds(&fork, &pinned) { gitlinks(&fork, &pinned) } else { Vec::new() }; - if primary.is_some() { - for (path, commit) in &recorded { - let at = fork.join(path); - assert!( - !at.join(".git").exists() || holds(&at, commit), - "{} does not hold {commit}, which the fork's {pinned} records there, and a linked worktree's \ - fork checkout gets a commit only from the primary's", - at.display(), - ); - } - } - // A nested submodule checked out at the commit the pin records there, and nothing more, is no work, nor is - // one at any commit while a move is in flight. One at any other commit may hold a commit nothing else records. - let in_flight = moving(&fork).exists(); - let status = git_out(&fork, &["status", "--porcelain=v2", "--ignore-submodules=none"]); - let work: Vec = status - .lines() - .filter_map(|entry| { - let path = entry.strip_prefix("1 .M SC.. ").and_then(|rest| rest.splitn(6, ' ').nth(5)); - if path.is_some() && in_flight { - return None; - } - let Some((path, commit)) = path.and_then(|path| recorded.iter().find(|(p, _)| p == path)) else { - return Some(entry.to_string()); - }; - let at = fork.join(path); - let at_head = git_out(&at, &["rev-parse", "HEAD"]).trim().to_string(); - (at_head != *commit).then(|| { - format!("{} is at {at_head}, not at {commit}, what {pinned} records there", at.display()) - }) - }) - .collect(); + "make the fork checkout", + || (!fork.join(".git").exists()).then_some(()), + |()| { + let stub = fs::read_dir(&fork).map_or(0, |d| d.count()); assert!( - work.is_empty(), - "{} is at {head} with uncommitted work, and this tree pins the fork at {pinned}: a build \ - here would compile a std this tree does not name, and moving the checkout would lose \ - that work.\n{}", - fork.display(), - work.join("\n"), + stub == 0, + "{} is neither a fork checkout nor the empty stub a worktree starts with", + fork.display() ); - let moving = moving(&fork); - fs::write(&moving, "").unwrap_or_else(|e| panic!("write {}: {e}", moving.display())); - match (&primary, holds(&fork, &pinned)) { - (_, true) => git_run(&fork, &["checkout", "--detach", "-q", &pinned]), - (None, false) => submodule_update(root, "rust"), - (Some(_), false) => unreachable!("a linked worktree's fork checkout fetched {pinned} above"), - } - for (path, commit) in gitlinks(&fork, "HEAD") { - let at = fork.join(&path); - if !at.join(".git").exists() || git_out(&at, &["rev-parse", "HEAD"]).trim() == commit { - continue; - } - match (&primary, holds(&at, &commit)) { - (_, true) => git_run(&at, &["checkout", "--detach", "-q", &commit]), - (None, false) => submodule_update(&fork, &path), - (Some(_), false) => unreachable!("{} lacks {commit}, which was refused above", at.display()), - } + let _ = fs::remove_dir(&fork); + eprintln!("Making {} a fork checkout at {pinned} (a git worktree of the primary's)", fork.display()); + git_run(&primary.join("rust"), &["worktree", "add", "--detach", path_str(&fork), &pinned]); + let backtrace = git_out(&fork, &["ls-tree", "HEAD", "library/backtrace"]); + let commit = backtrace + .split_whitespace() + .nth(2) + .unwrap_or_else(|| panic!("{} pins no library/backtrace: {backtrace:?}", fork.display())); + let theirs = primary.join("rust/library/backtrace"); + let at = fork.join("library/backtrace"); + if holds(&theirs, commit) { + let _ = fs::remove_dir(&at); + git_run(&theirs, &["worktree", "add", "--detach", path_str(&at), commit]); + } else { + git_run(&fork, &["submodule", "update", "--init", "library/backtrace"]); } - fs::remove_file(&moving).unwrap_or_else(|e| panic!("remove {}: {e}", moving.display())); - eprintln!("{} was at {head}, and this tree pins {pinned}: checked it out", fork.display()); }, ); + let head = head(); + let ahead = git_try(&fork, &["merge-base", "--is-ancestor", &pinned, &head]).is_ok(); + if head != pinned && ahead { + return fork; + } + // Each mismatch's own fix, in the order it must run: a nested one is known once the checkout holds its pin. + let mut fixes = Vec::new(); + let mut fix = |at: &Path, url: &str, commit: &str, from: &str| { + if !holds(at, commit) { + fixes.push(format!("git -C {} fetch --no-recurse-submodules {url} {commit}", at.display())); + } + fixes.push(format!("git -C {} checkout --detach -q {commit}", at.display())); + format!("{} is at {from}, not at {commit}, what this tree's pin records there", at.display()) + }; + let mut wrong = Vec::new(); + if head != pinned { + let url = git_out(root, &["config", "--file", ".gitmodules", "--get", "submodule.rust.url"]); + wrong.push(fix(&fork, url.trim(), &pinned, &head)); + } + let nested = if holds(&fork, &pinned) { misplaced(&fork, &pinned) } else { Vec::new() }; + for (path, from, commit) in nested { + let key = format!("{pinned}:.gitmodules"); + let url = git_out(&fork, &["config", "--blob", &key, "--get", &format!("submodule.{path}.url")]); + wrong.push(fix(&fork.join(&path), url.trim(), &commit, &from)); + } + assert!( + wrong.is_empty(), + "{}\na build would compile a std this tree does not name, and a linked worktree's fork checkout is \ + never moved for it: `{}` moves it", + wrong.join("\n"), + fixes.join(" && "), + ); fork } -/// The file in `fork`'s own git directory whose presence says a move of it to -/// its pin is in flight: the checkout may be at the pin while a nested -/// submodule is not yet. -fn moving(fork: &Path) -> PathBuf { - fork.join(git_out(fork, &["rev-parse", "--git-path", "toyos-fork-move"]).trim()) +/// Each nested submodule checked out in `fork` whose `HEAD` is not the commit +/// `tree` records there: its path, its `HEAD` and that commit. +fn misplaced(fork: &Path, tree: &str) -> Vec<(String, String, String)> { + gitlinks(fork, tree) + .into_iter() + .filter(|(path, _)| fork.join(path).join(".git").exists()) + .map(|(path, commit)| { + let at = git_out(&fork.join(&path), &["rev-parse", "HEAD"]).trim().to_string(); + (path, at, commit) + }) + .filter(|(_, at, commit)| at != commit) + .collect() } /// The submodules `tree` in `repo` records, by path, each with its commit. @@ -1577,71 +1565,63 @@ mod tests { ); assert_eq!(git(&linked, &["status", "--porcelain"]), "", "the worktree is not clean"); - // Work on the fork in the worktree's own checkout is what it builds. + // Work on the fork in the worktree's own checkout, strictly ahead of its pin, is what it builds. write(&fork.join("library/std/src/lib.rs"), "pub fn c() {}\n"); git(&fork, &["commit", "-qam", "C3, the agent's own"]); let c3 = git(&fork, &["rev-parse", "HEAD"]); assert_eq!(fork_checkout(&linked, &mut lock), fork); - assert_eq!(git(&fork, &["rev-parse", "HEAD"]), c3, "a checkout ahead of its pin was moved"); - // Even where a move of it was killed before the agent committed. - fs::write(moving(&fork), "").unwrap(); - assert_eq!(fork_checkout(&linked, &mut lock), fork); - assert_eq!(git(&fork, &["rev-parse", "HEAD"]), c3, "a checkout ahead of its pin was moved to finish a move"); - fs::remove_file(moving(&fork)).unwrap(); + assert_eq!(git(&fork, &["rev-parse", "HEAD"]), c3, "a checkout strictly ahead of its pin was moved"); - // A clean checkout behind what the tree pins is moved to the pin itself. - git(&fork, &["checkout", "-q", &c1]); - assert_eq!(fork_checkout(&linked, &mut lock), fork); - assert_eq!(git(&fork, &["rev-parse", "HEAD"]), c2, "a checkout behind its pin was not moved to it"); - let behind = git(&fork, &["rev-parse", &format!("{c1}:library/backtrace")]); - git(&fork.join("library/backtrace"), &["checkout", "-q", "--detach", &behind]); + // One behind its pin is refused with the command that moves it, and not moved. git(&fork, &["checkout", "-q", &c1]); + let said = refusal(|| drop(fork_checkout(&linked, &mut lock))); + assert!(said.contains(&format!("is at {c1}, not at {c2}")), "{said}"); + assert_eq!(git(&fork, &["rev-parse", "HEAD"]), c1, "a linked checkout behind its pin was moved"); + run_fix(&said); assert_eq!(fork_checkout(&linked, &mut lock), fork); - assert_eq!( - git(&fork.join("library/backtrace"), &["rev-parse", "HEAD"]), - git(&fork, &["rev-parse", "HEAD:library/backtrace"]), - "a nested submodule was left behind its pin" - ); - assert_eq!(git(&primary.join("rust"), &["status", "--porcelain"]), before, "the primary's nested submodule moved"); + assert_eq!(git(&fork, &["rev-parse", "HEAD"]), c2, "the refusal's command did not move the checkout to its pin"); - // One with local changes is never moved out from under whoever made them. - git(&fork, &["checkout", "-q", &c1]); - write(&fork.join("library/std/src/lib.rs"), "pub fn uncommitted() {}\n"); - let message = refusal(|| drop(fork_checkout(&linked, &mut lock))); - assert!(message.contains(&c1) && message.contains(&c2), "{message}"); - git(&fork, &["checkout", "-q", "--", "."]); - - // Nor one whose nested submodule holds a commit of the agent's that no gitlink records yet. + // One at its pin with a nested checkout off what the pin records there is refused, not built. let nested = fork.join("library/backtrace"); + let recorded = git(&fork, &["rev-parse", &format!("{c2}:library/backtrace")]); + let behind = git(&fork, &["rev-parse", &format!("{c1}:library/backtrace")]); git(&nested, &["checkout", "-q", "--detach", &behind]); + let said = refusal(|| drop(fork_checkout(&linked, &mut lock))); + assert!(said.contains(&format!("is at {behind}, not at {recorded}")), "{said}"); + assert_eq!(git(&nested, &["rev-parse", "HEAD"]), behind, "a linked nested checkout was moved"); + run_fix(&said); + assert_eq!(fork_checkout(&linked, &mut lock), fork); + assert_eq!(git(&nested, &["rev-parse", "HEAD"]), recorded, "the refusal's command did not move the nested checkout"); + + // Nor is a nested commit of the agent's that no gitlink records moved off. write(&nested.join("lib.rs"), "pub fn trace_mine() {}\n"); git(&nested, &["commit", "-qam", "the agent's own, not yet recorded"]); let mine = git(&nested, &["rev-parse", "HEAD"]); - let recorded = git(&fork, &["rev-parse", &format!("{c2}:library/backtrace")]); - let message = refusal(|| drop(fork_checkout(&linked, &mut lock))); - assert!(message.contains(&mine) && message.contains(&recorded), "{message}"); + let said = refusal(|| drop(fork_checkout(&linked, &mut lock))); + assert!(said.contains(&format!("is at {mine}, not at {recorded}")), "{said}"); assert_eq!(git(&nested, &["rev-parse", "HEAD"]), mine, "a nested commit nothing records was moved off"); - assert_eq!(git(&fork, &["rev-parse", "HEAD"]), c1, "a checkout over a nested commit nothing records was moved"); + assert_eq!(git(&primary.join("rust"), &["status", "--porcelain"]), before, "the primary's fork was touched"); + } - // Nor when that commit sits on top of what the pin records there. - git(&nested, &["checkout", "-q", "--detach", &recorded]); - write(&nested.join("lib.rs"), "pub fn trace_mine_later() {}\n"); - git(&nested, &["commit", "-qam", "the agent's own, on the pin's record"]); - let mine = git(&nested, &["rev-parse", "HEAD"]); - let message = refusal(|| drop(fork_checkout(&linked, &mut lock))); - assert!(message.contains(&mine) && message.contains(&recorded), "{message}"); - assert_eq!(git(&nested, &["rev-parse", "HEAD"]), mine, "a nested commit on the pin's record was moved off"); - assert_eq!(git(&fork, &["rev-parse", "HEAD"]), c1, "a checkout over a nested commit on the pin's record was moved"); + /// Runs the command a linked worktree's refusal names, each git command of + /// it in turn. + fn run_fix(said: &str) { + let command = said.rsplit_once("`").and_then(|(head, _)| head.rsplit_once("`")).map(|(_, c)| c); + let command = command.unwrap_or_else(|| panic!("the refusal names no command: {said}")); + for step in command.split(" && ") { + let args: Vec<&str> = step.split(' ').collect(); + assert_eq!(args[0], "git", "{step}"); + git(Path::new("/"), &args[1..]); + } } /// **Twelve builds starting at once in a worktree make its fork checkout - /// once, and move one behind its pin once**: each holds the worktree's lock - /// as a process of its own does, and each returns the checkout whole and at - /// the pin. + /// once**: each holds the worktree's lock as a process of its own does, and + /// each returns the checkout whole and at the pin. #[test] - fn twelve_builds_at_once_make_and_move_one_fork_checkout() { + fn twelve_builds_at_once_make_one_fork_checkout() { let base = TempDir::new("fork-builds"); - let (_primary, linked, c1, c2) = two_pins(&base); + let (_primary, linked, _c1, c2) = two_pins(&base); let fork = linked.join("rust"); let twelve_build = || { let start = std::sync::Barrier::new(12); @@ -1658,14 +1638,13 @@ mod tests { }); }; twelve_build(); - git(&fork, &["checkout", "-q", &c1]); - twelve_build(); } /// **A linked worktree's fork checkout runs no `git submodule`**: one whose /// pin records a nested commit no local repository holds, which only the /// URL the pin's tree records holds, is refused by name and not moved, and - /// the primary's fork configuration is not written. + /// the command the refusal names fetches it there without writing the + /// primary's fork configuration. #[test] fn a_linked_worktree_lacking_a_nested_commit_is_refused_and_the_primary_s_is_not_written() { let base = TempDir::new("fork-linked-nested"); @@ -1702,6 +1681,12 @@ mod tests { git(&theirs, &["status", "--porcelain", "--ignore-submodules=none"]); assert!(said.contains(&b3) && said.contains("linked worktree"), "{said}"); assert_eq!(git(&fork, &["rev-parse", "HEAD"]), c2, "a checkout was moved over a nested commit it lacks"); + + run_fix(&said); + assert_eq!(fork_checkout(&linked, &mut lock), fork); + assert_eq!(git(&fork.join("library/backtrace"), &["rev-parse", "HEAD"]), b3); + assert_eq!(configs(), before, "the refusal's command wrote the primary's fork configuration"); + git(&theirs, &["status", "--porcelain", "--ignore-submodules=none"]); } /// Where a linked worktree's `rust/` is a fork checkout that is not whole, @@ -1727,7 +1712,8 @@ mod tests { /// gitlink alone is moved is no work and moves too; one with local changes /// is refused by name and not moved; a commit it or a nested submodule does /// not hold is fetched from the URL its pin's tree records, whatever its - /// `origin` names; a move killed half-way is finished by the next; one not + /// `origin` names; one at its pin with a nested submodule off it, as a move + /// killed half-way leaves it, is moved; one not /// initialised yet is left to whoever initialises it, and git is run in no /// other repository. #[test] @@ -1819,12 +1805,10 @@ mod tests { // A move killed after the checkout moved and before its nested submodule did is finished by the next. let b2 = recorded(&c3); - fs::write(moving(&fork), "").unwrap(); git(&fork, &["checkout", "-q", "--detach", &c3]); pin(&c3); assert_eq!(fork_checkout(&primary, &mut lock), fork); assert_eq!((head(), nested_head()), (c3, b2), "a move killed half-way was left half-made"); - assert!(!moving(&fork).exists(), "a finished move is still in flight"); fs::remove_dir_all(&fork).unwrap(); fs::create_dir(&fork).unwrap(); From 5d56846c16a04fe5961edd95b2c05143b4060f95 Mon Sep 17 00:00:00 2001 From: japabu Date: Sat, 10 Oct 2026 13:20:26 +0200 Subject: [PATCH 4/5] The make step stays as main has it, and the linked worktree's check is a function of its own Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_017cSFvbD35xJ2kGANVdm23C --- src/sysroot.rs | 132 ++++++++++++++++++++++++++----------------------- 1 file changed, 69 insertions(+), 63 deletions(-) diff --git a/src/sysroot.rs b/src/sysroot.rs index f088679d364..41d12731c88 100644 --- a/src/sysroot.rs +++ b/src/sysroot.rs @@ -535,76 +535,84 @@ pub fn fork_checkout(root: &Path, lock: &mut Held) -> PathBuf { Owner::Elsewhere(primary) => Some(primary), }; let pinned = pinned_fork(root); - let head = || git_out(&fork, &["rev-parse", "HEAD"]).trim().to_string(); - let Some(primary) = primary else { + if let Some(primary) = &primary { lock.act_if( - "move the fork checkout to its pin", - || { - let head = head(); - (head != pinned || !misplaced(&fork, "HEAD").is_empty()).then_some(head) - }, - |head| { - // A nested submodule whose commit alone differs is no work: nothing commits there. - let status = git_out(&fork, &["status", "--porcelain=v2", "--ignore-submodules=none"]); - let work: Vec<&str> = status.lines().filter(|entry| !entry.starts_with("1 .M SC.. ")).collect(); + "make the fork checkout", + || (!fork.join(".git").exists()).then_some(()), + |()| { + let stub = fs::read_dir(&fork).map_or(0, |d| d.count()); assert!( - work.is_empty(), - "{} is at {head} with uncommitted work, and this tree pins the fork at {pinned}: a build \ - here would compile a std this tree does not name, and moving the checkout would lose \ - that work.\n{}", - fork.display(), - work.join("\n"), + stub == 0, + "{} is neither a fork checkout nor the empty stub a worktree starts with", + fork.display() ); - if holds(&fork, &pinned) { - git_run(&fork, &["checkout", "--detach", "-q", &pinned]); + let _ = fs::remove_dir(&fork); + eprintln!("Making {} a fork checkout at {pinned} (a git worktree of the primary's)", fork.display()); + git_run(&primary.join("rust"), &["worktree", "add", "--detach", path_str(&fork), &pinned]); + let backtrace = git_out(&fork, &["ls-tree", "HEAD", "library/backtrace"]); + let commit = backtrace.split_whitespace().nth(2).unwrap_or_else(|| { + panic!("{} pins no library/backtrace: {backtrace:?}", fork.display()) + }); + let theirs = primary.join("rust/library/backtrace"); + let at = fork.join("library/backtrace"); + if holds(&theirs, commit) { + let _ = fs::remove_dir(&at); + git_run(&theirs, &["worktree", "add", "--detach", path_str(&at), commit]); } else { - submodule_update(root, "rust"); - } - for (path, _, commit) in misplaced(&fork, "HEAD") { - let at = fork.join(&path); - if holds(&at, &commit) { - git_run(&at, &["checkout", "--detach", "-q", &commit]); - } else { - submodule_update(&fork, &path); - } + git_run(&fork, &["submodule", "update", "--init", "library/backtrace"]); } - eprintln!("{} was at {head}, and this tree pins {pinned}: checked it out", fork.display()); }, ); + } + if primary.is_some() { + refuse_off_pin(root, &fork, &pinned); return fork; - }; + } lock.act_if( - "make the fork checkout", - || (!fork.join(".git").exists()).then_some(()), - |()| { - let stub = fs::read_dir(&fork).map_or(0, |d| d.count()); + "move the fork checkout to its pin", + || { + let head = git_out(&fork, &["rev-parse", "HEAD"]).trim().to_string(); + (head != pinned || !misplaced(&fork, "HEAD").is_empty()).then_some(head) + }, + |head| { + // A nested submodule whose commit alone differs is no work: nothing commits there. + let status = git_out(&fork, &["status", "--porcelain=v2", "--ignore-submodules=none"]); + let work: Vec<&str> = status.lines().filter(|entry| !entry.starts_with("1 .M SC.. ")).collect(); assert!( - stub == 0, - "{} is neither a fork checkout nor the empty stub a worktree starts with", - fork.display() + work.is_empty(), + "{} is at {head} with uncommitted work, and this tree pins the fork at {pinned}: a build \ + here would compile a std this tree does not name, and moving the checkout would lose \ + that work.\n{}", + fork.display(), + work.join("\n"), ); - let _ = fs::remove_dir(&fork); - eprintln!("Making {} a fork checkout at {pinned} (a git worktree of the primary's)", fork.display()); - git_run(&primary.join("rust"), &["worktree", "add", "--detach", path_str(&fork), &pinned]); - let backtrace = git_out(&fork, &["ls-tree", "HEAD", "library/backtrace"]); - let commit = backtrace - .split_whitespace() - .nth(2) - .unwrap_or_else(|| panic!("{} pins no library/backtrace: {backtrace:?}", fork.display())); - let theirs = primary.join("rust/library/backtrace"); - let at = fork.join("library/backtrace"); - if holds(&theirs, commit) { - let _ = fs::remove_dir(&at); - git_run(&theirs, &["worktree", "add", "--detach", path_str(&at), commit]); + if holds(&fork, &pinned) { + git_run(&fork, &["checkout", "--detach", "-q", &pinned]); } else { - git_run(&fork, &["submodule", "update", "--init", "library/backtrace"]); + submodule_update(root, "rust"); } + for (path, _, commit) in misplaced(&fork, "HEAD") { + let at = fork.join(&path); + if holds(&at, &commit) { + git_run(&at, &["checkout", "--detach", "-q", &commit]); + } else { + submodule_update(&fork, &path); + } + } + eprintln!("{} was at {head}, and this tree pins {pinned}: checked it out", fork.display()); }, ); - let head = head(); - let ahead = git_try(&fork, &["merge-base", "--is-ancestor", &pinned, &head]).is_ok(); - if head != pinned && ahead { - return fork; + fork +} + +/// A linked worktree's fork checkout `fork` used as it stands where it is +/// strictly ahead of `pinned`; otherwise refused by name, with the command that +/// moves it, wherever it or a nested submodule checked out in it is not at the +/// commit `pinned` records. +fn refuse_off_pin(root: &Path, fork: &Path, pinned: &str) { + let head = git_out(fork, &["rev-parse", "HEAD"]).trim().to_string(); + if head != pinned && git_try(fork, &["merge-base", "--is-ancestor", pinned, &head]).is_ok() { + return; } // Each mismatch's own fix, in the order it must run: a nested one is known once the checkout holds its pin. let mut fixes = Vec::new(); @@ -618,12 +626,12 @@ pub fn fork_checkout(root: &Path, lock: &mut Held) -> PathBuf { let mut wrong = Vec::new(); if head != pinned { let url = git_out(root, &["config", "--file", ".gitmodules", "--get", "submodule.rust.url"]); - wrong.push(fix(&fork, url.trim(), &pinned, &head)); + wrong.push(fix(fork, url.trim(), pinned, &head)); } - let nested = if holds(&fork, &pinned) { misplaced(&fork, &pinned) } else { Vec::new() }; + let nested = if holds(fork, pinned) { misplaced(fork, pinned) } else { Vec::new() }; for (path, from, commit) in nested { - let key = format!("{pinned}:.gitmodules"); - let url = git_out(&fork, &["config", "--blob", &key, "--get", &format!("submodule.{path}.url")]); + let gitmodules = format!("{pinned}:.gitmodules"); + let url = git_out(fork, &["config", "--blob", &gitmodules, "--get", &format!("submodule.{path}.url")]); wrong.push(fix(&fork.join(&path), url.trim(), &commit, &from)); } assert!( @@ -633,7 +641,6 @@ pub fn fork_checkout(root: &Path, lock: &mut Held) -> PathBuf { wrong.join("\n"), fixes.join(" && "), ); - fork } /// Each nested submodule checked out in `fork` whose `HEAD` is not the commit @@ -1713,9 +1720,8 @@ mod tests { /// is refused by name and not moved; a commit it or a nested submodule does /// not hold is fetched from the URL its pin's tree records, whatever its /// `origin` names; one at its pin with a nested submodule off it, as a move - /// killed half-way leaves it, is moved; one not - /// initialised yet is left to whoever initialises it, and git is run in no - /// other repository. + /// killed half-way leaves it, is moved; one not initialised yet is left to + /// whoever initialises it, and git is run in no other repository. #[test] fn the_primary_s_fork_checkout_is_moved_to_its_pin() { let base = TempDir::new("fork-primary"); From faa1820149d86a240c45344b0888fd4aa2ca641a Mon Sep 17 00:00:00 2001 From: japabu Date: Sat, 10 Oct 2026 14:06:46 +0200 Subject: [PATCH 5/5] A linked refusal names the commits its move would leave, a submodule's depth is read by its path or refused, and a runner's fork takes the primary's update path Round 4's review named five changes. - A linked worktree's refusal named `checkout --detach ` even where the checkout's HEAD is not an ancestor of the pin, which leaves the agent's commits in the reflog alone. Where the pin is held, the refusal now names those commits (`git log --oneline ..`). Where it is not, they cannot be known before the fetch, so the named command runs `git merge-base --is-ancestor ` between the fetch and the checkout and stops there; the next build, the pin now held, names them. - `submodule..shallow` is read by name, and git keys it by the submodule's name: one named otherwise than by its path would have been fetched whole. `declared_shallow` now refuses that by name and only then reads `shallow`. - `gitlinks` is folded into its one caller, `misplaced`. - `ensure_shallow_fork` now runs `submodule_update` with depth 1, so a runner's fork and the primary's moves share sync, update and the no-recursion fetch. Their depths still differ, and the doc says why: a linked worktree's checkout shares the primary's objects and is told ahead from behind by ancestry, which a shallow `rust` would cut; a runner has no linked worktree. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_017cSFvbD35xJ2kGANVdm23C --- src/lib.rs | 13 ++-- src/sysroot.rs | 183 +++++++++++++++++++++++++++++++++++++------------ 2 files changed, 147 insertions(+), 49 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 872dbdaec64..3e8700e7da4 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -121,6 +121,11 @@ pub fn ensure_submodules(repo_dir: &Path) { /// the toolchain from them need. Refused in a linked worktree, whose `rust/` is /// `sysroot::fork_checkout`'s to make: `git submodule` there rewrites the /// `core.worktree` of the primary's fork. +/// +/// One commit deep, where the primary's moves take the depth `.gitmodules` +/// declares, which for `rust` is its whole history: a linked worktree's fork +/// checkout shares the primary's objects and is told ahead of its pin from +/// behind it by ancestry, and a runner has no linked worktree. pub fn ensure_shallow_fork(root: &Path) -> Result<(), String> { if root.join("rust/x.py").exists() { return Ok(()); @@ -131,12 +136,8 @@ pub fn ensure_shallow_fork(root: &Path) -> Result<(), String> { root.join("rust").display() )); } - let status = Command::new("git") - .args(["submodule", "update", "--init", "--depth", "1", "rust"]) - .current_dir(root) - .status() - .map_err(|e| format!("git submodule update --init --depth 1 rust: {e}"))?; - status.success().then_some(()).ok_or_else(|| format!("git submodule update --init --depth 1 rust exited {status}")) + sysroot::submodule_update(root, "rust", true); + Ok(()) } /// Ensure a single git submodule is checked out. diff --git a/src/sysroot.rs b/src/sysroot.rs index 41d12731c88..04fe6c5b06a 100644 --- a/src/sysroot.rs +++ b/src/sysroot.rs @@ -519,7 +519,7 @@ fn pinned_fork(root: &Path) -> String { /// fork, and is never moved: one strictly ahead of the pin is used as it /// stands, and any other whose `HEAD`, or a nested submodule checked out in it, /// is not at the commit the pin records is refused by name with the command -/// that moves it. +/// that moves it and the commits that move would leave. /// /// Every build in a worktree asks this at once, so the making and the move are /// each decided and done under the worktree's lock held exclusively @@ -589,14 +589,14 @@ pub fn fork_checkout(root: &Path, lock: &mut Held) -> PathBuf { if holds(&fork, &pinned) { git_run(&fork, &["checkout", "--detach", "-q", &pinned]); } else { - submodule_update(root, "rust"); + submodule_update(root, "rust", declared_shallow(root, "rust")); } for (path, _, commit) in misplaced(&fork, "HEAD") { let at = fork.join(&path); if holds(&at, &commit) { git_run(&at, &["checkout", "--detach", "-q", &commit]); } else { - submodule_update(&fork, &path); + submodule_update(&fork, &path, declared_shallow(&fork, &path)); } } eprintln!("{} was at {head}, and this tree pins {pinned}: checked it out", fork.display()); @@ -608,7 +608,9 @@ pub fn fork_checkout(root: &Path, lock: &mut Held) -> PathBuf { /// A linked worktree's fork checkout `fork` used as it stands where it is /// strictly ahead of `pinned`; otherwise refused by name, with the command that /// moves it, wherever it or a nested submodule checked out in it is not at the -/// commit `pinned` records. +/// commit `pinned` records. Where that move would leave commits the refusal +/// names them, or, where it cannot know them before a fetch, the command stops +/// after the fetch. fn refuse_off_pin(root: &Path, fork: &Path, pinned: &str) { let head = git_out(fork, &["rev-parse", "HEAD"]).trim().to_string(); if head != pinned && git_try(fork, &["merge-base", "--is-ancestor", pinned, &head]).is_ok() { @@ -617,11 +619,19 @@ fn refuse_off_pin(root: &Path, fork: &Path, pinned: &str) { // Each mismatch's own fix, in the order it must run: a nested one is known once the checkout holds its pin. let mut fixes = Vec::new(); let mut fix = |at: &Path, url: &str, commit: &str, from: &str| { + let mut line = format!("{} is at {from}, not at {commit}, what this tree's pin records there", at.display()); if !holds(at, commit) { fixes.push(format!("git -C {} fetch --no-recurse-submodules {url} {commit}", at.display())); + // Which commits the checkout would leave is known only once the commit is here. + fixes.push(format!("git -C {} merge-base --is-ancestor {from} {commit}", at.display())); + line += "; where checking it out would leave commits of its own the command stops before it, and the next \ + build names them"; + } else if git_try(at, &["merge-base", "--is-ancestor", from, commit]).is_err() { + let left = git_out(at, &["log", "--oneline", &format!("{commit}..{from}")]); + line += &format!("; checking it out leaves these, which {commit} does not hold:\n{}", left.trim_end()); } fixes.push(format!("git -C {} checkout --detach -q {commit}", at.display())); - format!("{} is at {from}, not at {commit}, what this tree's pin records there", at.display()) + line }; let mut wrong = Vec::new(); if head != pinned { @@ -646,20 +656,7 @@ fn refuse_off_pin(root: &Path, fork: &Path, pinned: &str) { /// Each nested submodule checked out in `fork` whose `HEAD` is not the commit /// `tree` records there: its path, its `HEAD` and that commit. fn misplaced(fork: &Path, tree: &str) -> Vec<(String, String, String)> { - gitlinks(fork, tree) - .into_iter() - .filter(|(path, _)| fork.join(path).join(".git").exists()) - .map(|(path, commit)| { - let at = git_out(&fork.join(&path), &["rev-parse", "HEAD"]).trim().to_string(); - (path, at, commit) - }) - .filter(|(_, at, commit)| at != commit) - .collect() -} - -/// The submodules `tree` in `repo` records, by path, each with its commit. -fn gitlinks(repo: &Path, tree: &str) -> Vec<(String, String)> { - git_out(repo, &["ls-tree", "-r", tree]) + git_out(fork, &["ls-tree", "-r", tree]) .lines() .filter_map(|entry| { let (meta, path) = entry.split_once('\t')?; @@ -668,26 +665,47 @@ fn gitlinks(repo: &Path, tree: &str) -> Vec<(String, String)> { _ => None, } }) + .filter(|(path, _)| fork.join(path).join(".git").exists()) + .map(|(path, commit)| { + let at = git_out(&fork.join(&path), &["rev-parse", "HEAD"]).trim().to_string(); + (path, at, commit) + }) + .filter(|(_, at, commit)| at != commit) .collect() } +/// Whether `repo`'s `.gitmodules` declares the submodule at `path` `shallow`, +/// which git keys by the submodule's name: one named otherwise than by its +/// path is refused, where a lookup by path would miss it and fetch the whole +/// history. +fn declared_shallow(repo: &Path, path: &str) -> bool { + let named = git_try(repo, &["config", "--file", ".gitmodules", "--get", &format!("submodule.{path}.path")]); + assert!( + named.as_deref().is_ok_and(|named| named.trim_ascii() == path.as_bytes()), + "{}'s .gitmodules names the submodule at {path} otherwise than by its path, and its depth is read by \ + that name: `git config --file .gitmodules --get-regexp '\\.path$'` shows its name", + repo.display(), + ); + let shallow = format!("submodule.{path}.shallow"); + let shallow = git_out(repo, &["config", "--file", ".gitmodules", "--type", "bool", "--default", "false", "--get", &shallow]); + shallow.trim() == "true" +} + /// The submodule at `path` in `repo` checked out at the commit `repo`'s index /// records there, fetched as rustc's bootstrap fetches its LLVM: `git submodule /// sync`, so its `origin` is the URL `repo`'s `.gitmodules` records, then `git -/// submodule update --init`, one commit deep where `.gitmodules` declares it -/// `shallow`. Its own fetch recurses into no submodule of its own, whose -/// `origin` is synced only once it has moved. +/// submodule update --init`, one commit deep where `shallow`. Its own fetch +/// recurses into no submodule of its own, whose `origin` is synced only once +/// it has moved. /// -/// `repo` must be the primary's superproject or its own fork checkout, whose +/// `repo` must be a superproject or the primary's own fork checkout, whose /// submodules' git directories are their own: run in a linked worktree's fork /// checkout, whose nested one is a git worktree of the primary's, `update` /// writes that checkout's path as `core.worktree` into the config the /// primary's shares, and git in the primary's fails. -fn submodule_update(repo: &Path, path: &str) { - let shallow = format!("submodule.{path}.shallow"); - let shallow = git_out(repo, &["config", "--file", ".gitmodules", "--type", "bool", "--default", "false", "--get", &shallow]); +pub(crate) fn submodule_update(repo: &Path, path: &str, shallow: bool) { git_run(repo, &["submodule", "sync", "--", path]); - let depth: &[&str] = if shallow.trim() == "true" { &["--depth", "1"] } else { &[] }; + let depth: &[&str] = if shallow { &["--depth", "1"] } else { &[] }; let update = ["-c", "fetch.recurseSubmodules=false", "submodule", "update", "--init", "--checkout"]; git_run(repo, &[&update[..], depth, &["--", path]].concat()); } @@ -1164,6 +1182,10 @@ mod tests { use toyos_tmpdir::TempDir; fn git(dir: &Path, args: &[&str]) -> String { + git_result(dir, args).unwrap_or_else(|why| panic!("{why}")) + } + + fn git_result(dir: &Path, args: &[&str]) -> Result { let out = Command::new("git") .args(["-c", "commit.gpgsign=false", "-c", "user.email=t@t", "-c", "user.name=t"]) .args(["-c", "protocol.file.allow=always", "-c", "init.defaultBranch=main"]) @@ -1172,8 +1194,10 @@ mod tests { .current_dir(dir) .output() .expect("run git"); - assert!(out.status.success(), "git {args:?} in {}: {}", dir.display(), String::from_utf8_lossy(&out.stderr)); - String::from_utf8(out.stdout).unwrap().trim().to_string() + if !out.status.success() { + return Err(format!("git {args:?} in {}: {}", dir.display(), String::from_utf8_lossy(&out.stderr))); + } + Ok(String::from_utf8(out.stdout).unwrap().trim().to_string()) } fn write(path: &Path, text: &str) { @@ -1583,11 +1607,23 @@ mod tests { git(&fork, &["checkout", "-q", &c1]); let said = refusal(|| drop(fork_checkout(&linked, &mut lock))); assert!(said.contains(&format!("is at {c1}, not at {c2}")), "{said}"); + assert!(!said.contains("leaves these"), "a checkout behind its pin was said to leave commits: {said}"); assert_eq!(git(&fork, &["rev-parse", "HEAD"]), c1, "a linked checkout behind its pin was moved"); run_fix(&said); assert_eq!(fork_checkout(&linked, &mut lock), fork); assert_eq!(git(&fork, &["rev-parse", "HEAD"]), c2, "the refusal's command did not move the checkout to its pin"); + // One with commits of the agent's on an older pin is refused naming the commits the command leaves. + git(&fork, &["checkout", "-q", &c1]); + write(&fork.join("library/std/src/lib.rs"), "pub fn d() {}\n"); + git(&fork, &["commit", "-qam", "D, the agent's on its old pin"]); + let d = git(&fork, &["rev-parse", "--short", "HEAD"]); + let said = refusal(|| drop(fork_checkout(&linked, &mut lock))); + assert!(said.contains(&format!("leaves these, which {c2} does not hold:\n{d} D, the agent's on its old pin")), "{said}"); + run_fix(&said); + assert_eq!(fork_checkout(&linked, &mut lock), fork); + assert_eq!(git(&fork, &["rev-parse", "HEAD"]), c2); + // One at its pin with a nested checkout off what the pin records there is refused, not built. let nested = fork.join("library/backtrace"); let recorded = git(&fork, &["rev-parse", &format!("{c2}:library/backtrace")]); @@ -1606,6 +1642,7 @@ mod tests { let mine = git(&nested, &["rev-parse", "HEAD"]); let said = refusal(|| drop(fork_checkout(&linked, &mut lock))); assert!(said.contains(&format!("is at {mine}, not at {recorded}")), "{said}"); + assert!(said.contains("leaves these") && said.contains("the agent's own, not yet recorded"), "{said}"); assert_eq!(git(&nested, &["rev-parse", "HEAD"]), mine, "a nested commit nothing records was moved off"); assert_eq!(git(&primary.join("rust"), &["status", "--porcelain"]), before, "the primary's fork was touched"); } @@ -1613,13 +1650,19 @@ mod tests { /// Runs the command a linked worktree's refusal names, each git command of /// it in turn. fn run_fix(said: &str) { + try_fix(said).unwrap_or_else(|why| panic!("{why}")); + } + + /// [`run_fix`], stopping at the first step that fails, as `&&` does. + fn try_fix(said: &str) -> Result<(), String> { let command = said.rsplit_once("`").and_then(|(head, _)| head.rsplit_once("`")).map(|(_, c)| c); let command = command.unwrap_or_else(|| panic!("the refusal names no command: {said}")); for step in command.split(" && ") { let args: Vec<&str> = step.split(' ').collect(); assert_eq!(args[0], "git", "{step}"); - git(Path::new("/"), &args[1..]); + git_result(Path::new("/"), &args[1..])?; } + Ok(()) } /// **Twelve builds starting at once in a worktree make its fork checkout @@ -1666,9 +1709,27 @@ mod tests { // What git asks of a submodule's local path, and of no `https` URL. git(&theirs.join("library/backtrace"), &["config", "protocol.file.allow", "always"]); let before = configs(); + let b3 = pin_a_nested_commit_only_its_url_holds(&base, &primary, &linked); + + let said = refusal(|| drop(fork_checkout(&linked, &mut lock))); + assert_eq!(configs(), before, "the primary's fork configuration was written"); + git(&theirs, &["status", "--porcelain", "--ignore-submodules=none"]); + assert!(said.contains(&b3) && said.contains("linked worktree"), "{said}"); + assert_eq!(git(&fork, &["rev-parse", "HEAD"]), c2, "a checkout was moved over a nested commit it lacks"); + + run_fix(&said); + assert_eq!(fork_checkout(&linked, &mut lock), fork); + assert_eq!(git(&fork.join("library/backtrace"), &["rev-parse", "HEAD"]), b3); + assert_eq!(configs(), before, "the refusal's command wrote the primary's fork configuration"); + git(&theirs, &["status", "--porcelain", "--ignore-submodules=none"]); + } + + /// `linked` pinned at C5, whose `library/backtrace` is a commit B3 that + /// only the URL C5's `.gitmodules` records holds; B3. + fn pin_a_nested_commit_only_its_url_holds(base: &Path, primary: &Path, linked: &Path) -> String { let bt = base.join("backtrace-moved"); - git(&base, &["clone", "-q", path_str(&base.join("backtrace-src")), path_str(&bt)]); + git(base, &["clone", "-q", path_str(&base.join("backtrace-src")), path_str(&bt)]); write(&bt.join("lib.rs"), "pub fn trace_most() {}\n"); git(&bt, &["commit", "-qam", "backtrace, moved"]); let b3 = git(&bt, &["rev-parse", "HEAD"]); @@ -1678,22 +1739,58 @@ mod tests { git(&upstream, &["add", ".gitmodules"]); git(&upstream, &["commit", "-qm", "C5"]); let c5 = git(&upstream, &["rev-parse", "HEAD"]); - git(&theirs, &["-c", "fetch.recurseSubmodules=false", "fetch", "-q", path_str(&upstream), &c5]); - git(&linked, &["update-index", "--cacheinfo", &format!("160000,{c5},rust")]); - git(&linked, &["commit", "-qm", "pins C5"]); + git(&primary.join("rust"), &["-c", "fetch.recurseSubmodules=false", "fetch", "-q", path_str(&upstream), &c5]); + git(linked, &["update-index", "--cacheinfo", &format!("160000,{c5},rust")]); + git(linked, &["commit", "-qm", "pins C5"]); + b3 + } + + /// **A linked worktree's command never leaves a nested commit of the + /// agent's**: where the commit its pin records is not fetched yet, the + /// refusal's command stops after fetching it rather than check out over + /// commits it does not hold, and the next refusal names them. + #[test] + fn a_linked_refusal_s_command_leaves_no_commit_of_the_agent_s() { + let base = TempDir::new("fork-linked-diverged"); + let (primary, linked, _c1, _c2) = two_pins(&base); + let mut lock = buildlock::shared(&linked, "a build"); + let fork = fork_checkout(&linked, &mut lock); + let nested = fork.join("library/backtrace"); + git(&primary.join("rust/library/backtrace"), &["config", "protocol.file.allow", "always"]); + write(&nested.join("lib.rs"), "pub fn trace_mine() {}\n"); + git(&nested, &["commit", "-qam", "the agent's own, on the old pin"]); + let mine = git(&nested, &["rev-parse", "HEAD"]); + let b3 = pin_a_nested_commit_only_its_url_holds(&base, &primary, &linked); let said = refusal(|| drop(fork_checkout(&linked, &mut lock))); + assert!(said.contains(&format!("is at {mine}, not at {b3}")), "{said}"); + let stopped = try_fix(&said); - assert_eq!(configs(), before, "the primary's fork configuration was written"); - git(&theirs, &["status", "--porcelain", "--ignore-submodules=none"]); - assert!(said.contains(&b3) && said.contains("linked worktree"), "{said}"); - assert_eq!(git(&fork, &["rev-parse", "HEAD"]), c2, "a checkout was moved over a nested commit it lacks"); + assert!(stopped.as_ref().is_err_and(|why| why.contains("merge-base")), "{stopped:?}"); + assert_eq!(git(&nested, &["rev-parse", "HEAD"]), mine, "the refusal's command left a commit of the agent's"); + let said = refusal(|| drop(fork_checkout(&linked, &mut lock))); + assert!(said.contains("leaves these") && said.contains("the agent's own, on the old pin"), "{said}"); + } - run_fix(&said); - assert_eq!(fork_checkout(&linked, &mut lock), fork); - assert_eq!(git(&fork.join("library/backtrace"), &["rev-parse", "HEAD"]), b3); - assert_eq!(configs(), before, "the refusal's command wrote the primary's fork configuration"); - git(&theirs, &["status", "--porcelain", "--ignore-submodules=none"]); + /// **A submodule's depth is read by its path or refused**: `shallow` is + /// read where `.gitmodules` names the submodule by its path, and one named + /// otherwise is refused by name rather than fetched whole. + #[test] + fn a_submodule_named_otherwise_than_by_its_path_is_refused() { + let base = TempDir::new("fork-named"); + let repo = base.join("repo"); + fs::create_dir_all(&repo).unwrap(); + git(&repo, &["init", "-q"]); + git(&repo, &["config", "--file", ".gitmodules", "submodule.sub.path", "sub"]); + assert!(!declared_shallow(&repo, "sub")); + git(&repo, &["config", "--file", ".gitmodules", "submodule.sub.shallow", "true"]); + assert!(declared_shallow(&repo, "sub")); + + git(&repo, &["config", "--file", ".gitmodules", "--rename-section", "submodule.sub", "submodule.other"]); + let said = refusal(|| { + declared_shallow(&repo, "sub"); + }); + assert!(said.contains("names the submodule at sub otherwise than by its path"), "{said}"); } /// Where a linked worktree's `rust/` is a fork checkout that is not whole,