Support .justfile as an alternative to justfile (#931)

This commit is contained in:
Casey Rodarmor 2021-07-31 12:25:49 -07:00 committed by GitHub
parent 0662e4c042
commit 9c3bbc9fa7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 96 additions and 35 deletions

4
Cargo.lock generated
View File

@ -549,9 +549,9 @@ dependencies = [
[[package]] [[package]]
name = "temptree" name = "temptree"
version = "0.1.0" version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "13f60523942b252a93f18dd6c8ba53488929d59f7b106be23a29bc9cbc466461" checksum = "8fda94d8251b40088cb769576f436da19ac1d1ae792c97d0afe1cadc890c8630"
dependencies = [ dependencies = [
"tempfile", "tempfile",
] ]

View File

@ -49,7 +49,7 @@ cradle = "0.0.13"
executable-path = "1.0.0" executable-path = "1.0.0"
pretty_assertions = "0.7.0" pretty_assertions = "0.7.0"
regex = "1.5.4" regex = "1.5.4"
temptree = "0.1.0" temptree = "0.2.0"
which = "4.0.0" which = "4.0.0"
yaml-rust = "0.4.5" yaml-rust = "0.4.5"

View File

@ -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. 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`: Running `just` with no arguments runs the first recipe in the `justfile`:
@ -1542,6 +1542,10 @@ $ just foo/build
$ just foo/ $ just foo/
``` ```
=== Hiding Justfiles
`just` looks for justfiles named `justfile` and `.justfile`, which can be used to keep a `justfile` hidden.
=== Just Scripts === 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: By adding a shebang line to the top of a justfile and making it executable, `just` can be used as an interpreter for scripts:

View File

@ -2,7 +2,8 @@ use crate::common::*;
use std::path::Component; 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"]; const PROJECT_ROOT_CHILDREN: &[&str] = &[".bzr", ".git", ".hg", ".svn", "_darcs"];
pub(crate) struct Search { pub(crate) struct Search {
@ -69,7 +70,7 @@ impl Search {
SearchConfig::FromInvocationDirectory => { SearchConfig::FromInvocationDirectory => {
let working_directory = Self::project_root(&invocation_directory)?; let working_directory = Self::project_root(&invocation_directory)?;
let justfile = working_directory.join(FILENAME); let justfile = working_directory.join(DEFAULT_JUSTFILE_NAME);
Ok(Self { Ok(Self {
justfile, justfile,
@ -82,7 +83,7 @@ impl Search {
let working_directory = Self::project_root(&search_directory)?; let working_directory = Self::project_root(&search_directory)?;
let justfile = working_directory.join(FILENAME); let justfile = working_directory.join(DEFAULT_JUSTFILE_NAME);
Ok(Self { Ok(Self {
justfile, justfile,
@ -113,7 +114,7 @@ impl Search {
fn justfile(directory: &Path) -> SearchResult<PathBuf> { fn justfile(directory: &Path) -> SearchResult<PathBuf> {
for directory in directory.ancestors() { 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 { let entries = fs::read_dir(directory).map_err(|io_error| SearchError::Io {
io_error, io_error,
@ -125,14 +126,16 @@ impl Search {
directory: directory.to_owned(), directory: directory.to_owned(),
})?; })?;
if let Some(name) = entry.file_name().to_str() { if let Some(name) = entry.file_name().to_str() {
if name.eq_ignore_ascii_case(FILENAME) { for justfile_name in JUSTFILE_NAMES {
candidates.push(entry.path()); if name.eq_ignore_ascii_case(justfile_name) {
candidates.insert(entry.path());
}
} }
} }
} }
if candidates.len() == 1 { if candidates.len() == 1 {
return Ok(candidates.pop().unwrap()); return Ok(candidates.into_iter().next().unwrap());
} else if candidates.len() > 1 { } else if candidates.len() > 1 {
return Err(SearchError::MultipleCandidates { candidates }); return Err(SearchError::MultipleCandidates { candidates });
} }
@ -212,10 +215,10 @@ mod tests {
fn multiple_candidates() { fn multiple_candidates() {
let tmp = testing::tempdir(); let tmp = testing::tempdir();
let mut path = tmp.path().to_path_buf(); let mut path = tmp.path().to_path_buf();
path.push(FILENAME); path.push(DEFAULT_JUSTFILE_NAME);
fs::write(&path, "default:\n\techo ok").unwrap(); fs::write(&path, "default:\n\techo ok").unwrap();
path.pop(); path.pop();
path.push(FILENAME.to_uppercase()); path.push(DEFAULT_JUSTFILE_NAME.to_uppercase());
if fs::File::open(path.as_path()).is_ok() { if fs::File::open(path.as_path()).is_ok() {
// We are in case-insensitive file system // We are in case-insensitive file system
return; return;
@ -232,7 +235,7 @@ mod tests {
fn found() { fn found() {
let tmp = testing::tempdir(); let tmp = testing::tempdir();
let mut path = tmp.path().to_path_buf(); let mut path = tmp.path().to_path_buf();
path.push(FILENAME); path.push(DEFAULT_JUSTFILE_NAME);
fs::write(&path, "default:\n\techo ok").unwrap(); fs::write(&path, "default:\n\techo ok").unwrap();
path.pop(); path.pop();
if let Err(err) = Search::justfile(path.as_path()) { if let Err(err) = Search::justfile(path.as_path()) {
@ -244,7 +247,7 @@ mod tests {
fn found_spongebob_case() { fn found_spongebob_case() {
let tmp = testing::tempdir(); let tmp = testing::tempdir();
let mut path = tmp.path().to_path_buf(); let mut path = tmp.path().to_path_buf();
let spongebob_case = FILENAME let spongebob_case = DEFAULT_JUSTFILE_NAME
.chars() .chars()
.enumerate() .enumerate()
.map(|(i, c)| { .map(|(i, c)| {
@ -267,7 +270,7 @@ mod tests {
fn found_from_inner_dir() { fn found_from_inner_dir() {
let tmp = testing::tempdir(); let tmp = testing::tempdir();
let mut path = tmp.path().to_path_buf(); let mut path = tmp.path().to_path_buf();
path.push(FILENAME); path.push(DEFAULT_JUSTFILE_NAME);
fs::write(&path, "default:\n\techo ok").unwrap(); fs::write(&path, "default:\n\techo ok").unwrap();
path.pop(); path.pop();
path.push("a"); path.push("a");
@ -283,12 +286,12 @@ mod tests {
fn found_and_stopped_at_first_justfile() { fn found_and_stopped_at_first_justfile() {
let tmp = testing::tempdir(); let tmp = testing::tempdir();
let mut path = tmp.path().to_path_buf(); let mut path = tmp.path().to_path_buf();
path.push(FILENAME); path.push(DEFAULT_JUSTFILE_NAME);
fs::write(&path, "default:\n\techo ok").unwrap(); fs::write(&path, "default:\n\techo ok").unwrap();
path.pop(); path.pop();
path.push("a"); path.push("a");
fs::create_dir(&path).expect("test justfile search: failed to create intermediary directory"); 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(); fs::write(&path, "default:\n\techo ok").unwrap();
path.pop(); path.pop();
path.push("b"); path.push("b");
@ -296,7 +299,7 @@ mod tests {
match Search::justfile(path.as_path()) { match Search::justfile(path.as_path()) {
Ok(found_path) => { Ok(found_path) => {
path.pop(); path.pop();
path.push(FILENAME); path.push(DEFAULT_JUSTFILE_NAME);
assert_eq!(found_path, path); assert_eq!(found_path, path);
}, },
Err(err) => panic!("No errors were expected: {}", err), Err(err) => panic!("No errors were expected: {}", err),

View File

@ -16,14 +16,14 @@ pub(crate) enum SearchError {
JustfileHadNoParent { path: PathBuf }, JustfileHadNoParent { path: PathBuf },
#[snafu(display( #[snafu(display(
"Multiple candidate justfiles found in `{}`: {}", "Multiple candidate justfiles found in `{}`: {}",
candidates[0].parent().unwrap().display(), candidates.iter().next().unwrap().parent().unwrap().display(),
List::and_ticked( List::and_ticked(
candidates candidates
.iter() .iter()
.map(|candidate| candidate.file_name().unwrap().to_string_lossy()) .map(|candidate| candidate.file_name().unwrap().to_string_lossy())
), ),
))] ))]
MultipleCandidates { candidates: Vec<PathBuf> }, MultipleCandidates { candidates: BTreeSet<PathBuf> },
#[snafu(display("No justfile found"))] #[snafu(display("No justfile found"))]
NotFound, NotFound,
} }
@ -35,15 +35,15 @@ mod tests {
#[test] #[test]
fn multiple_candidates_formatting() { fn multiple_candidates_formatting() {
let error = SearchError::MultipleCandidates { let error = SearchError::MultipleCandidates {
candidates: vec![ candidates: [Path::new("/foo/justfile"), Path::new("/foo/JUSTFILE")]
PathBuf::from("/foo/justfile"), .iter()
PathBuf::from("/foo/JUSTFILE"), .map(|path| path.to_path_buf())
], .collect(),
}; };
assert_eq!( assert_eq!(
error.to_string(), error.to_string(),
"Multiple candidate justfiles found in `/foo`: `justfile` and `JUSTFILE`" "Multiple candidate justfiles found in `/foo`: `JUSTFILE` and `justfile`"
); );
} }
} }

View File

@ -23,7 +23,7 @@ pub(crate) fn config(args: &[&str]) -> Config {
pub(crate) fn search(config: &Config) -> Search { pub(crate) fn search(config: &Config) -> Search {
let working_directory = config.invocation_directory.clone(); let working_directory = config.invocation_directory.clone();
let justfile = working_directory.join(crate::search::FILENAME); let justfile = working_directory.join("justfile");
Search { Search {
justfile, justfile,

View File

@ -18,7 +18,7 @@ pub(crate) use libc::{EXIT_FAILURE, EXIT_SUCCESS};
pub(crate) use pretty_assertions::Comparison; pub(crate) use pretty_assertions::Comparison;
pub(crate) use regex::Regex; pub(crate) use regex::Regex;
pub(crate) use tempfile::TempDir; 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 which::which;
pub(crate) use yaml_rust::YamlLoader; pub(crate) use yaml_rust::YamlLoader;

View File

@ -142,3 +142,44 @@ fn single_upwards() {
search_test(&path, &["../"]); 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();
}

View File

@ -33,17 +33,18 @@ macro_rules! test {
} }
pub(crate) struct Test { pub(crate) struct Test {
pub(crate) tempdir: TempDir,
pub(crate) justfile: Option<String>,
pub(crate) args: Vec<String>, pub(crate) args: Vec<String>,
pub(crate) current_dir: PathBuf,
pub(crate) env: BTreeMap<String, String>, pub(crate) env: BTreeMap<String, String>,
pub(crate) stdin: String, pub(crate) justfile: Option<String>,
pub(crate) stdout: String, pub(crate) shell: bool,
pub(crate) status: i32,
pub(crate) stderr: String, pub(crate) stderr: String,
pub(crate) stderr_regex: Option<Regex>, pub(crate) stderr_regex: Option<Regex>,
pub(crate) status: i32, pub(crate) stdin: String,
pub(crate) shell: bool, pub(crate) stdout: String,
pub(crate) suppress_dotenv_load_warning: bool, pub(crate) suppress_dotenv_load_warning: bool,
pub(crate) tempdir: TempDir,
} }
impl Test { impl Test {
@ -54,6 +55,7 @@ impl Test {
pub(crate) fn with_tempdir(tempdir: TempDir) -> Self { pub(crate) fn with_tempdir(tempdir: TempDir) -> Self {
Self { Self {
args: Vec::new(), args: Vec::new(),
current_dir: PathBuf::new(),
env: BTreeMap::new(), env: BTreeMap::new(),
justfile: Some(String::new()), justfile: Some(String::new()),
shell: true, shell: true,
@ -79,6 +81,11 @@ impl Test {
self self
} }
pub(crate) fn current_dir(mut self, path: impl AsRef<Path>) -> Self {
self.current_dir = path.as_ref().to_owned();
self
}
pub(crate) fn env(mut self, key: &str, val: &str) -> Self { pub(crate) fn env(mut self, key: &str, val: &str) -> Self {
self.env.insert(key.to_string(), val.to_string()); self.env.insert(key.to_string(), val.to_string());
self self
@ -132,6 +139,12 @@ impl Test {
self.suppress_dotenv_load_warning = suppress_dotenv_load_warning; self.suppress_dotenv_load_warning = suppress_dotenv_load_warning;
self 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 { impl Test {
@ -165,7 +178,7 @@ impl Test {
"0" "0"
}, },
) )
.current_dir(self.tempdir.path()) .current_dir(self.tempdir.path().join(self.current_dir))
.stdin(Stdio::piped()) .stdin(Stdio::piped())
.stdout(Stdio::piped()) .stdout(Stdio::piped())
.stderr(Stdio::piped()) .stderr(Stdio::piped())