diff --git a/src/search.rs b/src/search.rs index c90307755e..9ce0f14cc5 100644 --- a/src/search.rs +++ b/src/search.rs @@ -4,7 +4,7 @@ const DEFAULT_JUSTFILE_NAME: &str = JUSTFILE_NAMES[0]; pub(crate) const JUSTFILE_NAMES: [&str; 2] = ["justfile", ".justfile"]; const PROJECT_ROOT_CHILDREN: &[&str] = &[".bzr", ".git", ".hg", ".svn", "_darcs"]; -#[derive(Debug)] +#[derive(Debug, Clone)] pub(crate) struct Search { pub(crate) justfile: PathBuf, pub(crate) working_directory: PathBuf, diff --git a/src/subcommand.rs b/src/subcommand.rs index 821403046b..7ab5a26c51 100644 --- a/src/subcommand.rs +++ b/src/subcommand.rs @@ -107,7 +107,7 @@ impl Subcommand { } Dump { format } => Self::dump(compilation, *format)?, Groups => Self::groups(config, justfile), - List { path } => Self::list(config, justfile, path)?, + List { path } => Self::list(config, loader, &search, justfile, path)?, Run { arguments } => Self::run(config, loader, search, compilation, arguments)?, Show { path } => Self::show(config, justfile, path)?, Summary => Self::summary(config, justfile), @@ -475,7 +475,13 @@ impl Subcommand { Ok(()) } - fn list(config: &Config, mut module: &Justfile, path: &ModulePath) -> RunResult<'static> { + fn list<'src>( + config: &Config, + loader: &'src Loader, + search: &Search, + mut module: &Justfile, + path: &ModulePath, + ) -> RunResult<'src> { for name in &path.path { module = module .modules @@ -487,6 +493,56 @@ impl Subcommand { Self::list_module(config, 0, &config.groups, module)?; + // If fallback is enabled and we're at the top level, also list + // recipes from parent justfiles so users can see the full set of + // available recipes. + let fallback = module.settings.fallback + && matches!( + config.search_config, + SearchConfig::FromInvocationDirectory | SearchConfig::FromSearchDirectory { .. } + ); + + if fallback && path.path.is_empty() { + let mut parent_search = search.clone(); + + while let Ok(next) = parent_search.search_parent_directory(config.ceiling.as_deref()) { + parent_search = next; + + let parent_path = parent_search + .justfile + .parent() + .unwrap_or_else(|| Path::new(".")); + + if let Ok(compilation) = Self::compile(config, loader, &parent_search) { + let parent_justfile = &compilation.justfile; + + let has_recipes = !parent_justfile + .public_recipes(config) + .is_empty() + ; + + if has_recipes { + println!(); + println!( + "{}", + config.color.stdout().doc().paint(&format!( + "# Fallback recipes from {}", + parent_path.display() + )) + ); + Self::list_module(config, parent_justfile, 0); + } + + // Stop walking if this parent doesn't also have fallback + if !parent_justfile.settings.fallback { + break; + } + } else { + break; + } + } + } + Ok(()) } diff --git a/tests/fallback.rs b/tests/fallback.rs index 7b721ae5e7..876af62898 100644 --- a/tests/fallback.rs +++ b/tests/fallback.rs @@ -368,3 +368,31 @@ fn works_with_modules() { .stdout("BAZ\n") .success(); } + +#[test] +fn list_shows_fallback_recipes() { + Test::new() + .justfile( + " + # A parent recipe + parent-recipe: + echo parent + ", + ) + .write( + "sub/justfile", + unindent( + " + set fallback + + # A child recipe + child-recipe: + echo child + ", + ), + ) + .args(["--list"]) + .current_dir("sub") + .stdout_regex("(?s).*child-recipe.*Fallback.*parent-recipe.*") + .success(); +}