diff --git a/clippy.toml b/clippy.toml index 756c7dc2..15906305 100644 --- a/clippy.toml +++ b/clippy.toml @@ -1 +1 @@ -too-many-arguments-threshold = 9 +too-many-arguments-threshold = 10 diff --git a/docs/usage.md b/docs/usage.md index b5a3da74..6590d8a9 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -64,6 +64,14 @@ treefmt [FLAGS] [OPTIONS] [--] [paths]... > Only apply selected formatters. Defaults to all formatters. +`-H, --hidden` + +> Also traverse hidden files (files that start with a .). This behaviour can be overridden with the `--no-hidden` flag. + +`--no-hidden` + +> Override the `--hidden` flag. Don't traverse hidden files. + `--tree-root ` > Set the path to the tree root directory where treefmt will look for the files to format. Defaults to the folder holding the `treefmt.toml` file. It’s mostly useful in combination with `--config-file` to specify the project root which won’t coincide with the directory holding `treefmt.toml`. diff --git a/src/command/format.rs b/src/command/format.rs index 409d8098..16b7e97d 100644 --- a/src/command/format.rs +++ b/src/command/format.rs @@ -9,6 +9,7 @@ pub fn format_cmd( work_dir: &Path, config_file: &Path, paths: &[PathBuf], + hidden: bool, no_cache: bool, clear_cache: bool, fail_on_change: bool, @@ -55,6 +56,7 @@ pub fn format_cmd( &cache_dir, config_file, &paths, + hidden, no_cache, clear_cache, fail_on_change, diff --git a/src/command/mod.rs b/src/command/mod.rs index 13f8b162..24eafdee 100644 --- a/src/command/mod.rs +++ b/src/command/mod.rs @@ -23,7 +23,7 @@ use std::{ #[derive(Parser, Debug)] #[command(author, version, about, long_about = None)] pub struct Cli { - /// Create a new treefmt.toml + /// Create a new treefmt.toml. #[arg(short, long, default_value_t = false)] pub init: bool, @@ -39,6 +39,15 @@ pub struct Cli { #[arg(short, long, default_value_t = false)] pub clear_cache: bool, + #[arg(long = "hidden", short = 'H')] + /// Include hidden files while traversing the tree. + /// Override with the --no-hidden flag. + pub hidden: bool, + /// Overrides the --hidden flag. + /// Don't include hidden files while traversing the tree. + #[arg(long, overrides_with = "hidden", hide = true)] + no_hidden: bool, + /// Exit with error if any changes were made. Useful for CI. #[arg( long, @@ -52,7 +61,7 @@ pub struct Cli { #[arg(long, default_value_t = false)] pub allow_missing_formatter: bool, - /// Log verbosity is based off the number of v used + /// Log verbosity is based off the number of v used. #[clap(flatten)] pub verbose: Verbosity, @@ -158,6 +167,7 @@ pub fn run_cli(cli: &Cli) -> anyhow::Result<()> { &cli.work_dir, config_file, &cli.paths, + cli.hidden, cli.no_cache, cli.clear_cache, cli.fail_on_change, diff --git a/src/engine.rs b/src/engine.rs index 2d973fe9..311f7ca0 100644 --- a/src/engine.rs +++ b/src/engine.rs @@ -29,6 +29,7 @@ pub fn run_treefmt( cache_dir: &Path, treefmt_toml: &Path, paths: &[PathBuf], + hidden: bool, no_cache: bool, clear_cache: bool, fail_on_change: bool, @@ -89,7 +90,7 @@ pub fn run_treefmt( cache.update_formatters(formatters.clone()); } - let walker = build_walker(paths); + let walker = build_walker(paths, hidden); let matches = collect_matches_from_walker(walker, &formatters, &mut stats); stats.timed_debug("tree walk"); @@ -322,7 +323,7 @@ fn collect_matches_from_walker( } /// Configure and build the tree walker -fn build_walker(paths: Vec) -> Walk { +fn build_walker(paths: Vec, hidden: bool) -> Walk { // For some reason the WalkBuilder must start with one path, but can add more paths later. // unwrap: we checked before that there is at least one path in the vector let mut builder = WalkBuilder::new(paths.first().unwrap()); @@ -330,6 +331,7 @@ fn build_walker(paths: Vec) -> Walk { for path in paths[1..].iter() { builder.add(path); } + builder.hidden(!hidden); // TODO: builder has a lot of interesting options. // TODO: use build_parallel with a Visitor. // See https://docs.rs/ignore/0.4.17/ignore/struct.WalkParallel.html#method.visit @@ -981,7 +983,7 @@ mod tests { let formatters = load_formatters(root, tree_root, false, &None, &mut stats).unwrap(); - let walker = build_walker(vec![tree_root.to_path_buf()]); + let walker = build_walker(vec![tree_root.to_path_buf()], false); let _matches = collect_matches_from_walker(walker, &formatters, &mut stats); assert_eq!(stats.traversed_files, 3); @@ -1030,13 +1032,62 @@ mod tests { let formatters = load_formatters(root, tree_root, false, &None, &mut stats).unwrap(); - let walker = build_walker(vec![tree_root.to_path_buf()]); + let walker = build_walker(vec![tree_root.to_path_buf()], false); let _matches = collect_matches_from_walker(walker, &formatters, &mut stats); assert_eq!(stats.traversed_files, 15); assert_eq!(stats.matched_files, 9); } #[test] + fn test_walker_some_matches_walk_hidden() { + let tmpdir = utils::tmp_mkdir(); + + let black = tmpdir.path().join("black"); + let nixpkgs_fmt = tmpdir.path().join("nixpkgs-fmt"); + let elm_fmt = tmpdir.path().join("elm-fmt"); + utils::write_binary_file(&black, " "); + utils::write_binary_file(&nixpkgs_fmt, " "); + utils::write_binary_file(&elm_fmt, " "); + let tree_root = tmpdir.path(); + + let files = vec!["test", "test1", "test3", ".test4"]; + + for file in files { + utils::write_file(tree_root.join(format!("{file}.py")), " "); + utils::write_file(tree_root.join(format!("{file}.nix")), " "); + utils::write_file(tree_root.join(format!("{file}.elm")), " "); + utils::write_file(tree_root.join(file), " "); + } + + let config = format!( + " + [formatter.python] + command = {black:?} + includes = [\"*.py\"] + + [formatter.nix] + command = {nixpkgs_fmt:?} + includes = [\"*.nix\"] + + [formatter.elm] + command = {elm_fmt:?} + options = [\"--yes\"] + includes = [\"*.elm\"] + " + ); + + let root = from_string(&config).unwrap(); + let mut stats = Statistics::init(); + + let formatters = load_formatters(root, tree_root, false, &None, &mut stats).unwrap(); + + let walker = build_walker(vec![tree_root.to_path_buf()], true); + let _matches = collect_matches_from_walker(walker, &formatters, &mut stats); + + assert_eq!(stats.traversed_files, 19); + assert_eq!(stats.matched_files, 12); + } + #[test] fn test_walker_some_matches_specific_include() { let tmpdir = utils::tmp_mkdir(); @@ -1080,7 +1131,7 @@ mod tests { let formatters = load_formatters(root, tree_root, false, &None, &mut stats).unwrap(); - let walker = build_walker(vec![tree_root.to_path_buf()]); + let walker = build_walker(vec![tree_root.to_path_buf()], false); let _matches = collect_matches_from_walker(walker, &formatters, &mut stats); assert_eq!(stats.traversed_files, 15); @@ -1130,7 +1181,7 @@ mod tests { let formatters = load_formatters(root, tree_root, false, &None, &mut stats).unwrap(); - let walker = build_walker(vec![tree_root.to_path_buf()]); + let walker = build_walker(vec![tree_root.to_path_buf()], false); let matches = collect_matches_from_walker(walker, &formatters, &mut stats); assert_eq!(stats.traversed_files, 12); @@ -1213,7 +1264,7 @@ mod tests { let formatters = load_formatters(root, tree_root, false, &None, &mut stats).unwrap(); - let walker = build_walker(vec![tree_root.to_path_buf()]); + let walker = build_walker(vec![tree_root.to_path_buf()], false); let matches = collect_matches_from_walker(walker, &formatters, &mut stats); assert_eq!(stats.traversed_files, 18); @@ -1285,7 +1336,7 @@ mod tests { let formatters = load_formatters(root, tree_root, false, &None, &mut stats).unwrap(); - let walker = build_walker(vec![tree_root.to_path_buf()]); + let walker = build_walker(vec![tree_root.to_path_buf()], false); let matches = collect_matches_from_walker(walker, &formatters, &mut stats); assert_eq!(stats.traversed_files, 7); @@ -1347,7 +1398,7 @@ mod tests { let formatters = load_formatters(root, tree_root, false, &None, &mut stats).unwrap(); - let walker = build_walker(vec![tree_root.to_path_buf()]); + let walker = build_walker(vec![tree_root.to_path_buf()], false); let matches = collect_matches_from_walker(walker, &formatters, &mut stats); assert_eq!(stats.traversed_files, 7); @@ -1417,7 +1468,7 @@ mod tests { let formatters = load_formatters(root, tree_root, false, &None, &mut stats).unwrap(); - let walker = build_walker(vec![tree_root.to_path_buf()]); + let walker = build_walker(vec![tree_root.to_path_buf()], false); let matches = collect_matches_from_walker(walker, &formatters, &mut stats); assert_eq!(stats.traversed_files, 8); @@ -1486,7 +1537,7 @@ mod tests { let formatters = load_formatters(root, tree_root, false, &None, &mut stats).unwrap(); - let walker = build_walker(vec![tree_root.to_path_buf()]); + let walker = build_walker(vec![tree_root.to_path_buf()], false); let matches = collect_matches_from_walker(walker, &formatters, &mut stats); assert_eq!(stats.traversed_files, 7); @@ -1505,4 +1556,78 @@ mod tests { assert_eq!(python_matches, expected_python_matches); assert!(nix_matches); } + #[test] + fn test_walker_some_matches_exclude_gitignore_hidden() { + let tmpdir = utils::tmp_mkdir(); + + let black = tmpdir.path().join("black"); + let nixpkgs_fmt = tmpdir.path().join("nixpkgs-fmt"); + utils::write_binary_file(&black, " "); + utils::write_binary_file(&nixpkgs_fmt, " "); + let tree_root = tmpdir.path(); + let git_dir = tree_root.join(".git"); + + utils::Git::new(tmpdir.path().to_path_buf()) + .git_ignore("test1.nix\n.git/*.nix") + .exclude("result\n.direnv") + .create(); + + let files = vec!["test", "test1", ".test4"]; + + for file in files { + utils::write_file(tree_root.join(format!("{file}.py")), " "); + utils::write_file(tree_root.join(format!("{file}.nix")), " "); + utils::write_file(tree_root.join(format!("{file}.py")), " "); + utils::write_file(tree_root.join(format!("{file}.nix")), " "); + utils::write_file(git_dir.join(file), " "); + utils::write_file(tree_root.join(file), " "); + } + utils::write_file(tree_root.join("result"), " "); + utils::write_file(tree_root.join(".direnv"), " "); + + let config = format!( + " + [formatter.python] + command = {black:?} + includes = [\"*.py\", \"test\"] + excludes = [\"test.py\" ] + + [formatter.nix] + command = {nixpkgs_fmt:?} + includes = [\"*.nix\"] + excludes = [\"test.nix\"] + " + ); + + let root = from_string(&config).unwrap(); + let mut stats = Statistics::init(); + + let formatters = load_formatters(root, tree_root, false, &None, &mut stats).unwrap(); + + let walker = build_walker(vec![tree_root.to_path_buf()], true); + let matches = collect_matches_from_walker(walker, &formatters, &mut stats); + + assert_eq!(stats.traversed_files, 15); + assert_eq!(stats.matched_files, 5); + let python_matches: Vec = matches + .get(&FormatterName::new("python")) + .unwrap() + .keys() + .cloned() + .collect(); + let nix_matches: Vec = matches + .get(&FormatterName::new("nix")) + .unwrap() + .keys() + .cloned() + .collect(); + let expected_nix_matches: Vec = + [".test4.nix"].iter().map(|p| tree_root.join(p)).collect(); + let expected_python_matches: Vec = [".git/test", ".test4.py", "test", "test1.py"] + .iter() + .map(|p| tree_root.join(p)) + .collect(); + assert_eq!(python_matches, expected_python_matches); + assert_eq!(nix_matches, expected_nix_matches); + } }