From c380adc6fb21e731f3cd50df076eb9004f4bdd09 Mon Sep 17 00:00:00 2001 From: Jakub Sitnicki Date: Tue, 31 May 2022 10:36:50 +0200 Subject: [PATCH] Respect submodule update=none strategy in .gitmodules Git lets users define the default update/checkout strategy for a submodule by setting the `submodule..update` key in `.gitmodules` file. If the update strategy is `none`, the submodule will be skipped during update. It will not be fetched and checked out: 1. *foo* is a big git repo ``` /tmp $ git init foo Initialized empty Git repository in /tmp/foo/.git/ /tmp $ dd if=/dev/zero of=foo/big bs=1000M count=1 1+0 records in 1+0 records out 1048576000 bytes (1.0 GB, 1000 MiB) copied, 0.482087 s, 2.2 GB/s /tmp $ git -C foo add big /tmp $ git -C foo commit -m 'I am big' [main (root-commit) 84fb533] I am big 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 big ``` 2. *bar* is a repo with a big submodule with `update=none` ``` /tmp $ git init bar Initialized empty Git repository in /tmp/bar/.git/ /tmp $ git -C bar submodule add file:///tmp/foo foo Cloning into '/tmp/bar/foo'... remote: Enumerating objects: 3, done. remote: Counting objects: 100% (3/3), done. remote: Total 3 (delta 0), reused 1 (delta 0), pack-reused 0 Receiving objects: 100% (3/3), 995.50 KiB | 338.00 KiB/s, done. /tmp $ git -C bar config --file .gitmodules submodule.foo.update none /tmp $ cat bar/.gitmodules [submodule "foo"] path = foo url = file:///tmp/foo update = none /tmp $ git -C bar commit --all -m 'I have a big submodule with update=none' [main (root-commit) 6c355ea] I have a big submodule not updated by default 2 files changed, 4 insertions(+) create mode 100644 .gitmodules create mode 160000 foo ``` 3. *baz* is a clone of *bar*, notice *foo* submodule gets skipped ``` /tmp $ git clone --recurse-submodules file:///tmp/bar baz Cloning into 'baz'... remote: Enumerating objects: 3, done. remote: Counting objects: 100% (3/3), done. remote: Compressing objects: 100% (3/3), done. remote: Total 3 (delta 0), reused 0 (delta 0), pack-reused 0 Receiving objects: 100% (3/3), done. Submodule 'foo' (file:///tmp/foo) registered for path 'foo' Skipping submodule 'foo' /tmp $ git -C baz submodule update --init Skipping submodule 'foo' /tmp $ ``` Cargo, on the other hand, ignores the submodule update strategy set in `.gitmodules` properties when updating dependencies. Such behavior can be considered against the wish of the crate publisher. 4. *bar* is now a lib with a big submodule with update disabled ``` /tmp $ cargo init --lib bar Created library package /tmp $ git -C bar add . /tmp $ git -C bar commit -m 'I am a lib with a big submodule but update=none' [main eb07cf7] I am a lib with a big submodule but update=none 3 files changed, 18 insertions(+) create mode 100644 .gitignore create mode 100644 Cargo.toml create mode 100644 src/lib.rs /tmp $ ``` 5. *qux* depends on *bar*, notice *bar*'s submodules are fetched ``` /tmp $ cargo init qux && cd qux Created binary (application) package /tmp/qux $ echo -e '[dependencies.bar]\ngit = "file:///tmp/bar"' >> Cargo.toml /tmp/qux $ time cargo update Updating git repository `file:///tmp/bar` Updating git submodule `file:///tmp/foo` real 0m22.182s user 0m20.402s sys 0m1.714s /tmp/qux $ ``` Fix it by checking if a Git repository submodule should be updated when cargo processes dependencies. 6. With the change applied, submodules with `update=none` are skipped ``` /tmp/qux $ cargo cache -a > /dev/null /tmp/qux $ time ~/src/cargo/target/debug/cargo update Updating git repository `file:///tmp/bar` Skipping git submodule `file:///tmp/foo` real 0m0.029s user 0m0.021s sys 0m0.008s /tmp/qux $ ``` Fixes #4247. Signed-off-by: Jakub Sitnicki --- crates/cargo-test-support/src/compare.rs | 1 + src/cargo/sources/git/utils.rs | 12 ++++++ tests/testsuite/git.rs | 54 ++++++++++++++++++++++++ 3 files changed, 67 insertions(+) diff --git a/crates/cargo-test-support/src/compare.rs b/crates/cargo-test-support/src/compare.rs index 599772d4d1a..0118c5069cc 100644 --- a/crates/cargo-test-support/src/compare.rs +++ b/crates/cargo-test-support/src/compare.rs @@ -196,6 +196,7 @@ fn substitute_macros(input: &str) -> String { ("[OWNER]", " Owner"), ("[MIGRATING]", " Migrating"), ("[EXECUTABLE]", " Executable"), + ("[SKIPPING]", " Skipping"), ]; let mut result = input.to_owned(); for &(pat, subst) in ¯os { diff --git a/src/cargo/sources/git/utils.rs b/src/cargo/sources/git/utils.rs index 4eafae1c99c..f6c630d276d 100644 --- a/src/cargo/sources/git/utils.rs +++ b/src/cargo/sources/git/utils.rs @@ -370,6 +370,18 @@ impl<'a> GitCheckout<'a> { anyhow::format_err!("non-utf8 url for submodule {:?}?", child.path()) })?; + // Skip the submodule if the config says not to update it. + if child.update_strategy() == git2::SubmoduleUpdate::None { + cargo_config.shell().status( + "Skipping", + format!( + "git submodule `{}` due to update strategy in .gitmodules", + url + ), + )?; + return Ok(()); + } + // A submodule which is listed in .gitmodules but not actually // checked out will not have a head id, so we should ignore it. let head = match child.head_id() { diff --git a/tests/testsuite/git.rs b/tests/testsuite/git.rs index f94ec3a6a20..54dd8dcb05d 100644 --- a/tests/testsuite/git.rs +++ b/tests/testsuite/git.rs @@ -1023,6 +1023,60 @@ Caused by: .run(); } +#[cargo_test] +fn dep_with_skipped_submodule() { + // Ensure we skip dependency submodules if their update strategy is `none`. + let qux = git::new("qux", |project| { + project.no_manifest().file("README", "skip me") + }); + + let bar = git::new("bar", |project| { + project + .file("Cargo.toml", &basic_manifest("bar", "0.0.0")) + .file("src/lib.rs", "") + }); + + // `qux` is a submodule of `bar`, but we don't want to update it. + let repo = git2::Repository::open(&bar.root()).unwrap(); + git::add_submodule(&repo, qux.url().as_str(), Path::new("qux")); + + let mut conf = git2::Config::open(&bar.root().join(".gitmodules")).unwrap(); + conf.set_str("submodule.qux.update", "none").unwrap(); + + git::add(&repo); + git::commit(&repo); + + let foo = project() + .file( + "Cargo.toml", + &format!( + r#" + [project] + name = "foo" + version = "0.0.0" + authors = [] + + [dependencies.bar] + git = "{}" + "#, + bar.url() + ), + ) + .file("src/main.rs", "fn main() {}") + .build(); + + foo.cargo("build") + .with_stderr( + "\ +[UPDATING] git repository `file://[..]/bar` +[SKIPPING] git submodule `file://[..]/qux` [..] +[COMPILING] bar [..] +[COMPILING] foo [..] +[FINISHED] dev [unoptimized + debuginfo] target(s) in [..]\n", + ) + .run(); +} + #[cargo_test] fn dep_ambiguous() { let project = project();