garden/cmds/
exec.rs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
use std::sync::atomic;

use anyhow::Result;
use clap::{Parser, ValueHint};
use rayon::prelude::*;

use crate::cli::GardenOptions;
use crate::{cmd, constants, errors, model, query};

/// Evaluate garden expressions
#[derive(Parser, Clone, Debug)]
#[command(author, about, long_about)]
pub struct ExecOptions {
    /// Filter trees by name post-query using a glob pattern
    #[arg(long, short, default_value = "*")]
    trees: String,
    /// Perform a trial run without executing any commands
    #[arg(long, short = 'N', short_alias = 'n')]
    dry_run: bool,
    /// Run commands in parallel using the specified number of jobs.
    #[arg(long = "jobs", short = 'j', value_name = "JOBS")]
    num_jobs: Option<usize>,
    /// Be quiet
    #[arg(short, long)]
    quiet: bool,
    /// Increase verbosity level (default: 0)
    #[arg(short, long, action = clap::ArgAction::Count)]
    verbose: u8,
    /// Tree query for the gardens, groups or trees to run the command
    #[arg(value_hint=ValueHint::Other)]
    query: String,
    /// Command to run in the resolved environments
    #[arg(allow_hyphen_values = true, trailing_var_arg = true, required = true, value_hint=ValueHint::CommandWithArguments)]
    command: Vec<String>,
}

/// Main entry point for the "garden exec" command
pub fn main(app_context: &model::ApplicationContext, exec_options: &mut ExecOptions) -> Result<()> {
    exec_options.verbose += app_context.options.verbose;
    if app_context.options.debug_level(constants::DEBUG_LEVEL_EXEC) > 0 {
        debug!("query: {}", exec_options.query);
        debug!("command: {:?}", exec_options.command);
    }
    exec_options.quiet |= app_context.options.quiet;
    exec(app_context, exec_options)
}

/// Return true if the context has not been filtered out and refers to a valid configured tree.
fn is_valid_context(
    app_context: &model::ApplicationContext,
    pattern: &glob::Pattern,
    context: &model::TreeContext,
) -> bool {
    if !pattern.matches(&context.tree) {
        return false;
    }
    let tree_opt = match context.config {
        Some(graft_id) => app_context.get_config(graft_id).trees.get(&context.tree),
        None => app_context.get_root_config().trees.get(&context.tree),
    };
    let tree = match tree_opt {
        Some(tree) => tree,
        None => return false,
    };
    // Skip symlink trees.
    if tree.is_symlink {
        return false;
    }

    true
}

/// Execute a command over every tree in the evaluated tree query.
fn exec(app_context: &model::ApplicationContext, exec_options: &ExecOptions) -> Result<()> {
    let quiet = exec_options.quiet;
    let verbose = exec_options.verbose;
    let dry_run = exec_options.dry_run;
    let query = &exec_options.query;
    let tree_pattern = &exec_options.trees;
    let command = &exec_options.command;
    // Strategy: resolve the trees down to a set of tree indexes paired with
    // an optional garden context.
    //
    // If the names resolve to gardens, each garden is processed independently.
    // Trees that exist in multiple matching gardens will be processed multiple
    // times.
    //
    // If the names resolve to trees, each tree is processed independently
    // with no garden context.
    cmd::initialize_threads_option(exec_options.num_jobs)?;

    // Resolve the tree query into a vector of tree contexts.
    let config = app_context.get_root_config_mut();
    let contexts = query::resolve_trees(app_context, config, None, query);
    let pattern = glob::Pattern::new(tree_pattern).unwrap_or_default();
    let exit_status = atomic::AtomicI32::new(errors::EX_OK);

    // Loop over each context, evaluate the tree environment,
    // and run the command.
    if exec_options.num_jobs.is_some() {
        contexts.par_iter().for_each(|context| {
            let app_context_clone = app_context.clone();
            let app_context = &app_context_clone;
            if !is_valid_context(app_context, &pattern, context) {
                return;
            }
            // Run the command in the current context.
            if let Err(errors::GardenError::ExitStatus(status)) = cmd::exec_in_context(
                app_context,
                app_context.get_root_config(),
                context,
                quiet,
                verbose,
                dry_run,
                command,
            ) {
                exit_status.store(status, atomic::Ordering::Release);
            }
        });
    } else {
        for context in &contexts {
            if !is_valid_context(app_context, &pattern, context) {
                continue;
            }
            // Run the command in the current context.
            if let Err(errors::GardenError::ExitStatus(status)) = cmd::exec_in_context(
                app_context,
                config,
                context,
                quiet,
                verbose,
                dry_run,
                command,
            ) {
                exit_status.store(status, atomic::Ordering::Release);
            }
        }
    }

    // Return the last non-zero exit status.
    errors::exit_status_into_result(exit_status.load(atomic::Ordering::Acquire))
}