diff --git a/src/cargo/ops/cargo_package/vcs.rs b/src/cargo/ops/cargo_package/vcs.rs index 4f57c568bee..a8c89ca2470 100644 --- a/src/cargo/ops/cargo_package/vcs.rs +++ b/src/cargo/ops/cargo_package/vcs.rs @@ -268,8 +268,41 @@ fn dirty_files_outside_pkg_root( // Handle files outside package root but under git workdir, .filter_map(|p| paths::strip_prefix_canonical(p, workdir).ok()) { - if repo.status_file(&rel_path)? != git2::Status::CURRENT { - dirty_files.insert(workdir.join(rel_path)); + match repo.status_file(&rel_path) { + Ok(git2::Status::CURRENT) => {} + Ok(_) => { + dirty_files.insert(workdir.join(rel_path)); + } + Err(e) => { + if e.code() == git2::ErrorCode::NotFound { + // Object not found means this file might be inside a subrepo/submodule. + // Let's check its status from that repo. + let abs_path = workdir.join(&rel_path); + if let Ok(repo) = git2::Repository::discover(&abs_path) { + let is_dirty = if repo.workdir() == Some(workdir) { + false + } else if let Ok(path) = + paths::strip_prefix_canonical(&abs_path, repo.workdir().unwrap()) + { + repo.status_file(&path) != Ok(git2::Status::CURRENT) + } else { + false + }; + if is_dirty { + dirty_files.insert(abs_path); + } + } + } + + // Dirtiness check for symlinks is mostly informational. + // To avoid adding more complicated logic, + // for now we ignore the status check failure. + debug!( + "failed to get status from file `{}` in git repo at `{}`: {e}", + rel_path.display(), + workdir.display() + ); + } } } Ok(dirty_files) diff --git a/tests/testsuite/package.rs b/tests/testsuite/package.rs index 54d087747da..9e285299e81 100644 --- a/tests/testsuite/package.rs +++ b/tests/testsuite/package.rs @@ -1425,6 +1425,59 @@ edition = "2021" ); } +#[cargo_test] +fn dirty_file_outside_pkg_root_inside_submodule() { + if !symlink_supported() { + return; + } + let (p, repo) = git::new_repo("foo", |p| { + p.file( + "Cargo.toml", + r#" + [workspace] + members = ["isengard"] + resolver = "2" + "#, + ) + .file( + "isengard/Cargo.toml", + r#" + [package] + name = "isengard" + edition = "2015" + homepage = "saruman" + description = "saruman" + license = "ISC" + "#, + ) + .file("isengard/src/lib.rs", "") + }); + let submodule = git::new("submodule", |p| { + p.no_manifest().file("file.txt", "from-submodule") + }); + git::add_submodule( + &repo, + &submodule.root().to_url().to_string(), + Path::new("submodule"), + ); + p.symlink("submodule/file.txt", "isengard/src/file.txt"); + git::add(&repo); + git::commit(&repo); + p.change_file("submodule/file.txt", "changed"); + + p.cargo("package --workspace --no-verify") + .with_status(101) + .with_stderr_data(str![[r#" +[ERROR] 1 files in the working directory contain changes that were not yet committed into git: + +submodule/file.txt + +to proceed despite this and include the uncommitted changes, pass the `--allow-dirty` flag + +"#]]) + .run(); +} + #[cargo_test] fn issue_13695_allow_dirty_vcs_info() { let p = project()