From d3b85cd96a76482e83caaa1f9e0c1fb2d788165c Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Wed, 23 Jul 2025 04:36:07 +0200 Subject: [PATCH] Provide a `package` implementation with `gix-status`. This should also help fixing these spurious "cannot package because some excluded file is untracked" issues. Remove the respective `git2` implementation at the same time as there seems to be no need for it. --- Cargo.lock | 74 ++++++- Cargo.toml | 2 +- src/cargo/ops/cargo_package/mod.rs | 1 - src/cargo/ops/cargo_package/vcs.rs | 331 ++++++++++++++++++----------- tests/testsuite/git.rs | 6 +- tests/testsuite/package.rs | 15 +- 6 files changed, 286 insertions(+), 143 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index ceff47b22..88e829cef 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -892,6 +892,20 @@ dependencies = [ "syn 2.0.104", ] +[[package]] +name = "dashmap" +version = "6.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5041cc499144891f3790297212f32a74fb938e5136a14943f338ef9e0ae276cf" +dependencies = [ + "cfg-if", + "crossbeam-utils", + "hashbrown 0.14.5", + "lock_api", + "once_cell", + "parking_lot_core", +] + [[package]] name = "der" version = "0.7.10" @@ -1283,6 +1297,7 @@ dependencies = [ "gix-revwalk", "gix-sec", "gix-shallow", + "gix-status", "gix-submodule", "gix-tempfile", "gix-trace", @@ -1445,8 +1460,20 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "de854852010d44a317f30c92d67a983e691c9478c8a3fb4117c1f48626bcdea8" dependencies = [ "bstr", + "gix-attributes", + "gix-command", + "gix-filter", + "gix-fs", "gix-hash", + "gix-index", "gix-object", + "gix-path", + "gix-pathspec", + "gix-tempfile", + "gix-trace", + "gix-traverse", + "gix-worktree", + "imara-diff", "thiserror 2.0.12", ] @@ -1573,7 +1600,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c35300b54896153e55d53f4180460931ccd69b7e8d2f6b9d6401122cdedc4f07" dependencies = [ "gix-hash", - "hashbrown", + "hashbrown 0.15.4", "parking_lot", ] @@ -1609,7 +1636,7 @@ dependencies = [ "gix-traverse", "gix-utils", "gix-validate", - "hashbrown", + "hashbrown 0.15.4", "itoa 1.0.15", "libc", "memmap2", @@ -1902,6 +1929,29 @@ dependencies = [ "thiserror 2.0.12", ] +[[package]] +name = "gix-status" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a4afff9b34eeececa8bdc32b42fb318434b6b1391d9f8d45fe455af08dc2d35" +dependencies = [ + "bstr", + "filetime", + "gix-diff", + "gix-dir", + "gix-features", + "gix-filter", + "gix-fs", + "gix-hash", + "gix-index", + "gix-object", + "gix-path", + "gix-pathspec", + "gix-worktree", + "portable-atomic", + "thiserror 2.0.12", +] + [[package]] name = "gix-submodule" version = "0.20.0" @@ -1923,6 +1973,7 @@ version = "18.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "666c0041bcdedf5fa05e9bef663c897debab24b7dc1741605742412d1d47da57" dependencies = [ + "dashmap", "gix-fs", "libc", "once_cell", @@ -2092,6 +2143,12 @@ dependencies = [ "byteorder", ] +[[package]] +name = "hashbrown" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" + [[package]] name = "hashbrown" version = "0.15.4" @@ -2109,7 +2166,7 @@ version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7382cf6263419f2d8df38c55d7da83da5c18aef87fc7a7fc1fb1e344edfe14c1" dependencies = [ - "hashbrown", + "hashbrown 0.15.4", ] [[package]] @@ -2323,6 +2380,15 @@ dependencies = [ "version_check", ] +[[package]] +name = "imara-diff" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17d34b7d42178945f775e84bc4c36dde7c1c6cdfea656d3354d009056f2bb3d2" +dependencies = [ + "hashbrown 0.15.4", +] + [[package]] name = "indexmap" version = "2.10.0" @@ -2330,7 +2396,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fe4cd85333e22411419a0bcae1297d25e58c9443848b11dc6a86fefe8c78a661" dependencies = [ "equivalent", - "hashbrown", + "hashbrown 0.15.4", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index ffde02ad0..3e573091e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -49,7 +49,7 @@ flate2 = { version = "1.1.2", default-features = false, features = ["zlib-rs"] } git2 = "0.20.2" git2-curl = "0.21.0" # When updating this, also see if `gix-transport` further down needs updating or some auth-related tests will fail. -gix = { version = "0.73.0", default-features = false, features = ["blocking-http-transport-curl", "progress-tree", "parallel", "dirwalk"] } +gix = { version = "0.73.0", default-features = false, features = ["blocking-http-transport-curl", "progress-tree", "parallel", "dirwalk", "status"] } glob = "0.3.2" # Pinned due to https://github.com/sunng87/handlebars-rust/issues/711 handlebars = { version = "=6.3.1", features = ["dir_source"] } diff --git a/src/cargo/ops/cargo_package/mod.rs b/src/cargo/ops/cargo_package/mod.rs index 469233939..3aa0babbd 100644 --- a/src/cargo/ops/cargo_package/mod.rs +++ b/src/cargo/ops/cargo_package/mod.rs @@ -480,7 +480,6 @@ fn prepare_archive( // Check (git) repository state, getting the current commit hash. let vcs_info = vcs::check_repo_state(pkg, &src_files, ws, &opts)?; - build_ar_list(ws, pkg, src_files, vcs_info, opts.include_lockfile) } diff --git a/src/cargo/ops/cargo_package/vcs.rs b/src/cargo/ops/cargo_package/vcs.rs index 09107abf3..cbc899a4f 100644 --- a/src/cargo/ops/cargo_package/vcs.rs +++ b/src/cargo/ops/cargo_package/vcs.rs @@ -1,21 +1,19 @@ //! Helpers to gather the VCS information for `cargo package`. - -use std::collections::HashSet; -use std::path::Path; -use std::path::PathBuf; - -use anyhow::Context as _; -use cargo_util::paths; -use serde::Serialize; -use tracing::debug; - -use crate::CargoResult; -use crate::GlobalContext; -use crate::core::Package; -use crate::core::Workspace; +use crate::core::{Package, Workspace}; +use crate::ops::PackageOpts; use crate::sources::PathEntry; - -use super::PackageOpts; +use crate::{CargoResult, GlobalContext}; +use anyhow::Context; +use cargo_util::paths; +use gix::bstr::ByteSlice; +use gix::dir::walk::EmissionMode; +use gix::index::entry::Mode; +use gix::status::tree_index::TrackRenames; +use gix::worktree::stack::state::ignore::Source; +use serde::Serialize; +use std::collections::HashSet; +use std::path::{Path, PathBuf}; +use tracing::debug; /// Represents the VCS information when packaging. #[derive(Serialize)] @@ -29,7 +27,7 @@ pub struct VcsInfo { #[derive(Serialize)] pub struct GitVcsInfo { sha1: String, - /// Indicate whether or not the Git worktree is dirty. + /// Indicate whether the Git worktree is dirty. #[serde(skip_serializing_if = "std::ops::Not::not")] dirty: bool, } @@ -39,7 +37,7 @@ pub struct GitVcsInfo { /// If *git*, and the source is *dirty* (e.g., has uncommitted changes), /// and `--allow-dirty` has not been passed, /// then `bail!` with an informative message. -/// Otherwise return the sha1 hash of the current *HEAD* commit, +/// Otherwise, return the sha1 hash of the current *HEAD* commit, /// or `None` if no repo is found. #[tracing::instrument(skip_all)] pub fn check_repo_state( @@ -49,7 +47,7 @@ pub fn check_repo_state( opts: &PackageOpts<'_>, ) -> CargoResult> { let gctx = ws.gctx(); - let Ok(repo) = git2::Repository::discover(p.root()) else { + let Ok(repo) = gix::discover(p.root()) else { gctx.shell().verbose(|shell| { shell.warn(format_args!( "no (git) VCS found for `{}`", @@ -71,21 +69,33 @@ pub fn check_repo_state( debug!("found a git repo at `{}`", workdir.display()); let path = p.manifest_path(); + + let manifest_exists = path.exists(); let path = paths::strip_prefix_canonical(path, workdir).unwrap_or_else(|_| path.to_path_buf()); - let Ok(status) = repo.status_file(&path) else { + let rela_path = + gix::path::to_unix_separators_on_windows(gix::path::os_str_into_bstr(path.as_os_str())?); + if !manifest_exists { gctx.shell().verbose(|shell| { shell.warn(format_args!( - "no (git) Cargo.toml found at `{}` in workdir `{}`", + "Cargo.toml not found at `{}` in workdir `{}`", path.display(), workdir.display() )) })?; - // No checked-in `Cargo.toml` found. This package may be irrelevant. + // No `Cargo.toml` found. This package may be irrelevant. // Have to assume it is clean. return Ok(None); }; - if !(status & git2::Status::IGNORED).is_empty() { + let manifest_is_ignored = { + let index = repo.index_or_empty()?; + let mut excludes = + repo.excludes(&index, None, Source::WorktreeThenIdMappingIfNotSkipped)?; + excludes + .at_entry(rela_path.as_bstr(), Some(Mode::FILE))? + .is_excluded() + }; + if manifest_is_ignored { gctx.shell().verbose(|shell| { shell.warn(format_args!( "found (git) Cargo.toml ignored at `{}` in workdir `{}`", @@ -106,7 +116,7 @@ pub fn check_repo_state( workdir.display(), ); let Some(git) = git(ws, p, src_files, &repo, &opts)? else { - // If the git repo lacks essensial field like `sha1`, and since this field exists from the beginning, + // If the git repo lacks essential field like `sha1`, and since this field exists from the beginning, // then don't generate the corresponding file in order to maintain consistency with past behavior. return Ok(None); }; @@ -117,7 +127,7 @@ pub fn check_repo_state( .unwrap_or("") .replace("\\", "/"); - return Ok(Some(VcsInfo { git, path_in_vcs })); + Ok(Some(VcsInfo { git, path_in_vcs })) } /// Warns if any symlinks were checked out as plain text files. @@ -136,11 +146,11 @@ pub fn check_repo_state( fn warn_symlink_checked_out_as_plain_text_file( gctx: &GlobalContext, src_files: &[PathEntry], - repo: &git2::Repository, + repo: &gix::Repository, ) -> CargoResult<()> { if repo - .config() - .and_then(|c| c.get_bool("core.symlinks")) + .config_snapshot() + .boolean(&gix::config::tree::Core::SYMLINKS) .unwrap_or(true) { return Ok(()); @@ -149,8 +159,8 @@ fn warn_symlink_checked_out_as_plain_text_file( if src_files.iter().any(|f| f.maybe_plain_text_symlink()) { let mut shell = gctx.shell(); shell.warn(format_args!( - "found symbolic links that may be checked out as regular files for git repo at `{}`\n\ - This might cause the `.crate` file to include incorrect or incomplete files", + "found symbolic links that may be checked out as regular files for git repo at `{}/`\n\ + This might cause the `.crate` file to include incorrect or incomplete files", repo.workdir().unwrap().display(), ))?; let extra_note = if cfg!(windows) { @@ -171,7 +181,7 @@ fn git( ws: &Workspace<'_>, pkg: &Package, src_files: &[PathEntry], - repo: &git2::Repository, + repo: &gix::Repository, opts: &PackageOpts<'_>, ) -> CargoResult> { // This is a collection of any dirty or untracked files. This covers: @@ -179,13 +189,23 @@ fn git( // - untracked files (which are "new" worktree files) // - ignored (in case the user has an `include` directive that // conflicts with .gitignore). - let mut dirty_files = Vec::new(); - let pathspec = relative_pathspec(repo, pkg.root()); - collect_statuses(repo, &[pathspec.as_str()], &mut dirty_files)?; + let (mut dirty_files, mut dirty_files_outside_package_root) = (Vec::new(), Vec::new()); + let workdir = repo.workdir().unwrap(); + collect_statuses( + repo, + workdir, + relative_package_root(repo, pkg.root()).as_deref(), + &mut dirty_files, + &mut dirty_files_outside_package_root, + )?; // Include each submodule so that the error message can provide // specifically *which* files in a submodule are modified. - status_submodules(repo, &mut dirty_files)?; + status_submodules( + repo, + &mut dirty_files, + &mut dirty_files_outside_package_root, + )?; // Find the intersection of dirty in git, and the src_files that would // be packaged. This is a lazy n^2 check, but seems fine with @@ -193,9 +213,27 @@ fn git( let cwd = ws.gctx().cwd(); let mut dirty_src_files: Vec<_> = src_files .iter() - .filter(|src_file| dirty_files.iter().any(|path| src_file.starts_with(path))) + .filter(|src_file| { + if let Some(canon_src_file) = src_file.is_symlink_or_under_symlink().then(|| { + gix::path::realpath_opts( + &src_file, + ws.gctx().cwd(), + gix::path::realpath::MAX_SYMLINKS, + ) + .unwrap_or_else(|_| src_file.to_path_buf()) + }) { + dirty_files + .iter() + .any(|path| canon_src_file.starts_with(path)) + } else { + dirty_files.iter().any(|path| src_file.starts_with(path)) + } + }) .map(|p| p.as_ref()) - .chain(dirty_files_outside_pkg_root(ws, pkg, repo, src_files)?.iter()) + .chain( + dirty_files_outside_pkg_root(ws, pkg, &dirty_files_outside_package_root, src_files)? + .iter(), + ) .map(|path| { pathdiff::diff_paths(path, cwd) .as_ref() @@ -206,14 +244,9 @@ fn git( .collect(); let dirty = !dirty_src_files.is_empty(); if !dirty || opts.allow_dirty { - // Must check whetherthe repo has no commit firstly, otherwise `revparse_single` would fail on bare commit repo. - // Due to lacking the `sha1` field, it's better not record the `GitVcsInfo` for consistency. - if repo.is_empty()? { - return Ok(None); - } - let rev_obj = repo.revparse_single("HEAD")?; - Ok(Some(GitVcsInfo { - sha1: rev_obj.id().to_string(), + let maybe_head_id = repo.head()?.try_peel_to_id_in_place()?; + Ok(maybe_head_id.map(|id| GitVcsInfo { + sha1: id.to_string(), dirty, })) } else { @@ -228,6 +261,114 @@ fn git( } } +/// Helper to collect dirty statuses for a single repo. +/// `relative_package_root` is `Some` if the root is a sub-directory of the workdir. +/// Writes dirty files outside `relative_package_root` into `dirty_files_outside_package_root`, +/// and all *everything else* into `dirty_files`. +#[must_use] +fn collect_statuses( + repo: &gix::Repository, + workdir: &Path, + relative_package_root: Option<&Path>, + dirty_files: &mut Vec, + dirty_files_outside_package_root: &mut Vec, +) -> CargoResult<()> { + let statuses = repo + .status(gix::progress::Discard)? + .dirwalk_options(|opts| { + opts.emit_untracked(gix::dir::walk::EmissionMode::Matching) + // Also pick up ignored files or whole directories + // to specifically catch overzealously ignored source files. + // Later we will match these dirs by prefix, which is why collapsing + // them is desirable here. + .emit_ignored(Some(EmissionMode::CollapseDirectory)) + .emit_tracked(false) + .recurse_repositories(false) + .symlinks_to_directories_are_ignored_like_directories(true) + .emit_empty_directories(false) + }) + .tree_index_track_renames(TrackRenames::Disabled) + .index_worktree_submodules(None) + .into_iter(None /* pathspec patterns */) + .with_context(|| { + format!( + "failed to begin git status for repo {}", + repo.path().display() + ) + })?; + + for status in statuses { + let status = status.with_context(|| { + format!( + "failed to retrieve git status from repo {}", + repo.path().display() + ) + })?; + + let rel_path = gix::path::from_bstr(status.location()); + let path = workdir.join(&rel_path); + if relative_package_root.is_some_and(|pkg_root| !rel_path.starts_with(pkg_root)) { + dirty_files_outside_package_root.push(path); + continue; + } + + // It is OK to include Cargo.lock even if it is ignored. + if path.ends_with("Cargo.lock") + && matches!( + &status, + gix::status::Item::IndexWorktree( + gix::status::index_worktree::Item::DirectoryContents { entry, .. } + ) if matches!(entry.status, gix::dir::entry::Status::Ignored(_)) + ) + { + continue; + } + + dirty_files.push(path); + } + Ok(()) +} + +/// Helper to collect dirty statuses while recursing into submodules. +fn status_submodules( + repo: &gix::Repository, + dirty_files: &mut Vec, + dirty_files_outside_package_root: &mut Vec, +) -> CargoResult<()> { + let Some(submodules) = repo.submodules()? else { + return Ok(()); + }; + for submodule in submodules { + // Ignore submodules that don't open, they are probably not initialized. + // If its files are required, then the verification step should fail. + if let Some(sub_repo) = submodule.open()? { + let Some(workdir) = sub_repo.workdir() else { + continue; + }; + status_submodules(&sub_repo, dirty_files, dirty_files_outside_package_root)?; + collect_statuses( + &sub_repo, + workdir, + None, + dirty_files, + dirty_files_outside_package_root, + )?; + } + } + Ok(()) +} + +/// Make `pkg_root` relative to the `repo` workdir. +fn relative_package_root(repo: &gix::Repository, pkg_root: &Path) -> Option { + let workdir = repo.workdir().unwrap(); + let rela_root = pkg_root.strip_prefix(workdir).unwrap_or(Path::new("")); + if rela_root.as_os_str().is_empty() { + None + } else { + rela_root.to_owned().into() + } +} + /// Checks whether "included" source files outside package root have been modified. /// /// This currently looks at @@ -242,14 +383,10 @@ fn git( fn dirty_files_outside_pkg_root( ws: &Workspace<'_>, pkg: &Package, - repo: &git2::Repository, + dirty_files_outside_of_package_root: &[PathBuf], src_files: &[PathEntry], ) -> CargoResult> { let pkg_root = pkg.root(); - let workdir = repo.workdir().unwrap(); - - let mut dirty_files = HashSet::new(); - let meta = pkg.manifest().metadata(); let metadata_paths: Vec<_> = [&meta.license_file, &meta.readme] .into_iter() @@ -257,7 +394,7 @@ fn dirty_files_outside_pkg_root( .map(|path| paths::normalize_path(&pkg_root.join(path))) .collect(); - for rel_path in src_files + let dirty_files = src_files .iter() .filter(|p| p.is_symlink_or_under_symlink()) .map(|p| p.as_ref().as_path()) @@ -266,83 +403,19 @@ fn dirty_files_outside_pkg_root( // If inside package root. Don't bother checking git status. .filter(|p| paths::strip_prefix_canonical(p, pkg_root).is_err()) // Handle files outside package root but under git workdir, - .filter_map(|p| paths::strip_prefix_canonical(p, workdir).ok()) - { - match repo.status_file(&rel_path) { - Ok(git2::Status::CURRENT) => {} - Ok(_) => { - dirty_files.insert(workdir.join(rel_path)); - } - Err(e) => { - // Dirtiness check for symlinks is mostly informational. - // And changes in submodule would fail git-status as well (see #15384). - // To avoid adding complicated logic to handle that, - // 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() - ); - } - } - } + .filter_map(|src_file| { + let canon_src_path = gix::path::realpath_opts( + src_file, + ws.gctx().cwd(), + gix::path::realpath::MAX_SYMLINKS, + ) + .unwrap_or_else(|_| src_file.to_owned()); + + dirty_files_outside_of_package_root + .iter() + .any(|p| canon_src_path.starts_with(p)) + .then_some(canon_src_path) + }) + .collect(); Ok(dirty_files) } - -/// Helper to collect dirty statuses for a single repo. -fn collect_statuses( - repo: &git2::Repository, - pathspecs: &[&str], - dirty_files: &mut Vec, -) -> CargoResult<()> { - let mut status_opts = git2::StatusOptions::new(); - // Exclude submodules, as they are being handled manually by recursing - // into each one so that details about specific files can be - // retrieved. - pathspecs - .iter() - .fold(&mut status_opts, git2::StatusOptions::pathspec) - .exclude_submodules(true) - .include_ignored(true) - .include_untracked(true); - let repo_statuses = repo.statuses(Some(&mut status_opts)).with_context(|| { - format!( - "failed to retrieve git status from repo {}", - repo.path().display() - ) - })?; - let workdir = repo.workdir().unwrap(); - let this_dirty = repo_statuses.iter().filter_map(|entry| { - let path = entry.path().expect("valid utf-8 path"); - if path.ends_with("Cargo.lock") && entry.status() == git2::Status::IGNORED { - // It is OK to include Cargo.lock even if it is ignored. - return None; - } - // Use an absolute path, so that comparing paths is easier - // (particularly with submodules). - Some(workdir.join(path)) - }); - dirty_files.extend(this_dirty); - Ok(()) -} - -/// Helper to collect dirty statuses while recursing into submodules. -fn status_submodules(repo: &git2::Repository, dirty_files: &mut Vec) -> CargoResult<()> { - for submodule in repo.submodules()? { - // Ignore submodules that don't open, they are probably not initialized. - // If its files are required, then the verification step should fail. - if let Ok(sub_repo) = submodule.open() { - status_submodules(&sub_repo, dirty_files)?; - collect_statuses(&sub_repo, &[], dirty_files)?; - } - } - Ok(()) -} - -/// Use pathspec so git only matches a certain path prefix -fn relative_pathspec(repo: &git2::Repository, pkg_root: &Path) -> String { - let workdir = repo.workdir().unwrap(); - let relpath = pkg_root.strip_prefix(workdir).unwrap_or(Path::new("")); - // to unix separators - relpath.to_str().unwrap().replace('\\', "/") -} diff --git a/tests/testsuite/git.rs b/tests/testsuite/git.rs index cd86c8f0e..52fecb3a8 100644 --- a/tests/testsuite/git.rs +++ b/tests/testsuite/git.rs @@ -3162,9 +3162,10 @@ fn dirty_submodule() { .with_stderr_data(str![[r#" [WARNING] manifest has no description, license, license-file, documentation, homepage or repository. See https://doc.rust-lang.org/cargo/reference/manifest.html#package-metadata for more info. -[ERROR] 1 files in the working directory contain changes that were not yet committed into git: +[ERROR] 2 files in the working directory contain changes that were not yet committed into git: .gitmodules +src/lib.rs to proceed despite this and include the uncommitted changes, pass the `--allow-dirty` flag @@ -3208,9 +3209,10 @@ to proceed despite this and include the uncommitted changes, pass the `--allow-d .with_stderr_data(str![[r#" [WARNING] manifest has no description, license, license-file, documentation, homepage or repository. See https://doc.rust-lang.org/cargo/reference/manifest.html#package-metadata for more info. -[ERROR] 1 files in the working directory contain changes that were not yet committed into git: +[ERROR] 2 files in the working directory contain changes that were not yet committed into git: src/.gitmodules +src/bar/mod.rs to proceed despite this and include the uncommitted changes, pass the `--allow-dirty` flag diff --git a/tests/testsuite/package.rs b/tests/testsuite/package.rs index b3669244b..a0577f65a 100644 --- a/tests/testsuite/package.rs +++ b/tests/testsuite/package.rs @@ -1347,7 +1347,7 @@ fn dirty_file_outside_pkg_root_considered_dirty() { git::commit(&repo); // Changing files outside pkg root under situations below should be treated - // as dirty. `cargo package` is expected to fail on VCS stastus check. + // as dirty. `cargo package` is expected to fail on VCS status check. // // * Changes in files outside package root that source files symlink to p.change_file("README.md", "after"); @@ -1355,7 +1355,7 @@ fn dirty_file_outside_pkg_root_considered_dirty() { p.change_file("original-dir/file", "after"); // * Changes in files outside pkg root that `license-file`/`readme` point to p.change_file("LICENSE", "after"); - // * When workspace root manifest has changned, + // * When workspace root manifest has changed, // no matter whether workspace inheritance is involved. p.change_file( "Cargo.toml", @@ -1367,7 +1367,7 @@ fn dirty_file_outside_pkg_root_considered_dirty() { edition = "2021" "#, ); - // Changes in files outside git workdir won't affect vcs status check + // Changes in files outside git workdir won't affect VCS status check p.change_file( &main_outside_pkg_root, r#"fn main() { eprintln!("after"); }"#, @@ -1470,13 +1470,16 @@ fn dirty_file_outside_pkg_root_inside_submodule() { p.symlink("submodule/file.txt", "isengard/src/file.txt"); git::add(&repo); git::commit(&repo); - // This dirtyness should be detected in the future. p.change_file("submodule/file.txt", "changed"); p.cargo("package --workspace --no-verify") + .with_status(101) .with_stderr_data(str![[r#" -[PACKAGING] isengard v0.0.0 ([ROOT]/foo/isengard) -[PACKAGED] 6 files, [FILE_SIZE]B ([FILE_SIZE]B compressed) +[ERROR] 1 files in the working directory contain changes that were not yet committed into git: + +isengard/src/file.txt + +to proceed despite this and include the uncommitted changes, pass the `--allow-dirty` flag "#]]) .run();