mirror of
https://github.com/rust-lang/cargo.git
synced 2025-10-01 11:30:39 +00:00
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.
This commit is contained in:
parent
ac04a82c7f
commit
d3b85cd96a
74
Cargo.lock
generated
74
Cargo.lock
generated
@ -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]]
|
||||
|
@ -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"] }
|
||||
|
@ -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)
|
||||
}
|
||||
|
||||
|
@ -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<Option<VcsInfo>> {
|
||||
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,7 +159,7 @@ 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\
|
||||
"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(),
|
||||
))?;
|
||||
@ -171,7 +181,7 @@ fn git(
|
||||
ws: &Workspace<'_>,
|
||||
pkg: &Package,
|
||||
src_files: &[PathEntry],
|
||||
repo: &git2::Repository,
|
||||
repo: &gix::Repository,
|
||||
opts: &PackageOpts<'_>,
|
||||
) -> CargoResult<Option<GitVcsInfo>> {
|
||||
// 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<PathBuf>,
|
||||
dirty_files_outside_package_root: &mut Vec<PathBuf>,
|
||||
) -> 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<PathBuf>,
|
||||
dirty_files_outside_package_root: &mut Vec<PathBuf>,
|
||||
) -> 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<PathBuf> {
|
||||
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<HashSet<PathBuf>> {
|
||||
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<PathBuf>,
|
||||
) -> 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<PathBuf>) -> 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('\\', "/")
|
||||
}
|
||||
|
@ -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
|
||||
|
||||
|
@ -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();
|
||||
|
Loading…
x
Reference in New Issue
Block a user