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