Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 37 additions & 0 deletions e2e/tasks/test_task_config_includes_templates
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
#!/usr/bin/env bash

# Test Tera rendering and home expansion for task_config.includes.

mkdir -p root-tasks "$HOME/home-tasks" "$HOME/env-home-tasks"

cat >root-tasks/task.toml <<'EOF'
[root_include]
run = 'echo "root include"'
EOF

cat >"$HOME/home-tasks/task.toml" <<'EOF'
[tilde_include]
run = 'echo "tilde include"'
EOF

cat >"$HOME/env-home-tasks/task.toml" <<'EOF'
[env_home_include]
run = 'echo "env home include"'
EOF

cat >mise.toml <<'EOF'
[task_config]
includes = [
"{{ config_root }}/root-tasks/*.toml",
"~/home-tasks/*.toml",
"{{ env.HOME }}/env-home-tasks/*.toml",
]
EOF

assert_contains "mise tasks" "root_include"
assert_contains "mise tasks" "tilde_include"
assert_contains "mise tasks" "env_home_include"

assert "mise run root_include" "root include"
assert "mise run tilde_include" "tilde include"
assert "mise run env_home_include" "env home include"
2 changes: 1 addition & 1 deletion src/cli/tasks/ls.rs
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,7 @@ impl TasksLs {
&& !cfg!(windows)
&& let Some(cwd) = &*dirs::CWD
{
let includes = config::task_includes_for_dir(cwd, &config.config_files);
let includes = config::task_includes_for_dir(cwd, &config.config_files)?;
if !find_non_executable_task_files(&includes).is_empty() {
warn!(
"no tasks found, but non-executable files exist in task directories.\nFiles must be executable to be detected as tasks. Run `chmod +x` on the task files to fix this."
Expand Down
13 changes: 13 additions & 0 deletions src/config/config_file/mise_toml.rs
Original file line number Diff line number Diff line change
Expand Up @@ -915,6 +915,19 @@ impl ConfigFile for MiseToml {
&self.task_config
}

fn task_config_includes(&self) -> eyre::Result<Option<Vec<String>>> {
self.task_config
.includes
.as_ref()
.map(|includes| {
includes
.iter()
.map(|include| self.parse_template(include))
.collect()
})
.transpose()
}

fn experimental_monorepo_root(&self) -> Option<bool> {
self.experimental_monorepo_root
}
Expand Down
4 changes: 4 additions & 0 deletions src/config/config_file/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,10 @@ pub trait ConfigFile: Debug + Send + Sync {
&DEFAULT_TASK_CONFIG
}

fn task_config_includes(&self) -> eyre::Result<Option<Vec<String>>> {
Ok(self.task_config().includes.clone())
}

fn task_templates(&self) -> IndexMap<String, TaskTemplate> {
IndexMap::new()
}
Expand Down
39 changes: 21 additions & 18 deletions src/config/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1928,7 +1928,7 @@ async fn load_local_tasks_with_context(

// If no config file exists, still load default task include dirs
if !found_config {
let includes = task_includes_for_dir(&subdir, &config.config_files);
let includes = task_includes_for_dir(&subdir, &config.config_files)?;
for include in includes {
let mut subdir_tasks =
load_tasks_includes(&config, &include, &subdir, &None).await?;
Expand Down Expand Up @@ -2376,8 +2376,10 @@ fn is_glob_pattern(pattern: &str) -> bool {

/// Expand a task include pattern (which may be a glob) to a list of paths
fn expand_task_include(dir: &Path, pattern: &str) -> Vec<PathBuf> {
if is_glob_pattern(pattern) {
match glob(dir, pattern) {
let pattern = file::replace_path(pattern);
let pattern = pattern.to_string_lossy();
if is_glob_pattern(&pattern) {
match glob(dir, &pattern) {
Ok(paths) => paths,
Err(err) => {
warn!(
Expand All @@ -2391,7 +2393,7 @@ fn expand_task_include(dir: &Path, pattern: &str) -> Vec<PathBuf> {
}
} else {
// Literal path
let path = PathBuf::from(pattern);
let path = PathBuf::from(&*pattern);
let resolved = if path.is_absolute() {
path
} else {
Expand All @@ -2411,9 +2413,7 @@ async fn load_file_tasks(
config_root: &Path,
) -> Result<Vec<Task>> {
let includes = cf
.task_config()
.includes
.clone()
.task_config_includes()?
.unwrap_or_else(default_task_includes);

let mut tasks = vec![];
Expand All @@ -2439,25 +2439,28 @@ async fn load_file_tasks(
Ok(tasks)
}

pub fn task_includes_for_dir(dir: &Path, config_files: &ConfigMap) -> Vec<PathBuf> {
pub fn task_includes_for_dir(dir: &Path, config_files: &ConfigMap) -> Result<Vec<PathBuf>> {
let configs = configs_at_root(dir, config_files);

// Find the highest-precedence config that has explicit task_config.includes
// and resolve paths relative to that config file's directory
let (includes, resolve_dir) = configs
.iter()
.find_map(|cf| {
cf.task_config().includes.clone().map(|includes| {
.find_map(|cf| match cf.task_config_includes() {
Ok(Some(includes)) => Some(Ok({
// Resolve relative paths from the config root, not the config file's directory
(includes, cf.config_root())
})
})),
Ok(None) => None,
Err(err) => Some(Err(err)),
})
.transpose()?
.unwrap_or_else(|| {
// Default includes should be resolved relative to the search directory
(default_task_includes(), dir.to_path_buf())
});

includes
Ok(includes
.into_iter()
.flat_map(|p| {
// Git URLs are handled by load_file_tasks, not here
Expand All @@ -2467,7 +2470,7 @@ pub fn task_includes_for_dir(dir: &Path, config_files: &ConfigMap) -> Vec<PathBu
expand_task_include(&resolve_dir, &p)
})
.unique()
.collect::<Vec<_>>()
.collect::<Vec<_>>())
}

pub async fn load_tasks_in_dir(
Expand All @@ -2480,12 +2483,12 @@ pub async fn load_tasks_in_dir(

let (includes, resolve_dir) = configs
.iter()
.find_map(|cf| {
cf.task_config()
.includes
.clone()
.map(|includes| (includes, cf.config_root()))
.find_map(|cf| match cf.task_config_includes() {
Ok(Some(includes)) => Some(Ok((includes, cf.config_root()))),
Ok(None) => None,
Err(err) => Some(Err(err)),
})
.transpose()?
.unwrap_or_else(|| (default_task_includes(), dir.to_path_buf()));

let mut config_tasks = vec![];
Expand Down
10 changes: 9 additions & 1 deletion src/task/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -741,7 +741,15 @@ impl Task {
let config = Config::get().await.unwrap();
let cwd = dirs::CWD.clone().unwrap_or_default();
let project_root = config.project_root.clone().unwrap_or(cwd);
for dir in config::task_includes_for_dir(&project_root, &config.config_files) {
let task_includes = match config::task_includes_for_dir(&project_root, &config.config_files)
{
Ok(includes) => includes,
Err(err) => {
warn!("failed to resolve task include paths: {err:#}");
Vec::new()
}
};
for dir in task_includes {
if dir.is_dir() && project_root.join(&dir).exists() {
return project_root.join(dir);
}
Expand Down
4 changes: 2 additions & 2 deletions src/task/task_list.rs
Original file line number Diff line number Diff line change
Expand Up @@ -240,7 +240,7 @@ async fn err_no_task(config: &Config, name: &str) -> Result<()> {
if !cfg!(windows)
&& let Some(cwd) = &*dirs::CWD
{
let includes = config::task_includes_for_dir(cwd, &config.config_files);
let includes = config::task_includes_for_dir(cwd, &config.config_files)?;
let non_exec_files = find_non_executable_task_files(&includes);
if !non_exec_files.is_empty() {
let dirs_with_files: Vec<String> = includes
Expand Down Expand Up @@ -271,7 +271,7 @@ async fn err_no_task(config: &Config, name: &str) -> Result<()> {
);
}
if let Some(cwd) = &*dirs::CWD {
let includes = config::task_includes_for_dir(cwd, &config.config_files);
let includes = config::task_includes_for_dir(cwd, &config.config_files)?;
let path = includes
.iter()
.map(|d| d.join(name))
Expand Down
Loading