From be9d195a3b028994e5c022c4a8c16be08cd0ffe1 Mon Sep 17 00:00:00 2001 From: Chris Olszewski Date: Fri, 24 Jan 2025 11:41:57 -0500 Subject: [PATCH 1/3] feat(fs): make gitignore configurable --- Cargo.lock | 1 + crates/turborepo-fs/Cargo.toml | 1 + crates/turborepo-fs/src/lib.rs | 24 ++++++++++++++-------- crates/turborepo-lib/src/commands/prune.rs | 8 ++++---- 4 files changed, 21 insertions(+), 13 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 8dd037cedb45f..43a1541d7e2e6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6353,6 +6353,7 @@ dependencies = [ "fs-err", "ignore", "tempfile", + "test-case", "thiserror", "turbopath", ] diff --git a/crates/turborepo-fs/Cargo.toml b/crates/turborepo-fs/Cargo.toml index 83c26d5f84fd0..00207148f26aa 100644 --- a/crates/turborepo-fs/Cargo.toml +++ b/crates/turborepo-fs/Cargo.toml @@ -17,3 +17,4 @@ turbopath = { workspace = true } [dev-dependencies] tempfile = { workspace = true } +test-case = { workspace = true } diff --git a/crates/turborepo-fs/src/lib.rs b/crates/turborepo-fs/src/lib.rs index cb23fef9e830a..aa45871e2440f 100644 --- a/crates/turborepo-fs/src/lib.rs +++ b/crates/turborepo-fs/src/lib.rs @@ -22,6 +22,7 @@ pub enum Error { pub fn recursive_copy( src: impl AsRef, dst: impl AsRef, + use_gitignore: bool, ) -> Result<(), Error> { let src = src.as_ref(); let dst = dst.as_ref(); @@ -30,9 +31,9 @@ pub fn recursive_copy( if src_metadata.is_dir() { let walker = WalkBuilder::new(src.as_path()) .hidden(false) - .git_ignore(true) + .git_ignore(use_gitignore) .git_global(false) - .git_exclude(true) + .git_exclude(use_gitignore) .build(); for entry in walker { @@ -127,6 +128,7 @@ fn copy_file_with_type( mod tests { use std::path::Path; + use test_case::test_case; use turbopath::AbsoluteSystemPathBuf; use super::*; @@ -277,10 +279,10 @@ mod tests { let (_dst_tmp, dst_dir) = tmp_dir()?; - recursive_copy(&src_dir, &dst_dir)?; + recursive_copy(&src_dir, &dst_dir, true)?; // Ensure double copy doesn't error - recursive_copy(&src_dir, &dst_dir)?; + recursive_copy(&src_dir, &dst_dir, true)?; let dst_child_path = dst_dir.join_component("child"); let dst_a_path = dst_child_path.join_component("a"); @@ -318,8 +320,9 @@ mod tests { Ok(()) } - #[test] - fn test_recursive_copy_gitignore() -> Result<(), Error> { + #[test_case(true)] + #[test_case(false)] + fn test_recursive_copy_gitignore(use_gitignore: bool) -> Result<(), Error> { // Directory layout: // // / @@ -351,11 +354,14 @@ mod tests { hidden.create_with_contents("polo")?; let (_dst_tmp, dst_dir) = tmp_dir()?; - recursive_copy(&src_dir, &dst_dir)?; + recursive_copy(&src_dir, &dst_dir, use_gitignore)?; assert!(dst_dir.join_component(".gitignore").exists()); - assert!(!dst_dir.join_component("invisible.txt").exists()); - assert!(!dst_dir.join_component("dist").exists()); + assert_eq!( + !dst_dir.join_component("invisible.txt").exists(), + use_gitignore + ); + assert_eq!(!dst_dir.join_component("dist").exists(), use_gitignore); assert!(dst_dir.join_component("child").exists()); assert!(dst_dir.join_components(&["child", "seen.txt"]).exists()); assert!(dst_dir.join_components(&["child", ".hidden"]).exists()); diff --git a/crates/turborepo-lib/src/commands/prune.rs b/crates/turborepo-lib/src/commands/prune.rs index cde1cffc41d5c..8a3f7cc0ab62e 100644 --- a/crates/turborepo-lib/src/commands/prune.rs +++ b/crates/turborepo-lib/src/commands/prune.rs @@ -373,10 +373,10 @@ impl<'a> Prune<'a> { return Ok(()); } let full_to = self.full_directory.resolve(path); - turborepo_fs::recursive_copy(&from_path, full_to)?; + turborepo_fs::recursive_copy(&from_path, full_to, true)?; if matches!(destination, Some(CopyDestination::All)) { let out_to = self.out_directory.resolve(path); - turborepo_fs::recursive_copy(&from_path, out_to)?; + turborepo_fs::recursive_copy(&from_path, out_to, true)?; } if self.docker && matches!( @@ -385,7 +385,7 @@ impl<'a> Prune<'a> { ) { let docker_to = self.docker_directory().resolve(path); - turborepo_fs::recursive_copy(&from_path, docker_to)?; + turborepo_fs::recursive_copy(&from_path, docker_to, true)?; } Ok(()) } @@ -400,7 +400,7 @@ impl<'a> Prune<'a> { let target_dir = self.full_directory.resolve(&relative_workspace_dir); target_dir.create_dir_all_with_permissions(metadata.permissions())?; - turborepo_fs::recursive_copy(original_dir, &target_dir)?; + turborepo_fs::recursive_copy(original_dir, &target_dir, true)?; if self.docker { let docker_workspace_dir = self.docker_directory().resolve(&relative_workspace_dir); From a8ae33b0f1dab24aac15ec04061f525f2161f2c2 Mon Sep 17 00:00:00 2001 From: Chris Olszewski Date: Fri, 24 Jan 2025 14:08:36 -0500 Subject: [PATCH 2/3] feat(prune): add --use-gitignore flag --- Cargo.lock | 16 ++--- crates/turborepo-lib/src/cli/mod.rs | 76 +++++++++++++++++++++- crates/turborepo-lib/src/commands/prune.rs | 14 ++-- 3 files changed, 91 insertions(+), 15 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 43a1541d7e2e6..96a096cdd5add 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1080,9 +1080,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.17" +version = "4.5.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e5a21b8495e732f1b3c364c9949b201ca7bae518c502c80256c96ad79eaf6ac" +checksum = "769b0145982b4b48713e01ec42d61614425f27b7058bda7180a3a41f30104796" dependencies = [ "clap_builder", "clap_derive", @@ -1090,9 +1090,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.17" +version = "4.5.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8cf2dd12af7a047ad9d6da2b6b249759a22a7abc0f474c1dae1777afa4b21a73" +checksum = "1b26884eb4b57140e4d2d93652abfa49498b938b3c9179f9fc487b0acc3edad7" dependencies = [ "anstream", "anstyle", @@ -1111,9 +1111,9 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.5.13" +version = "4.5.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "501d359d5f3dcaf6ecdeee48833ae73ec6e42723a1e52419c79abf9507eec0a0" +checksum = "54b755194d6389280185988721fffba69495eed5ee9feeee9a599b53db80318c" dependencies = [ "heck 0.5.0", "proc-macro2", @@ -1123,9 +1123,9 @@ dependencies = [ [[package]] name = "clap_lex" -version = "0.7.0" +version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "98cc8fbded0c607b7ba9dd60cd98df59af97e84d24e49c8557331cfc26d301ce" +checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6" [[package]] name = "clipboard-win" diff --git a/crates/turborepo-lib/src/cli/mod.rs b/crates/turborepo-lib/src/cli/mod.rs index 0afce342631b3..5ccbbe26016ae 100644 --- a/crates/turborepo-lib/src/cli/mod.rs +++ b/crates/turborepo-lib/src/cli/mod.rs @@ -679,6 +679,9 @@ pub enum Command { docker: bool, #[clap(long = "out-dir", default_value_t = String::from(prune::DEFAULT_OUTPUT_DIR), value_parser)] output_dir: String, + /// Respect `.gitignore` when copying files to + #[clap(long, default_missing_value = "true", num_args = 0..=1, require_equals = true)] + use_gitignore: Option, }, /// Run tasks across projects in your monorepo @@ -1528,6 +1531,7 @@ pub async fn run( scope_arg, docker, output_dir, + use_gitignore, } => { let event = CommandEventBuilder::new("prune").with_parent(&root_telemetry); event.track_call(); @@ -1538,10 +1542,19 @@ pub async fn run( .unwrap_or_default(); let docker = *docker; let output_dir = output_dir.clone(); + let use_gitignore = use_gitignore.unwrap_or(true); let base = CommandBase::new(cli_args, repo_root, version, color_config)?; event.track_ui_mode(base.opts.run_opts.ui_mode); let event_child = event.child(); - prune::prune(&base, &scope, docker, &output_dir, event_child).await?; + prune::prune( + &base, + &scope, + docker, + &output_dir, + use_gitignore, + event_child, + ) + .await?; Ok(0) } Command::Completion { shell } => { @@ -2588,6 +2601,7 @@ mod test { scope_arg: Some(vec!["foo".into()]), docker: false, output_dir: "out".to_string(), + use_gitignore: None, }; assert_eq!( @@ -2618,6 +2632,7 @@ mod test { scope_arg: None, docker: false, output_dir: "out".to_string(), + use_gitignore: None, }), ..Args::default() } @@ -2631,6 +2646,7 @@ mod test { scope_arg: Some(vec!["foo".to_string(), "bar".to_string()]), docker: false, output_dir: "out".to_string(), + use_gitignore: None, }), ..Args::default() } @@ -2644,6 +2660,7 @@ mod test { scope_arg: Some(vec!["foo".into()]), docker: true, output_dir: "out".to_string(), + use_gitignore: None, }), ..Args::default() } @@ -2657,6 +2674,7 @@ mod test { scope_arg: Some(vec!["foo".into()]), docker: false, output_dir: "dist".to_string(), + use_gitignore: None, }), ..Args::default() } @@ -2672,6 +2690,7 @@ mod test { scope_arg: Some(vec!["foo".into()]), docker: true, output_dir: "dist".to_string(), + use_gitignore: None, }), ..Args::default() }, @@ -2688,6 +2707,7 @@ mod test { scope_arg: Some(vec!["foo".into()]), docker: true, output_dir: "dist".to_string(), + use_gitignore: None, }), cwd: Some(Utf8PathBuf::from("../examples/with-yarn")), ..Args::default() @@ -2709,6 +2729,58 @@ mod test { scope_arg: None, docker: true, output_dir: "dist".to_string(), + use_gitignore: None, + }), + ..Args::default() + }, + } + .test(); + + CommandTestCase { + command: "prune", + command_args: vec![vec!["foo"], vec!["--use-gitignore"]], + global_args: vec![], + expected_output: Args { + command: Some(Command::Prune { + scope: None, + scope_arg: Some(vec!["foo".to_string()]), + docker: false, + output_dir: "out".to_string(), + use_gitignore: Some(true), + }), + ..Args::default() + }, + } + .test(); + + CommandTestCase { + command: "prune", + command_args: vec![vec!["foo"], vec!["--use-gitignore=true"]], + global_args: vec![], + expected_output: Args { + command: Some(Command::Prune { + scope: None, + scope_arg: Some(vec!["foo".to_string()]), + docker: false, + output_dir: "out".to_string(), + use_gitignore: Some(true), + }), + ..Args::default() + }, + } + .test(); + + CommandTestCase { + command: "prune", + command_args: vec![vec!["foo"], vec!["--use-gitignore=false"]], + global_args: vec![], + expected_output: Args { + command: Some(Command::Prune { + scope: None, + scope_arg: Some(vec!["foo".to_string()]), + docker: false, + output_dir: "out".to_string(), + use_gitignore: Some(false), }), ..Args::default() }, @@ -3000,7 +3072,7 @@ mod test { assert!(inferred_run .execution_args .as_ref() - .map_or(false, |e| e.single_package)); + .is_some_and(|e| e.single_package)); assert!(explicit_run .command .as_ref() diff --git a/crates/turborepo-lib/src/commands/prune.rs b/crates/turborepo-lib/src/commands/prune.rs index 8a3f7cc0ab62e..7ffee275f3f9e 100644 --- a/crates/turborepo-lib/src/commands/prune.rs +++ b/crates/turborepo-lib/src/commands/prune.rs @@ -93,12 +93,13 @@ pub async fn prune( scope: &[String], docker: bool, output_dir: &str, + use_gitignore: bool, telemetry: CommandEventBuilder, ) -> Result<(), Error> { telemetry.track_arg_usage("docker", docker); telemetry.track_arg_usage("out-dir", output_dir != DEFAULT_OUTPUT_DIR); - let prune = Prune::new(base, scope, docker, output_dir, telemetry).await?; + let prune = Prune::new(base, scope, docker, output_dir, use_gitignore, telemetry).await?; if matches!( prune.package_graph.package_manager(), @@ -244,6 +245,7 @@ struct Prune<'a> { full_directory: AbsoluteSystemPathBuf, docker: bool, scope: &'a [String], + use_gitignore: bool, } #[derive(Copy, Clone, PartialEq, Eq)] @@ -261,6 +263,7 @@ impl<'a> Prune<'a> { scope: &'a [String], docker: bool, output_dir: &str, + use_gitignore: bool, telemetry: CommandEventBuilder, ) -> Result { let allow_missing_package_manager = base.opts().repo_opts.allow_no_package_manager; @@ -327,6 +330,7 @@ impl<'a> Prune<'a> { full_directory, docker, scope, + use_gitignore, }) } @@ -373,10 +377,10 @@ impl<'a> Prune<'a> { return Ok(()); } let full_to = self.full_directory.resolve(path); - turborepo_fs::recursive_copy(&from_path, full_to, true)?; + turborepo_fs::recursive_copy(&from_path, full_to, self.use_gitignore)?; if matches!(destination, Some(CopyDestination::All)) { let out_to = self.out_directory.resolve(path); - turborepo_fs::recursive_copy(&from_path, out_to, true)?; + turborepo_fs::recursive_copy(&from_path, out_to, self.use_gitignore)?; } if self.docker && matches!( @@ -385,7 +389,7 @@ impl<'a> Prune<'a> { ) { let docker_to = self.docker_directory().resolve(path); - turborepo_fs::recursive_copy(&from_path, docker_to, true)?; + turborepo_fs::recursive_copy(&from_path, docker_to, self.use_gitignore)?; } Ok(()) } @@ -400,7 +404,7 @@ impl<'a> Prune<'a> { let target_dir = self.full_directory.resolve(&relative_workspace_dir); target_dir.create_dir_all_with_permissions(metadata.permissions())?; - turborepo_fs::recursive_copy(original_dir, &target_dir, true)?; + turborepo_fs::recursive_copy(original_dir, &target_dir, self.use_gitignore)?; if self.docker { let docker_workspace_dir = self.docker_directory().resolve(&relative_workspace_dir); From 8bfe38cb44fcab749fd832fdac31f4443aca4360 Mon Sep 17 00:00:00 2001 From: Chris Olszewski Date: Fri, 24 Jan 2025 14:15:50 -0500 Subject: [PATCH 3/3] docs(prune): document --use-gitignore --- docs/repo-docs/reference/prune.mdx | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/docs/repo-docs/reference/prune.mdx b/docs/repo-docs/reference/prune.mdx index cff6d8ae52ff9..5c509c530b2db 100644 --- a/docs/repo-docs/reference/prune.mdx +++ b/docs/repo-docs/reference/prune.mdx @@ -201,3 +201,9 @@ Using the same example from above, running `turbo prune frontend --docker` will Defaults to `./out`. Customize the directory the pruned output is generated in. + +#### `--use-gitignore[=]` + +Default: `true` + +Respect `.gitignore` file(s) when copying files to the output directory.