diff --git a/README.md b/README.md index 1ae9d98..d867a17 100644 --- a/README.md +++ b/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 --------- diff --git a/src/common.rs b/src/common.rs index a88154a..339e7e2 100644 --- a/src/common.rs +++ b/src/common.rs @@ -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}, diff --git a/src/config.rs b/src/config.rs index d116355..a10826b 100644 --- a/src/config.rs +++ b/src/config.rs @@ -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) } } diff --git a/src/justfile.rs b/src/justfile.rs index eb0923c..ce0ce9f 100644 --- a/src/justfile.rs +++ b/src/justfile.rs @@ -74,10 +74,6 @@ impl<'src> Justfile<'src> { overrides: &BTreeMap, 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); diff --git a/src/search.rs b/src/search.rs index fc4aa94..eaf42f0 100644 --- a/src/search.rs +++ b/src/search.rs @@ -17,17 +17,7 @@ impl Search { invocation_directory: &Path, ) -> SearchResult { 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 { + 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, diff --git a/src/subcommand.rs b/src/subcommand.rs index 64ba573..eabb233 100644 --- a/src/subcommand.rs +++ b/src/subcommand.rs @@ -27,8 +27,8 @@ pub(crate) enum Subcommand { Init, List, Run { - overrides: BTreeMap, arguments: Vec, + overrides: BTreeMap, }, 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, + ) -> 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::() + .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, + 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() { diff --git a/tests/common.rs b/tests/common.rs index 05f53c1..1915197 100644 --- a/tests/common.rs +++ b/tests/common.rs @@ -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, }; diff --git a/tests/dotenv.rs b/tests/dotenv.rs index 35084ce..e068532 100644 --- a/tests/dotenv.rs +++ b/tests/dotenv.rs @@ -71,7 +71,6 @@ fn no_warning() { ) .stdout("unset\n") .stderr("echo ${DOTENV_KEY:-unset}\n") - .suppress_dotenv_load_warning(false) .run(); } diff --git a/tests/fall_back_to_parent.rs b/tests/fall_back_to_parent.rs new file mode 100644 index 0000000..74351a1 --- /dev/null +++ b/tests/fall_back_to_parent.rs @@ -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(); +} diff --git a/tests/lib.rs b/tests/lib.rs index 8739415..798b045 100644 --- a/tests/lib.rs +++ b/tests/lib.rs @@ -19,6 +19,7 @@ mod error_messages; mod evaluate; mod examples; mod export; +mod fall_back_to_parent; mod fmt; mod functions; mod init; diff --git a/tests/test.rs b/tests/test.rs index 77fb77b..4673ad2 100644 --- a/tests/test.rs +++ b/tests/test.rs @@ -45,7 +45,6 @@ pub(crate) struct Test { pub(crate) stderr_regex: Option, 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())