diff --git a/Cargo.toml b/Cargo.toml index 60e1560cda8..43b54da150c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,6 +17,7 @@ path = "src/cargo/lib.rs" [dependencies] atty = "0.2" +base64 = "0.13" bytesize = "1.0" cargo-platform = { path = "crates/cargo-platform", version = "0.1.2" } cargo-util = { path = "crates/cargo-util", version = "0.1.2" } diff --git a/src/cargo/sources/git/utils.rs b/src/cargo/sources/git/utils.rs index 4eafae1c99c..2477f3bbab4 100644 --- a/src/cargo/sources/git/utils.rs +++ b/src/cargo/sources/git/utils.rs @@ -344,18 +344,32 @@ impl<'a> GitCheckout<'a> { } fn update_submodules(&self, cargo_config: &Config) -> CargoResult<()> { - return update_submodules(&self.repo, cargo_config); - - fn update_submodules(repo: &git2::Repository, cargo_config: &Config) -> CargoResult<()> { + // `location` looks like `target/git/checkouts/crate-name-checksum/shorthash` + // `submodule_root` looks like `target/git/checkouts/submodules` + let checkout_root = self.location.parent().unwrap().parent().unwrap(); + // Share the same submodules between all checkouts. Without a shared path, + // cargo would reclone the submodule for each commit that's checked out, + // even if the submodule itself hasn't changed. + let submodule_root = checkout_root.join("submodules"); + + return update_submodules(&self.repo, cargo_config, &submodule_root); + + fn update_submodules( + repo: &git2::Repository, + cargo_config: &Config, + submodule_root: &Path, + ) -> CargoResult<()> { debug!("update submodules for: {:?}", repo.workdir().unwrap()); for mut child in repo.submodules()? { - update_submodule(repo, &mut child, cargo_config).with_context(|| { - format!( - "failed to update submodule `{}`", - child.name().unwrap_or("") - ) - })?; + update_submodule(repo, &mut child, cargo_config, submodule_root).with_context( + || { + format!( + "failed to update submodule `{}`", + child.name().unwrap_or("") + ) + }, + )?; } Ok(()) } @@ -364,6 +378,7 @@ impl<'a> GitCheckout<'a> { parent: &git2::Repository, child: &mut git2::Submodule<'_>, cargo_config: &Config, + submodule_root: &Path, ) -> CargoResult<()> { child.init(false)?; let url = child.url().ok_or_else(|| { @@ -388,14 +403,28 @@ impl<'a> GitCheckout<'a> { let mut repo = match head_and_repo { Ok((head, repo)) => { if child.head_id() == head { - return update_submodules(&repo, cargo_config); + debug!( + "saw up-to-date oid={:?} for submodule {}; skipping update", + head, url + ); + return update_submodules(&repo, cargo_config, submodule_root); } repo } Err(..) => { - let path = parent.workdir().unwrap().join(child.path()); - let _ = paths::remove_dir_all(&path); - init(&path, false)? + // NOTE: most URLs are invalid file paths on Windows. + // Base64-encode them to avoid FS errors. + let config = base64::Config::new(base64::CharacterSet::UrlSafe, true); + let encoded = base64::encode_config(url, config); + let shared_submodule_path = submodule_root.join(encoded); + std::fs::create_dir_all(&shared_submodule_path)?; + let submodule = init(&shared_submodule_path, true)?; + + let checkout_path = parent.workdir().unwrap().join(child.path()); + let _ = paths::remove_dir_all(&checkout_path); + std::fs::create_dir_all(&checkout_path)?; + submodule.set_workdir(&checkout_path, false)?; + submodule } }; // Fetch data from origin and reset to the head commit @@ -413,7 +442,7 @@ impl<'a> GitCheckout<'a> { let obj = repo.find_object(head, None)?; reset(&repo, &obj, cargo_config)?; - update_submodules(&repo, cargo_config) + update_submodules(&repo, cargo_config, submodule_root) } } } diff --git a/tests/testsuite/git.rs b/tests/testsuite/git.rs index 4db0f6ed6d5..ecd7a6d737d 100644 --- a/tests/testsuite/git.rs +++ b/tests/testsuite/git.rs @@ -1287,7 +1287,8 @@ fn dep_with_changed_submodule() { println!("last run"); p.cargo("run") .with_stderr( - "[COMPILING] dep1 v0.5.0 ([..])\n\ + "[UPDATING] git submodule `file://[..]/dep3`\n\ + [COMPILING] dep1 v0.5.0 ([..])\n\ [COMPILING] foo v0.5.0 ([..])\n\ [FINISHED] dev [unoptimized + debuginfo] target(s) in \ [..]\n\