diff --git a/Cargo.lock b/Cargo.lock index 0b921ebc5a6f6..7a72650696612 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6351,10 +6351,10 @@ name = "turborepo-fs" version = "0.1.0" dependencies = [ "fs-err", + "ignore", "tempfile", "thiserror", "turbopath", - "walkdir", ] [[package]] diff --git a/crates/turborepo-fs/Cargo.toml b/crates/turborepo-fs/Cargo.toml index c7fa2c17646dd..83c26d5f84fd0 100644 --- a/crates/turborepo-fs/Cargo.toml +++ b/crates/turborepo-fs/Cargo.toml @@ -11,9 +11,9 @@ workspace = true [dependencies] fs-err = "2.9.0" +ignore = "0.4.22" thiserror = "1.0.38" turbopath = { workspace = true } -walkdir = "2.3.3" [dev-dependencies] tempfile = { workspace = true } diff --git a/crates/turborepo-fs/src/lib.rs b/crates/turborepo-fs/src/lib.rs index 06fc7d4bf065f..9248a42ed355b 100644 --- a/crates/turborepo-fs/src/lib.rs +++ b/crates/turborepo-fs/src/lib.rs @@ -6,8 +6,8 @@ use std::{ }; use fs_err as fs; +use ignore::WalkBuilder; use turbopath::{AbsoluteSystemPath, AnchoredSystemPathBuf}; -use walkdir::WalkDir; #[derive(Debug, thiserror::Error)] pub enum Error { @@ -16,7 +16,7 @@ pub enum Error { #[error(transparent)] Io(#[from] io::Error), #[error("error walking directory during recursive copy: {0}")] - Walk(#[from] walkdir::Error), + Walk(#[from] ignore::Error), } pub fn recursive_copy( @@ -28,8 +28,14 @@ pub fn recursive_copy( let src_metadata = src.symlink_metadata()?; if src_metadata.is_dir() { - let walker = WalkDir::new(src.as_path()).follow_links(false); - for entry in walker.into_iter() { + let walker = WalkBuilder::new(src.as_path()) + .hidden(false) + .git_ignore(true) + .git_global(false) + .git_exclude(true) + .build(); + + for entry in walker { match entry { Err(e) => { if e.io_error().is_some() { @@ -43,7 +49,9 @@ pub fn recursive_copy( Ok(entry) => { let path = entry.path(); let path = AbsoluteSystemPath::from_std_path(path)?; - let file_type = entry.file_type(); + let file_type = entry + .file_type() + .expect("all dir entries aside from stdin should have a file type"); // Note that we also don't currently copy broken symlinks if file_type.is_symlink() && path.stat().is_err() { @@ -310,6 +318,51 @@ mod tests { Ok(()) } + #[test] + fn test_recursive_copy_gitignore() -> Result<(), Error> { + // Directory layout: + // + // / + // .gitignore + // invisible.txt <- ignored + // dist/ <- ignored + // output.txt + // child/ + // seen.txt + // .hidden + let (_src_tmp, src_dir) = tmp_dir()?; + // Need to create this for `.gitignore` to be respected + src_dir.join_component(".git").create_dir_all()?; + src_dir + .join_component(".gitignore") + .create_with_contents("invisible.txt\ndist/\n")?; + src_dir + .join_component("invisible.txt") + .create_with_contents("not here")?; + let output = src_dir.join_components(&["dist", "output.txt"]); + output.ensure_dir()?; + output.create_with_contents("hi!")?; + + let child = src_dir.join_component("child"); + let seen = child.join_component("seen.txt"); + seen.ensure_dir()?; + seen.create_with_contents("here")?; + let hidden = child.join_component(".hidden"); + hidden.create_with_contents("polo")?; + + let (_dst_tmp, dst_dir) = tmp_dir()?; + recursive_copy(&src_dir, &dst_dir)?; + + assert!(dst_dir.join_component(".gitignore").exists()); + assert!(!dst_dir.join_component("invisible.txt").exists()); + assert!(!dst_dir.join_component("dist").exists()); + assert!(dst_dir.join_component("child").exists()); + assert!(dst_dir.join_components(&["child", "seen.txt"]).exists()); + assert!(dst_dir.join_components(&["child", ".hidden"]).exists()); + + Ok(()) + } + fn assert_file_matches(a: impl AsRef, b: impl AsRef) { let a = a.as_ref(); let b = b.as_ref();