Support .justfile
as an alternative to justfile
(#931)
This commit is contained in:
parent
0662e4c042
commit
9c3bbc9fa7
4
Cargo.lock
generated
4
Cargo.lock
generated
@ -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",
|
||||||
]
|
]
|
||||||
|
@ -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"
|
||||||
|
|
||||||
|
@ -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:
|
||||||
|
@ -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),
|
||||||
|
@ -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`"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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,
|
||||||
|
@ -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;
|
||||||
|
|
||||||
|
@ -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();
|
||||||
|
}
|
||||||
|
@ -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())
|
||||||
|
Loading…
Reference in New Issue
Block a user