Skip to main content

garden/config/
reader.rs

1use yaml_rust::{yaml, Yaml, YamlLoader};
2
3use crate::{
4    constants, errors, eval, model,
5    model::{IndexMap, StringSet},
6    path, syntax,
7};
8
9/// Apply YAML Configuration from a string.
10pub fn parse(
11    app_context: &model::ApplicationContext,
12    string: &str,
13    config_verbose: u8,
14    config: &mut model::Configuration,
15) -> Result<(), errors::GardenError> {
16    parse_recursive(app_context, string, config_verbose, config, None)
17}
18
19/// The recursive guts of `parse()`.
20fn parse_recursive(
21    app_context: &model::ApplicationContext,
22    string: &str,
23    config_verbose: u8,
24    config: &mut model::Configuration,
25    current_include: Option<&std::path::Path>,
26) -> Result<(), errors::GardenError> {
27    let docs =
28        YamlLoader::load_from_str(string).map_err(|scan_err| errors::GardenError::ReadConfig {
29            err: scan_err,
30            path: config.get_path_for_display(),
31        })?;
32    if docs.is_empty() {
33        return Err(errors::GardenError::EmptyConfiguration {
34            path: config.get_path()?.into(),
35        });
36    }
37    let doc = &docs[0];
38
39    // Debug support
40    if config_verbose > 2 {
41        dump_node(doc, 1, "");
42    }
43
44    // garden.root
45    // Includes can cause parsing to update an object multiple times but only want to special-case
46    // emptiness of `garden.root` on the first pass. `root_is_dynamic` will only be set if
47    // we have already been here and do not need to reset `garden.root`.
48    if config.root.is_empty()
49        && !config.root_is_dynamic
50        && get_raw_str(
51            &doc[constants::GARDEN][constants::ROOT],
52            config.root.get_expr_mut(),
53        )
54    {
55        if config.root.is_empty() {
56            // The `garden.root` is dynamic and sensitive to the current directory
57            // when configured to the empty "" string.
58            config.root_is_dynamic = true;
59        }
60        if config_verbose > 0 {
61            debug!("config: garden.root = {}", config.root.get_expr());
62        }
63    }
64
65    // garden.shell
66    if get_str(&doc[constants::GARDEN][constants::SHELL], &mut config.shell) && config_verbose > 0 {
67        debug!("config: {} = {}", constants::GARDEN_SHELL, config.shell);
68    }
69    // garden.interactive-shell
70    if get_str(
71        &doc[constants::GARDEN][constants::INTERACTIVE_SHELL],
72        &mut config.interactive_shell,
73    ) && config_verbose > 0
74    {
75        debug!(
76            "config: {} = {}",
77            constants::GARDEN_INTERACTIVE_SHELL,
78            config.interactive_shell
79        );
80    }
81
82    // garden.shell-errexit
83    if get_bool(
84        &doc[constants::GARDEN][constants::SHELL_ERREXIT],
85        &mut config.shell_exit_on_error,
86    ) && config_verbose > 0
87    {
88        debug!(
89            "config: {} = {}",
90            constants::GARDEN_SHELL_ERREXIT,
91            config.shell_exit_on_error
92        );
93    }
94    // garden.shell-wordsplit
95    if get_bool(
96        &doc[constants::GARDEN][constants::SHELL_WORDSPLIT],
97        &mut config.shell_word_split,
98    ) && config_verbose > 0
99    {
100        debug!(
101            "config: {} = {}",
102            constants::GARDEN_SHELL_WORDSPLIT,
103            config.shell_word_split
104        );
105    }
106    // garden.tree-branches
107    if get_bool(
108        &doc[constants::GARDEN][constants::TREE_BRANCHES],
109        &mut config.tree_branches,
110    ) && config_verbose > 0
111    {
112        debug!(
113            "config: {} = {}",
114            constants::GARDEN_TREE_BRANCHES,
115            config.tree_branches
116        );
117    }
118
119    // GARDEN_ROOT and GARDEN_CONFIG_DIR are relative to the root configuration.
120    // Referencing these variables from garden files included using garden.includes
121    // resolves to the root config's location, not the included location.
122    if config_verbose > 1 {
123        debug!("config: built-in variables");
124    }
125    // Provide GARDEN_ROOT.
126    config.variables.insert(
127        string!(constants::GARDEN_ROOT),
128        model::Variable::from_expr(
129            constants::GARDEN_ROOT.to_string(),
130            config.root.get_expr().to_string(),
131        ),
132    );
133
134    if let Some(config_path_raw) = config.dirname.as_ref() {
135        // Calculate an absolute path for GARDEN_CONFIG_DIR.
136        if let Ok(config_path) = path::canonicalize(config_path_raw) {
137            config.variables.insert(
138                string!(constants::GARDEN_CONFIG_DIR),
139                model::Variable::from_expr(
140                    constants::GARDEN_CONFIG_DIR.to_string(),
141                    config_path.to_string_lossy().to_string(),
142                ),
143            );
144        }
145    }
146
147    // Provide GARDEN_CMD_QUIET and GARDEN_CMD_VERBOSE.
148    // When commands call update_quiet_and_verbose_variables() they are adding
149    // additional "-v" options beyond what has been parsed at this point.
150    // We use 0 here so that the config's verbosity level is maintained.
151    config.update_quiet_and_verbose_variables(config.quiet, 0);
152
153    // Variables are read early to make them available to config.eval_config_pathbuf_from_include().
154    // Variables are reloaded after "includes" to give the current garden file the highest priority.
155    if !get_variables_map(&doc[constants::VARIABLES], &mut config.variables) && config_verbose > 1 {
156        debug!("config: no variables");
157    }
158
159    // Process "includes" after initializing the GARDEN_ROOT and GARDEN_CONFIG_DIR.
160    // This allows the path strings to reference these ${variables}.
161    // This also means that variables defined by the outer-most garden config
162    // override the same variables when also defined in an included garden file.
163    let mut config_includes = Vec::new();
164    if get_vec_variables(
165        constants::INCLUDES,
166        &doc[constants::GARDEN][constants::INCLUDES],
167        &mut config_includes,
168    ) {
169        for garden_include in &config_includes {
170            let pathbuf = match config.eval_config_pathbuf_from_include(
171                app_context,
172                current_include,
173                garden_include.get_expr(),
174            ) {
175                Some(pathbuf) => pathbuf,
176                None => continue,
177            };
178            if !pathbuf.exists() {
179                if config_verbose > 0 {
180                    debug!(
181                        "warning: garden.includes entry not found: {:?} -> {:?}",
182                        garden_include, pathbuf
183                    );
184                }
185                continue;
186            }
187            if pathbuf.exists() {
188                if let Ok(content) = std::fs::read_to_string(&pathbuf) {
189                    parse_recursive(
190                        app_context,
191                        &content,
192                        config_verbose,
193                        config,
194                        Some(&pathbuf),
195                    )
196                    .unwrap_or(());
197                }
198            }
199        }
200
201        // Reload variables after processing includes. This gives the local garden file the highest priority
202        // when defining variables while also making variables available to the "includes" lines.
203        if !get_variables_map(&doc[constants::VARIABLES], &mut config.variables)
204            && config_verbose > 1
205        {
206            debug!("config: no reloaded variables");
207        }
208    }
209
210    // grafts
211    if config_verbose > 1 {
212        debug!("config: grafts");
213    }
214    if !get_grafts(&doc[constants::GRAFTS], &mut config.grafts) && config_verbose > 1 {
215        debug!("config: no grafts");
216    }
217
218    get_multivariables(&doc[constants::ENVIRONMENT], &mut config.environment);
219
220    // commands
221    if config_verbose > 1 {
222        debug!("config: commands");
223    }
224    if !get_multivariables_map(&doc[constants::COMMANDS], &mut config.commands)
225        && config_verbose > 1
226    {
227        debug!("config: no commands");
228    }
229
230    // templates
231    if config_verbose > 1 {
232        debug!("config: templates");
233    }
234    if !get_templates(
235        &doc["templates"],
236        &config.templates.clone(),
237        &mut config.templates,
238    ) && config_verbose > 1
239    {
240        debug!("config: no templates");
241    }
242
243    // trees
244    if config_verbose > 1 {
245        debug!("config: trees");
246    }
247    if !get_trees(app_context, config, &doc[constants::TREES]) && config_verbose > 1 {
248        debug!("config: no trees");
249    }
250
251    // groups
252    if config_verbose > 1 {
253        debug!("config: groups");
254    }
255    if !get_groups(&doc[constants::GROUPS], &mut config.groups) && config_verbose > 1 {
256        debug!("config: no groups");
257    }
258
259    // gardens
260    if config_verbose > 1 {
261        debug!("config: gardens");
262    }
263    if !get_gardens(&doc[constants::GARDENS], &mut config.gardens) && config_verbose > 1 {
264        debug!("config: no gardens");
265    }
266
267    Ok(())
268}
269
270/// Print 4 spaces for every indent level.
271fn print_indent(indent: usize) {
272    for _ in 0..indent {
273        print!("    ");
274    }
275}
276
277/// Dump a Yaml node for debugging purposes.
278fn dump_node(yaml: &Yaml, indent: usize, prefix: &str) {
279    match yaml {
280        Yaml::String(value) => {
281            print_indent(indent);
282            println!("{prefix}\"{value}\"");
283        }
284        Yaml::Array(value) => {
285            for x in value {
286                dump_node(x, indent + 1, "- ");
287            }
288        }
289        Yaml::Hash(hash) => {
290            for (k, v) in hash {
291                print_indent(indent);
292                match k {
293                    Yaml::String(x) => {
294                        println!("{prefix}{x}:");
295                    }
296                    _ => {
297                        println!("{prefix}{k:?}:");
298                    }
299                }
300                dump_node(v, indent + 1, prefix);
301            }
302        }
303        _ => {
304            print_indent(indent);
305            println!("{yaml:?}");
306        }
307    }
308}
309
310/// Extract a `String` from `yaml`.
311/// Return `false` when `yaml` is not a `Yaml::String`.
312fn get_raw_str(yaml: &Yaml, string: &mut String) -> bool {
313    match yaml {
314        Yaml::String(yaml_string) => {
315            string.clone_from(yaml_string);
316            true
317        }
318        _ => false,
319    }
320}
321
322/// Extract a `String` from `yaml`.
323/// Return `false` when the string is empty or `yaml` is not a `Yaml::String`.
324fn get_str(yaml: &Yaml, string: &mut String) -> bool {
325    get_raw_str(yaml, string) && !string.is_empty()
326}
327
328/// Extract a String from Yaml and trim the end of the value.
329/// Return `false` when the string is empty or `yaml` is not a `Yaml::String`.
330fn get_str_trimmed(yaml: &Yaml, string: &mut String) -> bool {
331    match yaml {
332        Yaml::String(yaml_string) => {
333            *string = yaml_string.trim_end().to_string();
334            !string.is_empty()
335        }
336        _ => false,
337    }
338}
339
340/// Extract an `i64` from `yaml`. Return `false` when `yaml` is not a `Yaml::Integer`.
341fn get_i64(yaml: &Yaml, value: &mut i64) -> bool {
342    match yaml {
343        Yaml::Integer(yaml_integer) => {
344            *value = *yaml_integer;
345            true
346        }
347        _ => false,
348    }
349}
350
351/// Extract a `bool` from `yaml`. Return `false` when `yaml` is not a `Yaml::Boolean`.
352fn get_bool(yaml: &Yaml, value: &mut bool) -> bool {
353    match yaml {
354        Yaml::Boolean(yaml_bool) => {
355            *value = *yaml_bool;
356            true
357        }
358        _ => false,
359    }
360}
361
362/// Extract a `StringSet` from `Yaml::String` or `Yaml::Array<Yaml::String>`.
363/// Return `false` when `yaml` is not `Yaml::String` or `Yaml::Array<Yaml::String>`.
364/// This function promotes a scalar `Yaml::String` into a `StringSet`
365/// with a single entry.
366fn get_indexset_str(yaml: &Yaml, values: &mut StringSet) -> bool {
367    match yaml {
368        Yaml::String(yaml_string) => {
369            values.insert(yaml_string.clone());
370            true
371        }
372        Yaml::Array(yaml_vec) => {
373            for value in yaml_vec {
374                if let Yaml::String(value_str) = value {
375                    values.insert(value_str.clone());
376                }
377            }
378            true
379        }
380        _ => false,
381    }
382}
383
384/// Construct a model::Variable from a ran YAML object.
385fn variable_from_yaml(name: String, yaml: &Yaml) -> Option<model::Variable> {
386    match yaml {
387        Yaml::String(yaml_str) => Some(model::Variable::from_expr(name, yaml_str.to_string())),
388        Yaml::Array(yaml_array) => {
389            // If we see an array then last value wins.
390            yaml_array
391                .iter()
392                .next_back()
393                .and_then(|yaml_value| variable_from_yaml(name, yaml_value))
394        }
395        Yaml::Integer(yaml_int) => {
396            // Integers are already resolved.
397            let int_value = yaml_int.to_string();
398
399            Some(model::Variable::from_resolved_expr(name, int_value))
400        }
401        Yaml::Boolean(yaml_bool) => {
402            // Booleans are already resolved.
403            let bool_value = syntax::bool_to_string(*yaml_bool);
404
405            Some(model::Variable::from_resolved_expr(name, bool_value))
406        }
407        Yaml::Hash(yaml_hash) => {
408            let expr = yaml_hash
409                .get(&Yaml::String(constants::VALUE.to_string()))
410                .and_then(|v| v.as_str())
411                .unwrap_or_default()
412                .to_string();
413            let required = yaml_hash
414                .get(&Yaml::String(constants::REQUIRED.to_string()))
415                .and_then(|v| v.as_bool())
416                .unwrap_or(false);
417
418            Some(model::Variable::new(name, expr, required))
419        }
420        _ => {
421            // dump_node(yaml, 1, "");
422            None
423        }
424    }
425}
426
427// Extract a `Variable` from `yaml`. Return `false` when `yaml` is not a `Yaml::String`.
428fn get_variable(name: String, yaml: &Yaml, value: &mut model::Variable) -> bool {
429    if let Some(variable) = variable_from_yaml(name, yaml) {
430        *value = variable;
431
432        true
433    } else {
434        false
435    }
436}
437
438/// Promote `Yaml::String` or `Yaml::Array<Yaml::String>` into a `Vec<Variable>`.
439fn get_vec_variables(name: &str, yaml: &Yaml, vec: &mut Vec<model::Variable>) -> bool {
440    if let Yaml::Array(yaml_array) = yaml {
441        for value in yaml_array {
442            if let Some(variable) = variable_from_yaml(name.to_string(), value) {
443                vec.push(variable);
444            }
445        }
446        return true;
447    }
448
449    if let Some(variable) = variable_from_yaml(name.to_string(), yaml) {
450        vec.push(variable);
451        return true;
452    }
453
454    false
455}
456
457/// Extract variable definitions from a `yaml::Hash` into a `VariablesMap`.
458/// Return `false` when `yaml` is not a `Yaml::Hash`.
459fn get_variables_map(yaml: &Yaml, map: &mut model::VariableMap) -> bool {
460    match yaml {
461        Yaml::Hash(hash) => {
462            for (k, v) in hash {
463                let key = match k.as_str() {
464                    Some(key_value) => key_value.to_string(),
465                    None => {
466                        continue;
467                    }
468                };
469                if let Some(variable) = variable_from_yaml(key.to_string(), v) {
470                    map.insert(key, variable);
471                }
472            }
473            true
474        }
475        _ => false,
476    }
477}
478
479/// Read `MultiVariable` definitions (e.g. "commands" and "environment").
480fn get_multivariables(yaml: &Yaml, vec: &mut Vec<model::MultiVariable>) -> bool {
481    if let Yaml::Hash(hash) = yaml {
482        for (k, v) in hash {
483            let key = match k.as_str() {
484                Some(key_value) => key_value.to_string(),
485                None => continue,
486            };
487            // Special-case arrays.
488            if let Yaml::Array(yaml_array) = v {
489                let mut variables = Vec::new();
490                for value in yaml_array {
491                    if let Some(variable) = variable_from_yaml(key.to_string(), value) {
492                        variables.push(variable);
493                    }
494                }
495                vec.push(model::MultiVariable::new(key, variables));
496                continue;
497            }
498
499            if let Some(variable) = variable_from_yaml(key.to_string(), v) {
500                let variables = vec![variable];
501                vec.push(model::MultiVariable::new(key, variables));
502            }
503        }
504
505        return true;
506    }
507
508    false
509}
510
511/// Read a `Yaml::Hash` of variable definitions into a `MultiVariableMap`.
512fn get_multivariables_map(yaml: &Yaml, multivariables: &mut model::MultiVariableMap) -> bool {
513    match yaml {
514        Yaml::Hash(hash) => {
515            for (k, v) in hash {
516                let key = match k.as_str() {
517                    Some(key_value) => key_value.to_string(),
518                    None => continue,
519                };
520                if let Yaml::Array(yaml_array) = v {
521                    let mut variables = Vec::new();
522                    for value in yaml_array {
523                        if let Some(variable) = variable_from_yaml(key.to_string(), value) {
524                            variables.push(variable);
525                        }
526                    }
527                    multivariables.insert(key, variables);
528                    continue;
529                }
530
531                if let Some(variable) = variable_from_yaml(key.to_string(), v) {
532                    let variables = vec![variable];
533                    multivariables.insert(key, variables);
534                }
535            }
536            true
537        }
538        _ => false,
539    }
540}
541
542/// Read template definitions.
543fn get_templates(
544    yaml: &Yaml,
545    config_templates: &IndexMap<String, model::Template>,
546    templates: &mut IndexMap<String, model::Template>,
547) -> bool {
548    match yaml {
549        Yaml::Hash(hash) => {
550            for (name, value) in hash {
551                let template_name = match &name.as_str() {
552                    Some(template_name) => template_name.to_string(),
553                    None => continue,
554                };
555                templates.insert(
556                    template_name,
557                    get_template(name, value, config_templates, yaml),
558                );
559            }
560            true
561        }
562        _ => false,
563    }
564}
565
566/// Read a single template definition.
567fn get_template(
568    name: &Yaml,
569    value: &Yaml,
570    config_templates: &IndexMap<String, model::Template>,
571    templates: &Yaml,
572) -> model::Template {
573    let mut template = model::Template::default();
574    get_str(name, template.get_name_mut());
575
576    {
577        let mut url = String::new();
578        // If the YAML configuration is just a single string value then the template
579        // expands out to url: <string-value> only.
580        // templates:
581        //   example: git://git.example.org/example/repo.git
582        if get_str(value, &mut url) {
583            template.tree.remotes.insert(
584                constants::ORIGIN.to_string(),
585                model::Variable::from_expr(constants::ORIGIN.to_string(), url),
586            );
587            return template;
588        }
589        // If a `<url>` is configured then populate the "origin" remote.
590        // The first remote is "origin" by convention.
591        if get_str(&value[constants::URL], &mut url) {
592            template.tree.remotes.insert(
593                string!(constants::ORIGIN),
594                model::Variable::from_expr(constants::URL.to_string(), url),
595            );
596        }
597    }
598
599    // Process the base templates in the specified order before processing
600    // the template itself. Any "VAR=" variables will be overridden
601    // by the tree entry itself, or the last template processed.
602    // "environment" follow last-set-wins semantics.
603    get_indexset_str(&value[constants::EXTEND], &mut template.extend);
604    for template_name in &template.extend {
605        // First check if we have this template in the local YAML data.
606        // We check here first so that parsing is not order-dependent.
607        if let Yaml::Hash(_) = templates[template_name.as_ref()] {
608            let base = get_template(
609                &Yaml::String(template_name.clone()),
610                &templates[template_name.as_ref()],
611                config_templates,
612                templates,
613            );
614
615            base.apply(&mut template.tree);
616        } else {
617            // If the template didn't exist in the local YAML then read it from
618            // the previously-parsed templates. This allows templates to be used
619            // from include files where the template definition is in a different
620            // file and not present in the current YAML payload.
621            if let Some(base) = config_templates.get(template_name) {
622                base.apply(&mut template.tree);
623            }
624        }
625        // The base templates were already processed.
626        template.tree.templates.truncate(0);
627    }
628
629    get_tree_fields(value, &mut template.tree);
630
631    template
632}
633
634/// Read tree definitions.
635fn get_trees(
636    app_context: &model::ApplicationContext,
637    config: &mut model::Configuration,
638    yaml: &Yaml,
639) -> bool {
640    match yaml {
641        Yaml::Hash(hash) => {
642            for (name, value) in hash {
643                if let Yaml::String(url) = value {
644                    // If the tree already exists then update it, otherwise create a new entry.
645                    let tree = get_tree_from_url(name, url);
646                    if let Some(current_tree) = config.trees.get_mut(tree.get_name()) {
647                        current_tree.clone_from_tree(&tree);
648                    } else {
649                        config.trees.insert(tree.get_name().to_string(), tree);
650                    }
651                } else {
652                    let tree = get_tree(app_context, config, name, value, hash, true);
653
654                    // Should we replace the current entry or sparsely override it?
655                    // We sparsely override by default.
656                    let replace = match value[constants::REPLACE] {
657                        Yaml::Boolean(value) => value,
658                        _ => false,
659                    };
660
661                    let current_tree_opt = config.trees.get_mut(tree.get_name());
662                    match current_tree_opt {
663                        Some(current_tree) if !replace => {
664                            current_tree.clone_from_tree(&tree);
665                        }
666                        _ => {
667                            config.trees.insert(tree.get_name().to_string(), tree);
668                        }
669                    }
670                }
671            }
672            true
673        }
674        _ => false,
675    }
676}
677
678/// Return a tree from a oneline `tree: <url>` entry.
679fn get_tree_from_url(name: &Yaml, url: &str) -> model::Tree {
680    let mut tree = model::Tree::default();
681
682    // Tree name
683    get_str(name, tree.get_name_mut());
684    // Default to the name when "path" is unspecified.
685    let tree_name = tree.get_name().to_string();
686    tree.get_path_mut().set_expr(tree_name.to_string());
687    tree.get_path().set_value(tree_name);
688    tree.add_builtin_variables();
689    if syntax::is_git_dir(tree.get_path().get_expr()) {
690        tree.is_bare_repository = true;
691    }
692    tree.remotes.insert(
693        constants::ORIGIN.to_string(),
694        model::Variable::from_expr(constants::ORIGIN.to_string(), url.to_string()),
695    );
696
697    tree
698}
699
700/// Read fields common to trees and templates.
701#[inline]
702fn get_tree_fields(value: &Yaml, tree: &mut model::Tree) {
703    get_variables_map(&value[constants::VARIABLES], &mut tree.variables);
704    get_multivariables_map(&value[constants::GITCONFIG], &mut tree.gitconfig);
705    get_str(&value[constants::DEFAULT_REMOTE], &mut tree.default_remote);
706    get_str_trimmed(&value[constants::DESCRIPTION], &mut tree.description);
707    get_str_variables_map(&value[constants::REMOTES], &mut tree.remotes);
708    get_vec_variables(constants::LINKS, &value[constants::LINKS], &mut tree.links);
709
710    get_multivariables(&value[constants::ENVIRONMENT], &mut tree.environment);
711    get_multivariables_map(&value[constants::COMMANDS], &mut tree.commands);
712
713    get_variable(
714        constants::BRANCH.to_string(),
715        &value[constants::BRANCH],
716        &mut tree.branch,
717    );
718    get_variables_map(&value[constants::BRANCHES], &mut tree.branches);
719    get_variable(
720        constants::SYMLINK.to_string(),
721        &value[constants::SYMLINK],
722        &mut tree.symlink,
723    );
724    get_variable(
725        constants::WORKTREE.to_string(),
726        &value[constants::WORKTREE],
727        &mut tree.worktree,
728    );
729
730    get_i64(&value[constants::DEPTH], &mut tree.clone_depth);
731    get_bool(&value[constants::BARE], &mut tree.is_bare_repository);
732    get_bool(&value[constants::SINGLE_BRANCH], &mut tree.is_single_branch);
733
734    // Load the URL and store it in the "origin" remote.
735    {
736        let mut url = String::new();
737        if get_str(&value[constants::URL], &mut url) {
738            tree.remotes.insert(
739                tree.default_remote.to_string(),
740                model::Variable::from_expr(constants::URL.to_string(), url),
741            );
742        }
743    }
744
745    tree.update_flags();
746}
747
748/// Read a single tree definition.
749fn get_tree(
750    app_context: &model::ApplicationContext,
751    config: &mut model::Configuration,
752    name: &Yaml,
753    value: &Yaml,
754    trees: &yaml::Hash,
755    variables: bool,
756) -> model::Tree {
757    // The tree that will be built and returned.
758    let mut tree = model::Tree::default();
759
760    // Allow extending an existing tree by specifying "extend".
761    let mut extend = String::new();
762    if get_str(&value[constants::EXTEND], &mut extend) {
763        // Holds a base tree specified using "extend: <tree>".
764        let tree_name = Yaml::String(extend.clone());
765        if let Some(tree_values) = trees.get(&tree_name) {
766            let base_tree = get_tree(app_context, config, &tree_name, tree_values, trees, false);
767            tree.clone_from_tree(&base_tree);
768        } else {
769            // Allow the referenced tree to be found from an earlier include.
770            if let Some(base) = config.get_tree(&extend) {
771                tree.clone_from_tree(base);
772            }
773        }
774        tree.templates.truncate(0); // Base templates were already processed.
775    }
776
777    // Load values from the parent tree when using "worktree: <parent>".
778    let mut parent_expr = String::new();
779    if get_str(&value[constants::WORKTREE], &mut parent_expr) {
780        let parent_name = eval::value(app_context, config, &parent_expr);
781        if !parent_expr.is_empty() {
782            let tree_name = Yaml::String(parent_name);
783            if let Some(tree_values) = trees.get(&tree_name) {
784                let base = get_tree(app_context, config, &tree_name, tree_values, trees, true);
785                tree.clone_from_tree(&base);
786            }
787        }
788        tree.templates.truncate(0); // Base templates were already processed.
789    }
790
791    // Templates
792    // Process the base templates in the specified order before processing
793    // the template itself.
794    get_indexset_str(&value[constants::TEMPLATES], &mut tree.templates);
795    for template_name in &tree.templates.clone() {
796        // Do we have a template by this name? If so, apply the template.
797        if let Some(template) = config.templates.get(template_name) {
798            template.apply(&mut tree);
799        }
800    }
801
802    // Tree name
803    get_str(name, tree.get_name_mut());
804
805    // Tree path
806    if !get_str(&value[constants::PATH], tree.get_path_mut().get_expr_mut()) {
807        // Default to the name when "path" is unspecified.
808        let tree_name = tree.get_name().to_string();
809        tree.get_path_mut().set_expr(tree_name.to_string());
810        tree.get_path().set_value(tree_name);
811    }
812
813    // Detect bare repositories.
814    if syntax::is_git_dir(tree.get_path().get_expr()) {
815        tree.is_bare_repository = true;
816    }
817
818    if variables {
819        tree.add_builtin_variables();
820    }
821
822    get_tree_fields(value, &mut tree);
823
824    tree
825}
826
827/// Read simple string values into a garden::model::VariableMap.
828fn get_str_variables_map(yaml: &Yaml, remotes: &mut model::VariableMap) {
829    let hash = match yaml {
830        Yaml::Hash(hash) => hash,
831        _ => return,
832    };
833    for (name, value) in hash {
834        if let (Some(name_str), Some(value_str)) = (name.as_str(), value.as_str()) {
835            remotes.insert(
836                name_str.to_string(),
837                model::Variable::from_expr(name_str.to_string(), value_str.to_string()),
838            );
839        }
840    }
841}
842
843/// Read group definitions. Return `false` when `yaml` is not a `Yaml::Hash`.
844fn get_groups(yaml: &Yaml, groups: &mut IndexMap<model::GroupName, model::Group>) -> bool {
845    match yaml {
846        Yaml::Hash(hash) => {
847            for (name, value) in hash {
848                let mut group = model::Group::default();
849                get_str(name, group.get_name_mut());
850                get_indexset_str(value, &mut group.members);
851                groups.insert(group.get_name_owned(), group);
852            }
853            true
854        }
855        _ => false,
856    }
857}
858
859/// Read garden definitions. Return `false` when `yaml` is not a `Yaml::Hash`.
860fn get_gardens(yaml: &Yaml, gardens: &mut IndexMap<String, model::Garden>) -> bool {
861    match yaml {
862        Yaml::Hash(hash) => {
863            for (name, value) in hash {
864                let mut garden = model::Garden::default();
865                get_str(name, garden.get_name_mut());
866                get_indexset_str(&value[constants::GROUPS], &mut garden.groups);
867                get_indexset_str(&value[constants::TREES], &mut garden.trees);
868                get_multivariables_map(&value[constants::GITCONFIG], &mut garden.gitconfig);
869                get_variables_map(&value[constants::VARIABLES], &mut garden.variables);
870                get_multivariables(&value[constants::ENVIRONMENT], &mut garden.environment);
871                get_multivariables_map(&value[constants::COMMANDS], &mut garden.commands);
872                gardens.insert(garden.get_name().to_string(), garden);
873            }
874            true
875        }
876        _ => false,
877    }
878}
879
880/// Read a "grafts" block from `yaml` into a `Vec<Graft>`.
881/// Return `false` when `yaml` is not a `Yaml::Hash`.
882fn get_grafts(yaml: &Yaml, grafts: &mut IndexMap<model::GardenName, model::Graft>) -> bool {
883    match yaml {
884        Yaml::Hash(yaml_hash) => {
885            for (name, value) in yaml_hash {
886                let graft = get_graft(name, value);
887                grafts.insert(graft.get_name().to_string(), graft);
888            }
889            true
890        }
891        _ => false,
892    }
893}
894
895/// Read a Graft entry from `Yaml`.
896fn get_graft(name: &Yaml, graft: &Yaml) -> model::Graft {
897    let mut graft_name = String::new();
898    let mut config = String::new();
899    let mut root = String::new();
900
901    get_str(name, &mut graft_name);
902
903    if !get_str(graft, &mut config) {
904        // The root was not specified.
905        if let Yaml::Hash(_hash) = graft {
906            // A config expression and root might be specified.
907            get_str(&graft[constants::CONFIG], &mut config);
908            get_str(&graft[constants::ROOT], &mut root);
909        }
910    }
911
912    model::Graft::new(graft_name, root, config)
913}
914
915/// Read and parse YAML from a file path.
916pub fn read_yaml<P>(path: P) -> Result<Yaml, errors::GardenError>
917where
918    P: std::convert::AsRef<std::path::Path> + std::fmt::Debug,
919{
920    let string =
921        std::fs::read_to_string(&path).map_err(|io_err| errors::GardenError::ReadFile {
922            path: path.as_ref().into(),
923            err: io_err,
924        })?;
925
926    let docs =
927        YamlLoader::load_from_str(&string).map_err(|err| errors::GardenError::ReadConfig {
928            err,
929            path: path.as_ref().display().to_string(),
930        })?;
931    if docs.is_empty() {
932        return Err(errors::GardenError::EmptyConfiguration {
933            path: path.as_ref().into(),
934        });
935    }
936
937    Ok(docs[0].clone())
938}
939
940/// Return an empty `Yaml::Hash` as a `Yaml` document.
941pub fn empty_doc() -> Yaml {
942    Yaml::Hash(yaml::Hash::new())
943}
944
945/// Add a top-level section to a Yaml configuration.
946pub(crate) fn add_section(key: &str, doc: &mut Yaml) -> Result<(), errors::GardenError> {
947    let exists = doc[key].as_hash().is_some();
948    if !exists {
949        if let Yaml::Hash(doc_hash) = doc {
950            let key = Yaml::String(key.to_string());
951            doc_hash.insert(key, Yaml::Hash(yaml::Hash::new()));
952        } else {
953            return Err(errors::GardenError::InvalidConfiguration {
954                msg: "document is not a hash".into(),
955            });
956        }
957    }
958
959    Ok(())
960}