Skip to content

Commit 3907e2d

Browse files
committed
refactor(package): extract vcs check to a separate module
1 parent b018327 commit 3907e2d

File tree

2 files changed

+252
-236
lines changed

2 files changed

+252
-236
lines changed

src/cargo/ops/cargo_package/mod.rs

Lines changed: 4 additions & 236 deletions
Original file line numberDiff line numberDiff line change
@@ -29,11 +29,14 @@ use anyhow::{bail, Context as _};
2929
use cargo_util::paths;
3030
use flate2::read::GzDecoder;
3131
use flate2::{Compression, GzBuilder};
32-
use serde::Serialize;
3332
use tar::{Archive, Builder, EntryType, Header, HeaderMode};
3433
use tracing::debug;
3534
use unicase::Ascii as UncasedAscii;
3635

36+
mod vcs;
37+
use self::vcs::check_repo_state;
38+
use self::vcs::VcsInfo;
39+
3740
#[derive(Clone)]
3841
pub struct PackageOpts<'gctx> {
3942
pub gctx: &'gctx GlobalContext,
@@ -78,21 +81,6 @@ enum GeneratedFile {
7881
VcsInfo(VcsInfo),
7982
}
8083

81-
#[derive(Serialize)]
82-
struct VcsInfo {
83-
git: GitVcsInfo,
84-
/// Path to the package within repo (empty string if root). / not \
85-
path_in_vcs: String,
86-
}
87-
88-
#[derive(Serialize)]
89-
struct GitVcsInfo {
90-
sha1: String,
91-
/// Indicate whether or not the Git worktree is dirty.
92-
#[serde(skip_serializing_if = "std::ops::Not::not")]
93-
dirty: bool,
94-
}
95-
9684
// Builds a tarball and places it in the output directory.
9785
#[tracing::instrument(skip_all)]
9886
fn create_package(
@@ -728,226 +716,6 @@ fn check_metadata(pkg: &Package, gctx: &GlobalContext) -> CargoResult<()> {
728716
Ok(())
729717
}
730718

731-
/// Checks if the package source is in a *git* DVCS repository. If *git*, and
732-
/// the source is *dirty* (e.g., has uncommitted changes), and `--allow-dirty`
733-
/// has not been passed, then `bail!` with an informative message. Otherwise
734-
/// return the sha1 hash of the current *HEAD* commit, or `None` if no repo is
735-
/// found.
736-
#[tracing::instrument(skip_all)]
737-
fn check_repo_state(
738-
p: &Package,
739-
src_files: &[PathBuf],
740-
gctx: &GlobalContext,
741-
opts: &PackageOpts<'_>,
742-
) -> CargoResult<Option<VcsInfo>> {
743-
let Ok(repo) = git2::Repository::discover(p.root()) else {
744-
gctx.shell().verbose(|shell| {
745-
shell.warn(format!("no (git) VCS found for `{}`", p.root().display()))
746-
})?;
747-
// No Git repo found. Have to assume it is clean.
748-
return Ok(None);
749-
};
750-
751-
let Some(workdir) = repo.workdir() else {
752-
debug!(
753-
"no (git) workdir found for repo at `{}`",
754-
repo.path().display()
755-
);
756-
// No git workdir. Have to assume it is clean.
757-
return Ok(None);
758-
};
759-
760-
debug!("found a git repo at `{}`", workdir.display());
761-
let path = p.manifest_path();
762-
let path = paths::strip_prefix_canonical(path, workdir).unwrap_or_else(|_| path.to_path_buf());
763-
let Ok(status) = repo.status_file(&path) else {
764-
gctx.shell().verbose(|shell| {
765-
shell.warn(format!(
766-
"no (git) Cargo.toml found at `{}` in workdir `{}`",
767-
path.display(),
768-
workdir.display()
769-
))
770-
})?;
771-
// No checked-in `Cargo.toml` found. This package may be irrelevant.
772-
// Have to assume it is clean.
773-
return Ok(None);
774-
};
775-
776-
if !(status & git2::Status::IGNORED).is_empty() {
777-
gctx.shell().verbose(|shell| {
778-
shell.warn(format!(
779-
"found (git) Cargo.toml ignored at `{}` in workdir `{}`",
780-
path.display(),
781-
workdir.display()
782-
))
783-
})?;
784-
// An ignored `Cargo.toml` found. This package may be irrelevant.
785-
// Have to assume it is clean.
786-
return Ok(None);
787-
}
788-
789-
debug!(
790-
"found (git) Cargo.toml at `{}` in workdir `{}`",
791-
path.display(),
792-
workdir.display(),
793-
);
794-
let path_in_vcs = path
795-
.parent()
796-
.and_then(|p| p.to_str())
797-
.unwrap_or("")
798-
.replace("\\", "/");
799-
let Some(git) = git(p, gctx, src_files, &repo, &opts)? else {
800-
// If the git repo lacks essensial field like `sha1`, and since this field exists from the beginning,
801-
// then don't generate the corresponding file in order to maintain consistency with past behavior.
802-
return Ok(None);
803-
};
804-
805-
return Ok(Some(VcsInfo { git, path_in_vcs }));
806-
807-
fn git(
808-
pkg: &Package,
809-
gctx: &GlobalContext,
810-
src_files: &[PathBuf],
811-
repo: &git2::Repository,
812-
opts: &PackageOpts<'_>,
813-
) -> CargoResult<Option<GitVcsInfo>> {
814-
// This is a collection of any dirty or untracked files. This covers:
815-
// - new/modified/deleted/renamed/type change (index or worktree)
816-
// - untracked files (which are "new" worktree files)
817-
// - ignored (in case the user has an `include` directive that
818-
// conflicts with .gitignore).
819-
let mut dirty_files = Vec::new();
820-
collect_statuses(repo, &mut dirty_files)?;
821-
// Include each submodule so that the error message can provide
822-
// specifically *which* files in a submodule are modified.
823-
status_submodules(repo, &mut dirty_files)?;
824-
825-
// Find the intersection of dirty in git, and the src_files that would
826-
// be packaged. This is a lazy n^2 check, but seems fine with
827-
// thousands of files.
828-
let cwd = gctx.cwd();
829-
let mut dirty_src_files: Vec<_> = src_files
830-
.iter()
831-
.filter(|src_file| dirty_files.iter().any(|path| src_file.starts_with(path)))
832-
.chain(dirty_metadata_paths(pkg, repo)?.iter())
833-
.map(|path| {
834-
pathdiff::diff_paths(path, cwd)
835-
.as_ref()
836-
.unwrap_or(path)
837-
.display()
838-
.to_string()
839-
})
840-
.collect();
841-
let dirty = !dirty_src_files.is_empty();
842-
if !dirty || opts.allow_dirty {
843-
// Must check whetherthe repo has no commit firstly, otherwise `revparse_single` would fail on bare commit repo.
844-
// Due to lacking the `sha1` field, it's better not record the `GitVcsInfo` for consistency.
845-
if repo.is_empty()? {
846-
return Ok(None);
847-
}
848-
let rev_obj = repo.revparse_single("HEAD")?;
849-
Ok(Some(GitVcsInfo {
850-
sha1: rev_obj.id().to_string(),
851-
dirty,
852-
}))
853-
} else {
854-
dirty_src_files.sort_unstable();
855-
anyhow::bail!(
856-
"{} files in the working directory contain changes that were \
857-
not yet committed into git:\n\n{}\n\n\
858-
to proceed despite this and include the uncommitted changes, pass the `--allow-dirty` flag",
859-
dirty_src_files.len(),
860-
dirty_src_files.join("\n")
861-
)
862-
}
863-
}
864-
865-
/// Checks whether files at paths specified in `package.readme` and
866-
/// `package.license-file` have been modified.
867-
///
868-
/// This is required because those paths may link to a file outside the
869-
/// current package root, but still under the git workdir, affecting the
870-
/// final packaged `.crate` file.
871-
fn dirty_metadata_paths(pkg: &Package, repo: &git2::Repository) -> CargoResult<Vec<PathBuf>> {
872-
let mut dirty_files = Vec::new();
873-
let workdir = repo.workdir().unwrap();
874-
let root = pkg.root();
875-
let meta = pkg.manifest().metadata();
876-
for path in [&meta.license_file, &meta.readme] {
877-
let Some(path) = path.as_deref().map(Path::new) else {
878-
continue;
879-
};
880-
let abs_path = paths::normalize_path(&root.join(path));
881-
if paths::strip_prefix_canonical(abs_path.as_path(), root).is_ok() {
882-
// Inside package root. Don't bother checking git status.
883-
continue;
884-
}
885-
if let Ok(rel_path) = paths::strip_prefix_canonical(abs_path.as_path(), workdir) {
886-
// Outside package root but under git workdir,
887-
if repo.status_file(&rel_path)? != git2::Status::CURRENT {
888-
dirty_files.push(if abs_path.is_symlink() {
889-
// For symlinks, shows paths to symlink sources
890-
workdir.join(rel_path)
891-
} else {
892-
abs_path
893-
});
894-
}
895-
}
896-
}
897-
Ok(dirty_files)
898-
}
899-
900-
// Helper to collect dirty statuses for a single repo.
901-
fn collect_statuses(
902-
repo: &git2::Repository,
903-
dirty_files: &mut Vec<PathBuf>,
904-
) -> CargoResult<()> {
905-
let mut status_opts = git2::StatusOptions::new();
906-
// Exclude submodules, as they are being handled manually by recursing
907-
// into each one so that details about specific files can be
908-
// retrieved.
909-
status_opts
910-
.exclude_submodules(true)
911-
.include_ignored(true)
912-
.include_untracked(true);
913-
let repo_statuses = repo.statuses(Some(&mut status_opts)).with_context(|| {
914-
format!(
915-
"failed to retrieve git status from repo {}",
916-
repo.path().display()
917-
)
918-
})?;
919-
let workdir = repo.workdir().unwrap();
920-
let this_dirty = repo_statuses.iter().filter_map(|entry| {
921-
let path = entry.path().expect("valid utf-8 path");
922-
if path.ends_with("Cargo.lock") && entry.status() == git2::Status::IGNORED {
923-
// It is OK to include Cargo.lock even if it is ignored.
924-
return None;
925-
}
926-
// Use an absolute path, so that comparing paths is easier
927-
// (particularly with submodules).
928-
Some(workdir.join(path))
929-
});
930-
dirty_files.extend(this_dirty);
931-
Ok(())
932-
}
933-
934-
// Helper to collect dirty statuses while recursing into submodules.
935-
fn status_submodules(
936-
repo: &git2::Repository,
937-
dirty_files: &mut Vec<PathBuf>,
938-
) -> CargoResult<()> {
939-
for submodule in repo.submodules()? {
940-
// Ignore submodules that don't open, they are probably not initialized.
941-
// If its files are required, then the verification step should fail.
942-
if let Ok(sub_repo) = submodule.open() {
943-
status_submodules(&sub_repo, dirty_files)?;
944-
collect_statuses(&sub_repo, dirty_files)?;
945-
}
946-
}
947-
Ok(())
948-
}
949-
}
950-
951719
/// Compresses and packages a list of [`ArchiveFile`]s and writes into the given file.
952720
///
953721
/// Returns the uncompressed size of the contents of the new archive file.

0 commit comments

Comments
 (0)