Skip to content

Commit

Permalink
feat(prune): add --use-gitignore flag (#9797)
Browse files Browse the repository at this point in the history
### Description

Closes #9789

Adds a flag for opting out of respecting `.gitignore`. If you want to
copy over files that are ignored by `git` you can now pass
`--use-gitignore=false` to `turbo prune`.

### Testing Instructions

Added unit tests. Quick manual test:
```
# A gitignored file in the app
[0 olszewski@chriss-mbp] /tmp/turborepo-gitignore-bug $ stat apps/web/.turbo       
16777234 160702923 drwxr-xr-x 6 olszewski wheel 0 192 "Jan 24 14:05:04 2025" "Jan 24 14:05:04 2025" "Jan 24 14:05:04 2025" "Jan 24 14:05:04 2025" 4096 0 0 .turbo
[0 olszewski@chriss-mbp] /tmp/turborepo-gitignore-bug $ turbo_dev --skip-infer prune web --use-gitignore=false
turbo 2.3.4

Generating pruned monorepo for web in /private/tmp/turborepo-gitignore-bug/out
 - Added @repo/eslint-config
 - Added @repo/typescript-config
 - Added @repo/ui
 - Added web
[0 olszewski@chriss-mbp] /tmp/turborepo-gitignore-bug $ stat out/apps/web/.turbo/                
16777234 160756779 drwxr-xr-x 3 olszewski wheel 0 96 "Jan 24 14:07:47 2025" "Jan 24 14:07:30 2025" "Jan 24 14:07:30 2025" "Jan 24 14:07:30 2025" 4096 0 0 out/apps/web/.turbo/
[0 olszewski@chriss-mbp] /tmp/turborepo-gitignore-bug $ rm -r out 
[0 olszewski@chriss-mbp] /tmp/turborepo-gitignore-bug $ turbo_dev --skip-infer prune web --use-gitignore      
turbo 2.3.4

Generating pruned monorepo for web in /private/tmp/turborepo-gitignore-bug/out
 - Added @repo/eslint-config
 - Added @repo/typescript-config
 - Added @repo/ui
 - Added web
[0 olszewski@chriss-mbp] /tmp/turborepo-gitignore-bug $ stat out/apps/web/.turbo/                       
stat: out/apps/web/.turbo/: stat: No such file or directory
```
  • Loading branch information
chris-olszewski authored Jan 24, 2025
1 parent 02fc06c commit 878a22a
Show file tree
Hide file tree
Showing 6 changed files with 114 additions and 24 deletions.
17 changes: 9 additions & 8 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions crates/turborepo-fs/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,4 @@ turbopath = { workspace = true }

[dev-dependencies]
tempfile = { workspace = true }
test-case = { workspace = true }
24 changes: 15 additions & 9 deletions crates/turborepo-fs/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ pub enum Error {
pub fn recursive_copy(
src: impl AsRef<AbsoluteSystemPath>,
dst: impl AsRef<AbsoluteSystemPath>,
use_gitignore: bool,
) -> Result<(), Error> {
let src = src.as_ref();
let dst = dst.as_ref();
Expand All @@ -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 {
Expand Down Expand Up @@ -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::*;
Expand Down Expand Up @@ -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");
Expand Down Expand Up @@ -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:
//
// <src>/
Expand Down Expand Up @@ -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());
Expand Down
76 changes: 74 additions & 2 deletions crates/turborepo-lib/src/cli/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 <OUT-DIR>
#[clap(long, default_missing_value = "true", num_args = 0..=1, require_equals = true)]
use_gitignore: Option<bool>,
},

/// Run tasks across projects in your monorepo
Expand Down Expand Up @@ -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();
Expand All @@ -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 } => {
Expand Down Expand Up @@ -2588,6 +2601,7 @@ mod test {
scope_arg: Some(vec!["foo".into()]),
docker: false,
output_dir: "out".to_string(),
use_gitignore: None,
};

assert_eq!(
Expand Down Expand Up @@ -2618,6 +2632,7 @@ mod test {
scope_arg: None,
docker: false,
output_dir: "out".to_string(),
use_gitignore: None,
}),
..Args::default()
}
Expand All @@ -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()
}
Expand All @@ -2644,6 +2660,7 @@ mod test {
scope_arg: Some(vec!["foo".into()]),
docker: true,
output_dir: "out".to_string(),
use_gitignore: None,
}),
..Args::default()
}
Expand All @@ -2657,6 +2674,7 @@ mod test {
scope_arg: Some(vec!["foo".into()]),
docker: false,
output_dir: "dist".to_string(),
use_gitignore: None,
}),
..Args::default()
}
Expand All @@ -2672,6 +2690,7 @@ mod test {
scope_arg: Some(vec!["foo".into()]),
docker: true,
output_dir: "dist".to_string(),
use_gitignore: None,
}),
..Args::default()
},
Expand All @@ -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()
Expand All @@ -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()
},
Expand Down Expand Up @@ -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()
Expand Down
14 changes: 9 additions & 5 deletions crates/turborepo-lib/src/commands/prune.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
Expand Down Expand Up @@ -244,6 +245,7 @@ struct Prune<'a> {
full_directory: AbsoluteSystemPathBuf,
docker: bool,
scope: &'a [String],
use_gitignore: bool,
}

#[derive(Copy, Clone, PartialEq, Eq)]
Expand All @@ -261,6 +263,7 @@ impl<'a> Prune<'a> {
scope: &'a [String],
docker: bool,
output_dir: &str,
use_gitignore: bool,
telemetry: CommandEventBuilder,
) -> Result<Self, Error> {
let allow_missing_package_manager = base.opts().repo_opts.allow_no_package_manager;
Expand Down Expand Up @@ -327,6 +330,7 @@ impl<'a> Prune<'a> {
full_directory,
docker,
scope,
use_gitignore,
})
}

Expand Down Expand Up @@ -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)?;
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)?;
turborepo_fs::recursive_copy(&from_path, out_to, self.use_gitignore)?;
}
if self.docker
&& matches!(
Expand All @@ -385,7 +389,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, self.use_gitignore)?;
}
Ok(())
}
Expand All @@ -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)?;
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);
Expand Down
6 changes: 6 additions & 0 deletions docs/repo-docs/reference/prune.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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[=<bool>]`

Default: `true`

Respect `.gitignore` file(s) when copying files to the output directory.

0 comments on commit 878a22a

Please sign in to comment.