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]]
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",
]

View File

@ -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"

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.
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:

View File

@ -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<PathBuf> {
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),

View File

@ -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<PathBuf> },
MultipleCandidates { candidates: BTreeSet<PathBuf> },
#[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`"
);
}
}

View File

@ -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,

View File

@ -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;

View File

@ -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();
}

View File

@ -33,17 +33,18 @@ macro_rules! test {
}
pub(crate) struct Test {
pub(crate) tempdir: TempDir,
pub(crate) justfile: Option<String>,
pub(crate) args: Vec<String>,
pub(crate) current_dir: PathBuf,
pub(crate) env: BTreeMap<String, String>,
pub(crate) stdin: String,
pub(crate) stdout: String,
pub(crate) justfile: Option<String>,
pub(crate) shell: bool,
pub(crate) status: i32,
pub(crate) stderr: String,
pub(crate) stderr_regex: Option<Regex>,
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<Path>) -> 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())