Search for missing recipes in parent directory justfiles (#1149)

This commit is contained in:
Casey Rodarmor 2022-03-30 22:13:59 -07:00 committed by GitHub
parent 1c9297452b
commit 52f73db33d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 352 additions and 61 deletions

View File

@ -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. 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 Changelog
--------- ---------

View File

@ -10,7 +10,7 @@ pub(crate) use std::{
iter::{self, FromIterator}, iter::{self, FromIterator},
mem, mem,
ops::{Index, Range, RangeInclusive}, ops::{Index, Range, RangeInclusive},
path::{Path, PathBuf}, path::{self, Path, PathBuf},
process::{self, Command, ExitStatus, Stdio}, process::{self, Command, ExitStatus, Stdio},
rc::Rc, rc::Rc,
str::{self, Chars}, str::{self, Chars},

View File

@ -606,7 +606,11 @@ impl Config {
} }
pub(crate) fn run<'src>(self, loader: &'src Loader) -> Result<(), Error<'src>> { 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)
} }
} }

View File

@ -74,10 +74,6 @@ impl<'src> Justfile<'src> {
overrides: &BTreeMap<String, String>, overrides: &BTreeMap<String, String>,
arguments: &[String], arguments: &[String],
) -> RunResult<'src, ()> { ) -> RunResult<'src, ()> {
if let Err(error) = InterruptHandler::install(config.verbosity) {
warn!("Failed to set CTRL-C handler: {}", error);
}
let unknown_overrides = overrides let unknown_overrides = overrides
.keys() .keys()
.filter(|name| !self.assignments.contains_key(name.as_str())) .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()]; let mut invocation = vec![recipe.name().to_owned()];
for argument in arguments.iter().copied() { for argument in arguments {
invocation.push(argument.to_owned()); invocation.push((*argument).to_string());
} }
ran.insert(invocation); ran.insert(invocation);

View File

@ -17,17 +17,7 @@ impl Search {
invocation_directory: &Path, invocation_directory: &Path,
) -> SearchResult<Self> { ) -> SearchResult<Self> {
match search_config { match search_config {
SearchConfig::FromInvocationDirectory => { SearchConfig::FromInvocationDirectory => Self::find_next(invocation_directory),
let justfile = Self::justfile(invocation_directory)?;
let working_directory = Self::working_directory_from_justfile(&justfile)?;
Ok(Self {
justfile,
working_directory,
})
}
SearchConfig::FromSearchDirectory { search_directory } => { SearchConfig::FromSearchDirectory { search_directory } => {
let search_directory = Self::clean(invocation_directory, search_directory); let search_directory = Self::clean(invocation_directory, search_directory);
@ -40,7 +30,6 @@ impl Search {
working_directory, working_directory,
}) })
} }
SearchConfig::WithJustfile { justfile } => { SearchConfig::WithJustfile { justfile } => {
let justfile = Self::clean(invocation_directory, justfile); let justfile = Self::clean(invocation_directory, justfile);
@ -51,7 +40,6 @@ impl Search {
working_directory, working_directory,
}) })
} }
SearchConfig::WithJustfileAndWorkingDirectory { SearchConfig::WithJustfileAndWorkingDirectory {
justfile, justfile,
working_directory, 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( pub(crate) fn init(
search_config: &SearchConfig, search_config: &SearchConfig,
invocation_directory: &Path, invocation_directory: &Path,

View File

@ -27,8 +27,8 @@ pub(crate) enum Subcommand {
Init, Init,
List, List,
Run { Run {
overrides: BTreeMap<String, String>,
arguments: Vec<String>, arguments: Vec<String>,
overrides: BTreeMap<String, String>,
}, },
Show { Show {
name: String, name: String,
@ -38,7 +38,11 @@ pub(crate) enum Subcommand {
} }
impl 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::*; use Subcommand::*;
match self { match self {
@ -48,6 +52,10 @@ impl Subcommand {
} }
Completions { shell } => return Self::completions(shell), Completions { shell } => return Self::completions(shell),
Init => return Self::init(config), 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); 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 src = loader.load(&search.justfile)?;
let tokens = Lexer::lex(src)?; let tokens = Lexer::lex(src)?;
@ -69,27 +175,7 @@ impl Subcommand {
} }
} }
match self { Ok((src, ast, justfile))
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(())
} }
fn changelog() { fn changelog() {

View File

@ -6,7 +6,7 @@ pub(crate) use std::{
fs, fs,
io::Write, io::Write,
iter, iter,
path::{Path, PathBuf}, path::{Path, PathBuf, MAIN_SEPARATOR},
process::{Command, Output, Stdio}, process::{Command, Output, Stdio},
str, str,
}; };

View File

@ -71,7 +71,6 @@ fn no_warning() {
) )
.stdout("unset\n") .stdout("unset\n")
.stderr("echo ${DOTENV_KEY:-unset}\n") .stderr("echo ${DOTENV_KEY:-unset}\n")
.suppress_dotenv_load_warning(false)
.run(); .run();
} }

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

View File

@ -19,6 +19,7 @@ mod error_messages;
mod evaluate; mod evaluate;
mod examples; mod examples;
mod export; mod export;
mod fall_back_to_parent;
mod fmt; mod fmt;
mod functions; mod functions;
mod init; mod init;

View File

@ -45,7 +45,6 @@ pub(crate) struct Test {
pub(crate) stderr_regex: Option<Regex>, pub(crate) stderr_regex: Option<Regex>,
pub(crate) stdin: String, pub(crate) stdin: String,
pub(crate) stdout: String, pub(crate) stdout: String,
pub(crate) suppress_dotenv_load_warning: bool,
pub(crate) tempdir: TempDir, pub(crate) tempdir: TempDir,
pub(crate) unindent_stdout: bool, pub(crate) unindent_stdout: bool,
} }
@ -67,7 +66,6 @@ impl Test {
stderr_regex: None, stderr_regex: None,
stdin: String::new(), stdin: String::new(),
stdout: String::new(), stdout: String::new(),
suppress_dotenv_load_warning: true,
tempdir, tempdir,
unindent_stdout: true, unindent_stdout: true,
} }
@ -139,11 +137,6 @@ impl Test {
self 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 { pub(crate) fn tree(self, mut tree: Tree) -> Self {
tree.map(|_name, content| unindent(content)); tree.map(|_name, content| unindent(content));
tree.instantiate(self.tempdir.path()).unwrap(); tree.instantiate(self.tempdir.path()).unwrap();
@ -183,14 +176,6 @@ impl Test {
let mut child = command let mut child = command
.args(self.args) .args(self.args)
.envs(&self.env) .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)) .current_dir(self.tempdir.path().join(self.current_dir))
.stdin(Stdio::piped()) .stdin(Stdio::piped())
.stdout(Stdio::piped()) .stdout(Stdio::piped())