diff --git a/Cargo.lock b/Cargo.lock index 07888d8..f455ae2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -549,9 +549,9 @@ dependencies = [ [[package]] name = "temptree" -version = "0.1.0" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13f60523942b252a93f18dd6c8ba53488929d59f7b106be23a29bc9cbc466461" +checksum = "8fda94d8251b40088cb769576f436da19ac1d1ae792c97d0afe1cadc890c8630" dependencies = [ "tempfile", ] diff --git a/Cargo.toml b/Cargo.toml index 8cb1b36..7a957e9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -49,7 +49,7 @@ cradle = "0.0.13" executable-path = "1.0.0" pretty_assertions = "0.7.0" regex = "1.5.4" -temptree = "0.1.0" +temptree = "0.2.0" which = "4.0.0" yaml-rust = "0.4.5" diff --git a/README.adoc b/README.adoc index fbfc974..00b72cf 100644 --- a/README.adoc +++ b/README.adoc @@ -231,7 +231,7 @@ another-recipe: When you invoke `just` it looks for file `justfile` in the current directory and upwards, so you can invoke it from any subdirectory of your project. -The search for a `justfile` is case insensitive, so any case, like `Justfile`, `JUSTFILE`, or `JuStFiLe`, will work. +The search for a `justfile` is case insensitive, so any case, like `Justfile`, `JUSTFILE`, or `JuStFiLe`, will work. `just` will also look for files with the name `.justfile`, in case you'd like to hide a `justfile`. Running `just` with no arguments runs the first recipe in the `justfile`: @@ -1542,6 +1542,10 @@ $ just foo/build $ just foo/ ``` +=== Hiding Justfiles + +`just` looks for justfiles named `justfile` and `.justfile`, which can be used to keep a `justfile` hidden. + === Just Scripts By adding a shebang line to the top of a justfile and making it executable, `just` can be used as an interpreter for scripts: diff --git a/src/search.rs b/src/search.rs index 1b95a52..c1ae20d 100644 --- a/src/search.rs +++ b/src/search.rs @@ -2,7 +2,8 @@ use crate::common::*; use std::path::Component; -pub(crate) const FILENAME: &str = "justfile"; +const DEFAULT_JUSTFILE_NAME: &str = JUSTFILE_NAMES[0]; +const JUSTFILE_NAMES: &[&str] = &["justfile", ".justfile"]; const PROJECT_ROOT_CHILDREN: &[&str] = &[".bzr", ".git", ".hg", ".svn", "_darcs"]; pub(crate) struct Search { @@ -69,7 +70,7 @@ impl Search { SearchConfig::FromInvocationDirectory => { let working_directory = Self::project_root(&invocation_directory)?; - let justfile = working_directory.join(FILENAME); + let justfile = working_directory.join(DEFAULT_JUSTFILE_NAME); Ok(Self { justfile, @@ -82,7 +83,7 @@ impl Search { let working_directory = Self::project_root(&search_directory)?; - let justfile = working_directory.join(FILENAME); + let justfile = working_directory.join(DEFAULT_JUSTFILE_NAME); Ok(Self { justfile, @@ -113,7 +114,7 @@ impl Search { fn justfile(directory: &Path) -> SearchResult { for directory in directory.ancestors() { - let mut candidates = Vec::new(); + let mut candidates = BTreeSet::new(); let entries = fs::read_dir(directory).map_err(|io_error| SearchError::Io { io_error, @@ -125,14 +126,16 @@ impl Search { directory: directory.to_owned(), })?; if let Some(name) = entry.file_name().to_str() { - if name.eq_ignore_ascii_case(FILENAME) { - candidates.push(entry.path()); + for justfile_name in JUSTFILE_NAMES { + if name.eq_ignore_ascii_case(justfile_name) { + candidates.insert(entry.path()); + } } } } if candidates.len() == 1 { - return Ok(candidates.pop().unwrap()); + return Ok(candidates.into_iter().next().unwrap()); } else if candidates.len() > 1 { return Err(SearchError::MultipleCandidates { candidates }); } @@ -212,10 +215,10 @@ mod tests { fn multiple_candidates() { let tmp = testing::tempdir(); let mut path = tmp.path().to_path_buf(); - path.push(FILENAME); + path.push(DEFAULT_JUSTFILE_NAME); fs::write(&path, "default:\n\techo ok").unwrap(); path.pop(); - path.push(FILENAME.to_uppercase()); + path.push(DEFAULT_JUSTFILE_NAME.to_uppercase()); if fs::File::open(path.as_path()).is_ok() { // We are in case-insensitive file system return; @@ -232,7 +235,7 @@ mod tests { fn found() { let tmp = testing::tempdir(); let mut path = tmp.path().to_path_buf(); - path.push(FILENAME); + path.push(DEFAULT_JUSTFILE_NAME); fs::write(&path, "default:\n\techo ok").unwrap(); path.pop(); if let Err(err) = Search::justfile(path.as_path()) { @@ -244,7 +247,7 @@ mod tests { fn found_spongebob_case() { let tmp = testing::tempdir(); let mut path = tmp.path().to_path_buf(); - let spongebob_case = FILENAME + let spongebob_case = DEFAULT_JUSTFILE_NAME .chars() .enumerate() .map(|(i, c)| { @@ -267,7 +270,7 @@ mod tests { fn found_from_inner_dir() { let tmp = testing::tempdir(); let mut path = tmp.path().to_path_buf(); - path.push(FILENAME); + path.push(DEFAULT_JUSTFILE_NAME); fs::write(&path, "default:\n\techo ok").unwrap(); path.pop(); path.push("a"); @@ -283,12 +286,12 @@ mod tests { fn found_and_stopped_at_first_justfile() { let tmp = testing::tempdir(); let mut path = tmp.path().to_path_buf(); - path.push(FILENAME); + path.push(DEFAULT_JUSTFILE_NAME); fs::write(&path, "default:\n\techo ok").unwrap(); path.pop(); path.push("a"); fs::create_dir(&path).expect("test justfile search: failed to create intermediary directory"); - path.push(FILENAME); + path.push(DEFAULT_JUSTFILE_NAME); fs::write(&path, "default:\n\techo ok").unwrap(); path.pop(); path.push("b"); @@ -296,7 +299,7 @@ mod tests { match Search::justfile(path.as_path()) { Ok(found_path) => { path.pop(); - path.push(FILENAME); + path.push(DEFAULT_JUSTFILE_NAME); assert_eq!(found_path, path); }, Err(err) => panic!("No errors were expected: {}", err), diff --git a/src/search_error.rs b/src/search_error.rs index 6a8e708..cea46cf 100644 --- a/src/search_error.rs +++ b/src/search_error.rs @@ -16,14 +16,14 @@ pub(crate) enum SearchError { JustfileHadNoParent { path: PathBuf }, #[snafu(display( "Multiple candidate justfiles found in `{}`: {}", - candidates[0].parent().unwrap().display(), + candidates.iter().next().unwrap().parent().unwrap().display(), List::and_ticked( candidates .iter() .map(|candidate| candidate.file_name().unwrap().to_string_lossy()) ), ))] - MultipleCandidates { candidates: Vec }, + MultipleCandidates { candidates: BTreeSet }, #[snafu(display("No justfile found"))] NotFound, } @@ -35,15 +35,15 @@ mod tests { #[test] fn multiple_candidates_formatting() { let error = SearchError::MultipleCandidates { - candidates: vec![ - PathBuf::from("/foo/justfile"), - PathBuf::from("/foo/JUSTFILE"), - ], + candidates: [Path::new("/foo/justfile"), Path::new("/foo/JUSTFILE")] + .iter() + .map(|path| path.to_path_buf()) + .collect(), }; assert_eq!( error.to_string(), - "Multiple candidate justfiles found in `/foo`: `justfile` and `JUSTFILE`" + "Multiple candidate justfiles found in `/foo`: `JUSTFILE` and `justfile`" ); } } diff --git a/src/testing.rs b/src/testing.rs index a69dcc4..d9e4ea4 100644 --- a/src/testing.rs +++ b/src/testing.rs @@ -23,7 +23,7 @@ pub(crate) fn config(args: &[&str]) -> Config { pub(crate) fn search(config: &Config) -> Search { let working_directory = config.invocation_directory.clone(); - let justfile = working_directory.join(crate::search::FILENAME); + let justfile = working_directory.join("justfile"); Search { justfile, diff --git a/tests/common.rs b/tests/common.rs index f874084..881afad 100644 --- a/tests/common.rs +++ b/tests/common.rs @@ -18,7 +18,7 @@ pub(crate) use libc::{EXIT_FAILURE, EXIT_SUCCESS}; pub(crate) use pretty_assertions::Comparison; pub(crate) use regex::Regex; pub(crate) use tempfile::TempDir; -pub(crate) use temptree::temptree; +pub(crate) use temptree::{temptree, tree, Tree}; pub(crate) use which::which; pub(crate) use yaml_rust::YamlLoader; diff --git a/tests/search.rs b/tests/search.rs index eb4a5f6..425ba8d 100644 --- a/tests/search.rs +++ b/tests/search.rs @@ -142,3 +142,44 @@ fn single_upwards() { search_test(&path, &["../"]); } + +#[test] +fn find_dot_justfile() { + Test::new() + .justfile( + " + foo: + echo bad + ", + ) + .tree(tree! { + dir: { + ".justfile": " + foo: + echo ok + " + } + }) + .current_dir("dir") + .stderr("echo ok\n") + .stdout("ok\n") + .run(); +} + +#[test] +fn dot_justfile_conflicts_with_justfile() { + Test::new() + .justfile( + " + foo: + ", + ) + .tree(tree! { + ".justfile": " + foo: + ", + }) + .stderr_regex("error: Multiple candidate justfiles found in `.*`: `.justfile` and `justfile`\n") + .status(EXIT_FAILURE) + .run(); +} diff --git a/tests/test.rs b/tests/test.rs index df0dd61..b998dd5 100644 --- a/tests/test.rs +++ b/tests/test.rs @@ -33,17 +33,18 @@ macro_rules! test { } pub(crate) struct Test { - pub(crate) tempdir: TempDir, - pub(crate) justfile: Option, pub(crate) args: Vec, + pub(crate) current_dir: PathBuf, pub(crate) env: BTreeMap, - pub(crate) stdin: String, - pub(crate) stdout: String, + pub(crate) justfile: Option, + pub(crate) shell: bool, + pub(crate) status: i32, pub(crate) stderr: String, pub(crate) stderr_regex: Option, - pub(crate) status: i32, - pub(crate) shell: bool, + pub(crate) stdin: String, + pub(crate) stdout: String, pub(crate) suppress_dotenv_load_warning: bool, + pub(crate) tempdir: TempDir, } impl Test { @@ -54,6 +55,7 @@ impl Test { pub(crate) fn with_tempdir(tempdir: TempDir) -> Self { Self { args: Vec::new(), + current_dir: PathBuf::new(), env: BTreeMap::new(), justfile: Some(String::new()), shell: true, @@ -79,6 +81,11 @@ impl Test { self } + pub(crate) fn current_dir(mut self, path: impl AsRef) -> Self { + self.current_dir = path.as_ref().to_owned(); + self + } + pub(crate) fn env(mut self, key: &str, val: &str) -> Self { self.env.insert(key.to_string(), val.to_string()); self @@ -132,6 +139,12 @@ impl Test { self.suppress_dotenv_load_warning = suppress_dotenv_load_warning; self } + + pub(crate) fn tree(self, mut tree: Tree) -> Self { + tree.map(|_name, content| unindent(content)); + tree.instantiate(&self.tempdir.path()).unwrap(); + self + } } impl Test { @@ -165,7 +178,7 @@ impl Test { "0" }, ) - .current_dir(self.tempdir.path()) + .current_dir(self.tempdir.path().join(self.current_dir)) .stdin(Stdio::piped()) .stdout(Stdio::piped()) .stderr(Stdio::piped())