Search for missing recipes in parent directory justfiles (#1149)
This commit is contained in:
parent
1c9297452b
commit
52f73db33d
29
README.md
29
README.md
@ -1831,6 +1831,35 @@ default:
|
||||
|
||||
The `--dump` command can be used with `--dump-format json` to print a JSON representation of a `justfile`. The JSON format is currently unstable, so the `--unstable` flag is required.
|
||||
|
||||
### Falling back to parent `justfile`s
|
||||
|
||||
If a recipe is not found, `just` will look for `justfile`s in the parent
|
||||
directory and up, until it reaches the root directory.
|
||||
|
||||
This feature is currently unstable, and so must be enabled with the
|
||||
`--unstable` flag.
|
||||
|
||||
As an example, suppose the current directory contains this `justfile`:
|
||||
|
||||
```make
|
||||
foo:
|
||||
echo foo
|
||||
```
|
||||
|
||||
And the parent directory contains this `justfile`:
|
||||
|
||||
```make
|
||||
bar:
|
||||
echo bar
|
||||
```
|
||||
|
||||
```sh
|
||||
$ just --unstable bar
|
||||
Trying ../justfile
|
||||
echo bar
|
||||
bar
|
||||
```
|
||||
|
||||
Changelog
|
||||
---------
|
||||
|
||||
|
@ -10,7 +10,7 @@ pub(crate) use std::{
|
||||
iter::{self, FromIterator},
|
||||
mem,
|
||||
ops::{Index, Range, RangeInclusive},
|
||||
path::{Path, PathBuf},
|
||||
path::{self, Path, PathBuf},
|
||||
process::{self, Command, ExitStatus, Stdio},
|
||||
rc::Rc,
|
||||
str::{self, Chars},
|
||||
|
@ -606,7 +606,11 @@ impl Config {
|
||||
}
|
||||
|
||||
pub(crate) fn run<'src>(self, loader: &'src Loader) -> Result<(), Error<'src>> {
|
||||
self.subcommand.run(&self, loader)
|
||||
if let Err(error) = InterruptHandler::install(self.verbosity) {
|
||||
warn!("Failed to set CTRL-C handler: {}", error);
|
||||
}
|
||||
|
||||
self.subcommand.execute(&self, loader)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -74,10 +74,6 @@ impl<'src> Justfile<'src> {
|
||||
overrides: &BTreeMap<String, String>,
|
||||
arguments: &[String],
|
||||
) -> RunResult<'src, ()> {
|
||||
if let Err(error) = InterruptHandler::install(config.verbosity) {
|
||||
warn!("Failed to set CTRL-C handler: {}", error);
|
||||
}
|
||||
|
||||
let unknown_overrides = overrides
|
||||
.keys()
|
||||
.filter(|name| !self.assignments.contains_key(name.as_str()))
|
||||
@ -344,8 +340,8 @@ impl<'src> Justfile<'src> {
|
||||
}
|
||||
|
||||
let mut invocation = vec![recipe.name().to_owned()];
|
||||
for argument in arguments.iter().copied() {
|
||||
invocation.push(argument.to_owned());
|
||||
for argument in arguments {
|
||||
invocation.push((*argument).to_string());
|
||||
}
|
||||
|
||||
ran.insert(invocation);
|
||||
|
@ -17,17 +17,7 @@ impl Search {
|
||||
invocation_directory: &Path,
|
||||
) -> SearchResult<Self> {
|
||||
match search_config {
|
||||
SearchConfig::FromInvocationDirectory => {
|
||||
let justfile = Self::justfile(invocation_directory)?;
|
||||
|
||||
let working_directory = Self::working_directory_from_justfile(&justfile)?;
|
||||
|
||||
Ok(Self {
|
||||
justfile,
|
||||
working_directory,
|
||||
})
|
||||
}
|
||||
|
||||
SearchConfig::FromInvocationDirectory => Self::find_next(invocation_directory),
|
||||
SearchConfig::FromSearchDirectory { search_directory } => {
|
||||
let search_directory = Self::clean(invocation_directory, search_directory);
|
||||
|
||||
@ -40,7 +30,6 @@ impl Search {
|
||||
working_directory,
|
||||
})
|
||||
}
|
||||
|
||||
SearchConfig::WithJustfile { justfile } => {
|
||||
let justfile = Self::clean(invocation_directory, justfile);
|
||||
|
||||
@ -51,7 +40,6 @@ impl Search {
|
||||
working_directory,
|
||||
})
|
||||
}
|
||||
|
||||
SearchConfig::WithJustfileAndWorkingDirectory {
|
||||
justfile,
|
||||
working_directory,
|
||||
@ -62,6 +50,17 @@ impl Search {
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn find_next(starting_dir: &Path) -> SearchResult<Self> {
|
||||
let justfile = Self::justfile(starting_dir)?;
|
||||
|
||||
let working_directory = Self::working_directory_from_justfile(&justfile)?;
|
||||
|
||||
Ok(Self {
|
||||
justfile,
|
||||
working_directory,
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn init(
|
||||
search_config: &SearchConfig,
|
||||
invocation_directory: &Path,
|
||||
|
@ -27,8 +27,8 @@ pub(crate) enum Subcommand {
|
||||
Init,
|
||||
List,
|
||||
Run {
|
||||
overrides: BTreeMap<String, String>,
|
||||
arguments: Vec<String>,
|
||||
overrides: BTreeMap<String, String>,
|
||||
},
|
||||
Show {
|
||||
name: String,
|
||||
@ -38,7 +38,11 @@ pub(crate) enum Subcommand {
|
||||
}
|
||||
|
||||
impl Subcommand {
|
||||
pub(crate) fn run<'src>(&self, config: &Config, loader: &'src Loader) -> Result<(), Error<'src>> {
|
||||
pub(crate) fn execute<'src>(
|
||||
&self,
|
||||
config: &Config,
|
||||
loader: &'src Loader,
|
||||
) -> Result<(), Error<'src>> {
|
||||
use Subcommand::*;
|
||||
|
||||
match self {
|
||||
@ -48,6 +52,10 @@ impl Subcommand {
|
||||
}
|
||||
Completions { shell } => return Self::completions(shell),
|
||||
Init => return Self::init(config),
|
||||
Run {
|
||||
arguments,
|
||||
overrides,
|
||||
} => return Self::run(config, loader, arguments, overrides),
|
||||
_ => {}
|
||||
}
|
||||
|
||||
@ -57,6 +65,104 @@ impl Subcommand {
|
||||
return Self::edit(&search);
|
||||
}
|
||||
|
||||
let (src, ast, justfile) = Self::compile(config, loader, &search)?;
|
||||
|
||||
match self {
|
||||
Choose { overrides, chooser } => {
|
||||
Self::choose(config, justfile, &search, overrides, chooser.as_deref())?;
|
||||
}
|
||||
Command { overrides, .. } | Evaluate { overrides, .. } => {
|
||||
justfile.run(config, &search, overrides, &[])?;
|
||||
}
|
||||
Dump => Self::dump(config, ast, justfile)?,
|
||||
Format => Self::format(config, &search, src, ast)?,
|
||||
List => Self::list(config, justfile),
|
||||
Show { ref name } => Self::show(config, name, justfile)?,
|
||||
Summary => Self::summary(config, justfile),
|
||||
Variables => Self::variables(justfile),
|
||||
Changelog | Completions { .. } | Edit | Init | Run { .. } => unreachable!(),
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn run<'src>(
|
||||
config: &Config,
|
||||
loader: &'src Loader,
|
||||
arguments: &[String],
|
||||
overrides: &BTreeMap<String, String>,
|
||||
) -> Result<(), Error<'src>> {
|
||||
if config.unstable && config.search_config == SearchConfig::FromInvocationDirectory {
|
||||
let mut path = config.invocation_directory.clone();
|
||||
|
||||
let mut unknown_recipes_errors = None;
|
||||
|
||||
loop {
|
||||
let search = match Search::find_next(&path) {
|
||||
Err(SearchError::NotFound) => match unknown_recipes_errors {
|
||||
Some(err) => return Err(err),
|
||||
None => return Err(SearchError::NotFound.into()),
|
||||
},
|
||||
Err(err) => return Err(err.into()),
|
||||
Ok(search) => {
|
||||
if config.verbosity.loud() && path != config.invocation_directory {
|
||||
eprintln!(
|
||||
"Trying {}",
|
||||
config
|
||||
.invocation_directory
|
||||
.strip_prefix(path)
|
||||
.unwrap()
|
||||
.components()
|
||||
.map(|_| path::Component::ParentDir)
|
||||
.collect::<PathBuf>()
|
||||
.join(search.justfile.file_name().unwrap())
|
||||
.display()
|
||||
);
|
||||
}
|
||||
search
|
||||
}
|
||||
};
|
||||
|
||||
match Self::run_inner(config, loader, arguments, overrides, &search) {
|
||||
Err(err @ Error::UnknownRecipes { .. }) => {
|
||||
match search.justfile.parent().unwrap().parent() {
|
||||
Some(parent) => {
|
||||
unknown_recipes_errors.get_or_insert(err);
|
||||
path = parent.into();
|
||||
}
|
||||
None => return Err(err),
|
||||
}
|
||||
}
|
||||
result => return result,
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Self::run_inner(
|
||||
config,
|
||||
loader,
|
||||
arguments,
|
||||
overrides,
|
||||
&Search::find(&config.search_config, &config.invocation_directory)?,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fn run_inner<'src>(
|
||||
config: &Config,
|
||||
loader: &'src Loader,
|
||||
arguments: &[String],
|
||||
overrides: &BTreeMap<String, String>,
|
||||
search: &Search,
|
||||
) -> Result<(), Error<'src>> {
|
||||
let (_src, _ast, justfile) = Self::compile(config, loader, search)?;
|
||||
justfile.run(config, search, overrides, arguments)
|
||||
}
|
||||
|
||||
fn compile<'src>(
|
||||
config: &Config,
|
||||
loader: &'src Loader,
|
||||
search: &Search,
|
||||
) -> Result<(&'src str, Ast<'src>, Justfile<'src>), Error<'src>> {
|
||||
let src = loader.load(&search.justfile)?;
|
||||
|
||||
let tokens = Lexer::lex(src)?;
|
||||
@ -69,27 +175,7 @@ impl Subcommand {
|
||||
}
|
||||
}
|
||||
|
||||
match self {
|
||||
Choose { overrides, chooser } => {
|
||||
Self::choose(config, justfile, &search, overrides, chooser.as_deref())?;
|
||||
}
|
||||
Command { overrides, .. } | Evaluate { overrides, .. } => {
|
||||
justfile.run(config, &search, overrides, &[])?;
|
||||
}
|
||||
Dump => Self::dump(config, ast, justfile)?,
|
||||
Format => Self::format(config, &search, src, ast)?,
|
||||
List => Self::list(config, justfile),
|
||||
Run {
|
||||
arguments,
|
||||
overrides,
|
||||
} => justfile.run(config, &search, overrides, arguments)?,
|
||||
Show { ref name } => Self::show(config, name, justfile)?,
|
||||
Summary => Self::summary(config, justfile),
|
||||
Variables => Self::variables(justfile),
|
||||
Changelog | Completions { .. } | Edit | Init => unreachable!(),
|
||||
}
|
||||
|
||||
Ok(())
|
||||
Ok((src, ast, justfile))
|
||||
}
|
||||
|
||||
fn changelog() {
|
||||
|
@ -6,7 +6,7 @@ pub(crate) use std::{
|
||||
fs,
|
||||
io::Write,
|
||||
iter,
|
||||
path::{Path, PathBuf},
|
||||
path::{Path, PathBuf, MAIN_SEPARATOR},
|
||||
process::{Command, Output, Stdio},
|
||||
str,
|
||||
};
|
||||
|
@ -71,7 +71,6 @@ fn no_warning() {
|
||||
)
|
||||
.stdout("unset\n")
|
||||
.stderr("echo ${DOTENV_KEY:-unset}\n")
|
||||
.suppress_dotenv_load_warning(false)
|
||||
.run();
|
||||
}
|
||||
|
||||
|
192
tests/fall_back_to_parent.rs
Normal file
192
tests/fall_back_to_parent.rs
Normal file
@ -0,0 +1,192 @@
|
||||
use crate::common::*;
|
||||
|
||||
#[test]
|
||||
fn runs_recipe_in_parent_if_not_found_in_current() {
|
||||
Test::new()
|
||||
.tree(tree! {
|
||||
bar: {
|
||||
justfile: "
|
||||
baz:
|
||||
echo subdir
|
||||
"
|
||||
}
|
||||
})
|
||||
.justfile(
|
||||
"
|
||||
foo:
|
||||
echo root
|
||||
",
|
||||
)
|
||||
.args(&["--unstable", "foo"])
|
||||
.current_dir("bar")
|
||||
.stderr(format!(
|
||||
"
|
||||
Trying ..{}justfile
|
||||
echo root
|
||||
",
|
||||
MAIN_SEPARATOR
|
||||
))
|
||||
.stdout("root\n")
|
||||
.run();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn print_error_from_parent_if_recipe_not_found_in_current() {
|
||||
Test::new()
|
||||
.tree(tree! {
|
||||
bar: {
|
||||
justfile: "
|
||||
baz:
|
||||
echo subdir
|
||||
"
|
||||
}
|
||||
})
|
||||
.justfile("foo:\n echo {{bar}}")
|
||||
.args(&["--unstable", "foo"])
|
||||
.current_dir("bar")
|
||||
.stderr(format!(
|
||||
"
|
||||
Trying ..{}justfile
|
||||
error: Variable `bar` not defined
|
||||
|
|
||||
2 | echo {{{{bar}}}}
|
||||
| ^^^
|
||||
",
|
||||
MAIN_SEPARATOR
|
||||
))
|
||||
.status(EXIT_FAILURE)
|
||||
.run();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn requires_unstable() {
|
||||
Test::new()
|
||||
.tree(tree! {
|
||||
bar: {
|
||||
justfile: "
|
||||
baz:
|
||||
echo subdir
|
||||
"
|
||||
}
|
||||
})
|
||||
.justfile(
|
||||
"
|
||||
foo:
|
||||
echo root
|
||||
",
|
||||
)
|
||||
.args(&["foo"])
|
||||
.current_dir("bar")
|
||||
.status(EXIT_FAILURE)
|
||||
.stderr("error: Justfile does not contain recipe `foo`.\n")
|
||||
.run();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn doesnt_work_with_search_directory() {
|
||||
Test::new()
|
||||
.tree(tree! {
|
||||
bar: {
|
||||
justfile: "
|
||||
baz:
|
||||
echo subdir
|
||||
"
|
||||
}
|
||||
})
|
||||
.justfile(
|
||||
"
|
||||
foo:
|
||||
echo root
|
||||
",
|
||||
)
|
||||
.args(&["--unstable", "./foo"])
|
||||
.current_dir("bar")
|
||||
.status(EXIT_FAILURE)
|
||||
.stderr("error: Justfile does not contain recipe `foo`.\n")
|
||||
.run();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn doesnt_work_with_justfile() {
|
||||
Test::new()
|
||||
.tree(tree! {
|
||||
bar: {
|
||||
justfile: "
|
||||
baz:
|
||||
echo subdir
|
||||
"
|
||||
}
|
||||
})
|
||||
.justfile(
|
||||
"
|
||||
foo:
|
||||
echo root
|
||||
",
|
||||
)
|
||||
.args(&["--unstable", "--justfile", "justfile", "foo"])
|
||||
.current_dir("bar")
|
||||
.status(EXIT_FAILURE)
|
||||
.stderr("error: Justfile does not contain recipe `foo`.\n")
|
||||
.run();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn doesnt_work_with_justfile_and_working_directory() {
|
||||
Test::new()
|
||||
.tree(tree! {
|
||||
bar: {
|
||||
justfile: "
|
||||
baz:
|
||||
echo subdir
|
||||
"
|
||||
}
|
||||
})
|
||||
.justfile(
|
||||
"
|
||||
foo:
|
||||
echo root
|
||||
",
|
||||
)
|
||||
.args(&[
|
||||
"--unstable",
|
||||
"--justfile",
|
||||
"justfile",
|
||||
"--working-directory",
|
||||
".",
|
||||
"foo",
|
||||
])
|
||||
.current_dir("bar")
|
||||
.status(EXIT_FAILURE)
|
||||
.stderr("error: Justfile does not contain recipe `foo`.\n")
|
||||
.run();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn prints_correct_error_message_when_recipe_not_found() {
|
||||
Test::new()
|
||||
.tree(tree! {
|
||||
bar: {
|
||||
justfile: "
|
||||
bar:
|
||||
echo subdir
|
||||
"
|
||||
}
|
||||
})
|
||||
.justfile(
|
||||
"
|
||||
bar:
|
||||
echo root
|
||||
",
|
||||
)
|
||||
.args(&["--unstable", "foo"])
|
||||
.current_dir("bar")
|
||||
.status(EXIT_FAILURE)
|
||||
.stderr(format!(
|
||||
"
|
||||
Trying ..{}justfile
|
||||
error: Justfile does not contain recipe `foo`.
|
||||
",
|
||||
MAIN_SEPARATOR,
|
||||
))
|
||||
.run();
|
||||
}
|
@ -19,6 +19,7 @@ mod error_messages;
|
||||
mod evaluate;
|
||||
mod examples;
|
||||
mod export;
|
||||
mod fall_back_to_parent;
|
||||
mod fmt;
|
||||
mod functions;
|
||||
mod init;
|
||||
|
@ -45,7 +45,6 @@ pub(crate) struct Test {
|
||||
pub(crate) stderr_regex: Option<Regex>,
|
||||
pub(crate) stdin: String,
|
||||
pub(crate) stdout: String,
|
||||
pub(crate) suppress_dotenv_load_warning: bool,
|
||||
pub(crate) tempdir: TempDir,
|
||||
pub(crate) unindent_stdout: bool,
|
||||
}
|
||||
@ -67,7 +66,6 @@ impl Test {
|
||||
stderr_regex: None,
|
||||
stdin: String::new(),
|
||||
stdout: String::new(),
|
||||
suppress_dotenv_load_warning: true,
|
||||
tempdir,
|
||||
unindent_stdout: true,
|
||||
}
|
||||
@ -139,11 +137,6 @@ impl Test {
|
||||
self
|
||||
}
|
||||
|
||||
pub(crate) fn suppress_dotenv_load_warning(mut self, suppress_dotenv_load_warning: bool) -> Self {
|
||||
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();
|
||||
@ -183,14 +176,6 @@ impl Test {
|
||||
let mut child = command
|
||||
.args(self.args)
|
||||
.envs(&self.env)
|
||||
.env(
|
||||
"JUST_SUPPRESS_DOTENV_LOAD_WARNING",
|
||||
if self.suppress_dotenv_load_warning {
|
||||
"1"
|
||||
} else {
|
||||
"0"
|
||||
},
|
||||
)
|
||||
.current_dir(self.tempdir.path().join(self.current_dir))
|
||||
.stdin(Stdio::piped())
|
||||
.stdout(Stdio::piped())
|
||||
|
Loading…
Reference in New Issue
Block a user