Skip to main content

garden/
query.rs

1use crate::{constants, errors, eval, model, path, query, syntax};
2
3/// Resolve a tree query into a `Vec<garden::model::TreeContext>`.
4///
5/// Parameters:
6/// - `config`: `&garden::model::Configuration`.
7/// - `query`: Tree query `&str`.
8///
9/// Returns:
10/// - `Vec<garden::model::TreeContext>`
11pub fn resolve_trees(
12    app_context: &model::ApplicationContext,
13    config: &model::Configuration,
14    graft_config: Option<&model::Configuration>,
15    query: &str,
16) -> Vec<model::TreeContext> {
17    let mut result = Vec::new();
18    let tree_query = model::TreeQuery::new(query);
19    let pattern = &tree_query.pattern;
20
21    if tree_query.include_gardens {
22        result = garden_trees(app_context, config, graft_config, pattern);
23        if !result.is_empty() {
24            return result;
25        }
26    }
27
28    let mut group_found = false;
29    if tree_query.include_groups {
30        if let Some(graft_cfg) = graft_config {
31            for (name, group) in &graft_cfg.groups {
32                // Find the matching group
33                if !pattern.matches(name) {
34                    continue;
35                }
36                // Matching group found, collect its trees
37                result.append(&mut trees_from_group(
38                    app_context,
39                    config,
40                    graft_config,
41                    None,
42                    group,
43                ));
44                group_found = true;
45            }
46        }
47        if !group_found {
48            for (name, group) in &config.groups {
49                // Find the matching group
50                if !pattern.matches(name) {
51                    continue;
52                }
53                // Matching group found, collect its trees
54                result.append(&mut trees_from_group(
55                    app_context,
56                    config,
57                    graft_config,
58                    None,
59                    group,
60                ));
61            }
62            if !result.is_empty() {
63                return result;
64            }
65        }
66    }
67
68    // No matching gardens or groups were found.
69    // Search for matching trees.
70    if tree_query.include_trees {
71        if syntax::is_graft(query) {
72            if let Ok((graft_id, remainder)) = config.get_graft_id(query) {
73                result.append(&mut resolve_trees(
74                    app_context,
75                    config,
76                    Some(app_context.get_config(graft_id)),
77                    remainder,
78                ));
79            }
80        } else if let Some(graft_cfg) = graft_config {
81            result.append(&mut trees(graft_cfg, pattern));
82        } else {
83            result.append(&mut trees(config, pattern));
84        }
85        if !result.is_empty() {
86            return result;
87        }
88    }
89
90    // Lowest precedence: match paths on the filesystem.
91    // The pattern is a default non-special pattern, and its value points to an
92    // existing tree on the filesystem.  Look up the tree context for this
93    // entry and use the matching tree.
94    if tree_query.is_default {
95        if let Some(ctx) = tree_from_path(config, &tree_query.query) {
96            result.push(ctx);
97        }
98    }
99
100    result
101}
102
103/// Resolve a tree query into a filtered `Vec<garden::model::TreeContext>`.
104///
105/// Parameters:
106/// - `config`: `&garden::model::Configuration`.
107/// - `query`: Tree query `&str`.
108/// - `pattern`: Tree name glob pattern used to filter the results.
109///
110/// Returns:
111/// - `Vec<garden::model::TreeContext>`
112pub(crate) fn resolve_and_filter_trees(
113    app_context: &model::ApplicationContext,
114    config: &model::Configuration,
115    query: &str,
116    pattern: &str,
117) -> Vec<model::TreeContext> {
118    let contexts = resolve_trees(app_context, config, None, query);
119    let tree_pattern = glob::Pattern::new(pattern).unwrap_or_default();
120    let mut result = Vec::with_capacity(contexts.len());
121    for context in contexts {
122        if tree_pattern.matches(&context.tree) {
123            result.push(context);
124        }
125    }
126
127    result
128}
129
130/// Return tree contexts for every garden matching the specified pattern.
131/// Parameters:
132/// - config: `&garden::model::Configuration`
133/// - pattern: `&glob::Pattern`
134fn garden_trees(
135    app_context: &model::ApplicationContext,
136    config: &model::Configuration,
137    graft_config: Option<&model::Configuration>,
138    pattern: &glob::Pattern,
139) -> Vec<model::TreeContext> {
140    let mut result = Vec::new();
141    let mut garden_found = false;
142    if let Some(graft_cfg) = graft_config {
143        for (name, garden) in &graft_cfg.gardens {
144            if !pattern.matches(name) {
145                continue;
146            }
147            result.append(&mut trees_from_garden(
148                app_context,
149                config,
150                graft_config,
151                garden,
152            ));
153            garden_found = true;
154        }
155    }
156    if !garden_found {
157        for (name, garden) in &config.gardens {
158            if !pattern.matches(name) {
159                continue;
160            }
161            result.append(&mut trees_from_garden(
162                app_context,
163                config,
164                graft_config,
165                garden,
166            ));
167        }
168    }
169
170    result
171}
172
173/// Return the tree contexts for a garden
174pub fn trees_from_garden(
175    app_context: &model::ApplicationContext,
176    config: &model::Configuration,
177    graft_config: Option<&model::Configuration>,
178    garden: &model::Garden,
179) -> Vec<model::TreeContext> {
180    let mut result = Vec::new();
181
182    // Loop over the garden's groups.
183    for group in &garden.groups {
184        // Create a glob pattern for the group entry
185        let pattern = match glob::Pattern::new(group) {
186            Ok(value) => value,
187            Err(_) => continue,
188        };
189        // Loop over configured groups to find the matching groups
190        let config_groups = match graft_config {
191            Some(graft_cfg) => &graft_cfg.groups,
192            None => &config.groups,
193        };
194        for (name, cfg_group) in config_groups {
195            if !pattern.matches(name) {
196                continue;
197            }
198            // Match found -- take all of the discovered trees.
199            result.append(&mut trees_from_group(
200                app_context,
201                config,
202                graft_config,
203                Some(garden.get_name()),
204                cfg_group,
205            ));
206        }
207    }
208
209    // Collect tree contexts for each tree in this garden
210    for tree in &garden.trees {
211        result.append(&mut trees_from_pattern(
212            app_context,
213            config,
214            graft_config,
215            tree,
216            Some(garden.get_name()),
217            None,
218        ));
219    }
220
221    result
222}
223
224/// Return the tree contexts for a garden
225pub fn trees_from_group(
226    app_context: &model::ApplicationContext,
227    config: &model::Configuration,
228    graft_config: Option<&model::Configuration>,
229    garden: Option<&model::GardenName>,
230    group: &model::Group,
231) -> Vec<model::TreeContext> {
232    let mut result = Vec::new();
233    for tree in &group.members {
234        result.append(&mut trees_from_pattern(
235            app_context,
236            config,
237            graft_config,
238            tree,
239            garden,
240            Some(group.get_name()),
241        ));
242    }
243
244    result
245}
246
247/// Find a tree by name
248/// Parameters:
249/// - config: `&garden::model::Configuration`
250/// - tree: Tree name `&str`
251/// - garden_name: optional name of the garden in which to operate.
252/// - group: optional name of the group in which to operate.
253pub fn tree_from_name(
254    config: &model::Configuration,
255    tree_name: &str,
256    garden_name: Option<&model::GardenName>,
257    group: Option<&model::GroupName>,
258) -> Option<model::TreeContext> {
259    // Collect tree indexes for the configured trees
260    if let Some(tree) = config.trees.get(tree_name) {
261        return Some(model::TreeContext::new(
262            tree.get_name(),
263            config.graft_id(),
264            garden_name.cloned(),
265            group.cloned(),
266        ));
267    }
268
269    // Try to find the specified name on the filesystem if no tree was found
270    // that matched the specified name.  Matching trees are found by matching
271    // tree paths against the specified name.
272    if let Some(ctx) = tree_from_path(config, tree_name) {
273        return Some(ctx);
274    }
275
276    None
277}
278
279/// Find trees matching a pattern
280/// Parameters:
281/// - config: `&garden::model::Configuration`
282/// - tree: Tree name pattern `&str`
283/// - garden_name: `Option<garden::model::GardenName>`
284pub fn trees_from_pattern(
285    app_context: &model::ApplicationContext,
286    config: &model::Configuration,
287    graft_config: Option<&model::Configuration>,
288    tree: &str,
289    garden_name: Option<&model::GardenName>,
290    group: Option<&model::GroupName>,
291) -> Vec<model::TreeContext> {
292    if syntax::is_graft(tree) {
293        // First, try the current config.
294        if let Ok((graft_id, remainder)) = config.get_graft_id(tree) {
295            return trees_from_pattern(
296                app_context,
297                config,
298                Some(app_context.get_config(graft_id)),
299                remainder,
300                garden_name,
301                group,
302            );
303        }
304    }
305
306    // Collect tree indexes for the configured trees
307    let mut result = Vec::new();
308    let pattern = match glob::Pattern::new(tree) {
309        Ok(value) => value,
310        Err(_) => return result,
311    };
312
313    if let Some(graft_cfg) = graft_config {
314        for (tree_name, cfg_tree) in &graft_cfg.trees {
315            if pattern.matches(tree_name) {
316                // Tree found in a grafted configuration.
317                result.push(model::TreeContext::new(
318                    cfg_tree.get_name(),
319                    graft_cfg.get_id(),
320                    garden_name.cloned(),
321                    group.cloned(),
322                ));
323            }
324        }
325    }
326    for (tree_name, cfg_tree) in &config.trees {
327        if pattern.matches(tree_name) {
328            // Tree found in a grafted configuration.
329            result.push(model::TreeContext::new(
330                cfg_tree.get_name(),
331                config.get_id(),
332                garden_name.cloned(),
333                group.cloned(),
334            ));
335        }
336    }
337
338    // Try to find the specified name on the filesystem if no tree was found
339    // that matched the specified name.  Matching trees are found by matching
340    // tree paths against the specified name.
341    if result.is_empty() {
342        if let Some(ctx) = tree_from_path(config, tree) {
343            result.push(ctx);
344        }
345    }
346
347    result
348}
349
350/// Return a tree context for the specified path string.
351pub(crate) fn tree_from_path(
352    config: &model::Configuration,
353    path: &str,
354) -> Option<model::TreeContext> {
355    tree_from_pathbuf(config, &std::path::PathBuf::from(path))
356}
357
358/// Return a tree context for the specified path.
359fn tree_from_pathbuf(
360    config: &model::Configuration,
361    path: &std::path::Path,
362) -> Option<model::TreeContext> {
363    // First check whether the specified path (including ".") is a configured tree.
364    let pathbuf = match path::canonicalize(path) {
365        Ok(canon) => canon,
366        Err(_) => return None,
367    };
368    for (name, tree) in &config.trees {
369        let tree_path = match tree.path_as_ref() {
370            Ok(value) => value,
371            Err(_) => continue,
372        };
373        let tree_canon = match path::canonicalize(std::path::PathBuf::from(tree_path)) {
374            Ok(value) => value,
375            Err(_) => continue,
376        };
377        if pathbuf == tree_canon {
378            return Some(model::TreeContext::new(name, config.get_id(), None, None));
379        }
380    }
381
382    // Nothing was found. If "." was specified (or implicitly used) then the
383    // current directory is not a configured tree. Fallback to treating "." as
384    // the garden config directory to find either configured trees at the root
385    // or the implicit default tree when "trees" is omitted.
386    let is_dot = path
387        .to_str()
388        .map(|value| value == constants::DOT)
389        .unwrap_or_default();
390    if is_dot {
391        if let Some(ref dirname) = config.dirname {
392            for (name, tree) in &config.trees {
393                let tree_path = match tree.path_as_ref() {
394                    Ok(value) => value,
395                    Err(_) => continue,
396                };
397                let tree_canon = match path::canonicalize(std::path::PathBuf::from(tree_path)) {
398                    Ok(value) => value,
399                    Err(_) => continue,
400                };
401                if dirname == &tree_canon {
402                    return Some(model::TreeContext::new(name, config.get_id(), None, None));
403                }
404            }
405        }
406    }
407
408    None
409}
410
411/// Return the name of an existing tree from the specified path.
412pub fn tree_name_from_path(
413    config: &model::Configuration,
414    path: &std::path::Path,
415) -> Option<String> {
416    tree_name_from_abspath(config, &path::abspath(path))
417}
418
419/// Return the name of an existing tree from an absolute path.
420pub(crate) fn tree_name_from_abspath(
421    config: &model::Configuration,
422    path: &std::path::Path,
423) -> Option<String> {
424    // Do we already have a tree with this tree path?
425    for tree in config.trees.values() {
426        // Skip entries that do not exist on disk.
427        if !tree.path_is_valid() {
428            continue;
429        }
430        let tree_path_str = match tree.path_as_ref() {
431            Ok(path_str) => path_str,
432            Err(_) => continue,
433        };
434        // Check if this tree matches the specified path.
435        let tree_pathbuf = std::path::PathBuf::from(tree_path_str);
436        if let Ok(canon_path) = path::canonicalize(tree_pathbuf) {
437            if canon_path == path {
438                // Existing tree found: use the configured name.
439                return Some(tree.get_name().to_string());
440            }
441        }
442    }
443
444    None
445}
446
447/// Returns tree contexts matching the specified pattern
448fn trees(config: &model::Configuration, pattern: &glob::Pattern) -> Vec<model::TreeContext> {
449    let mut result = Vec::new();
450    for (tree_name, tree) in &config.trees {
451        if pattern.matches(tree_name) {
452            result.push(model::TreeContext::new(
453                tree.get_name(),
454                config.graft_id(),
455                None,
456                None,
457            ));
458        }
459    }
460
461    result
462}
463
464/// Return a Result<garden::model::TreeContext, garden::errors::GardenError>
465/// when the tree and optional garden are present.
466pub fn tree_context(
467    app_context: &model::ApplicationContext,
468    config: &model::Configuration,
469    tree: &str,
470    garden: Option<&str>,
471) -> Result<model::TreeContext, errors::GardenError> {
472    let mut ctx = tree_from_name(config, tree, None, None).ok_or_else(|| {
473        errors::GardenError::TreeNotFound {
474            tree: tree.to_string(),
475        }
476    })?;
477    // If current configuration is a graft then reset the context to the root configuration
478    // and record the graft so that later lookups use the graft's context.
479    // This is effectively the inverse of the recursive deepening performed by find_tree().
480    if config.parent_id.is_some() {
481        ctx.config = config.get_id();
482    }
483
484    if let Some(garden_name) = garden {
485        let pattern = glob::Pattern::new(garden_name).map_err(|_| {
486            errors::GardenError::GardenPatternError {
487                garden: garden_name.into(),
488            }
489        })?;
490        let contexts = garden_trees(
491            app_context,
492            app_context.get_root_config(),
493            Some(config),
494            &pattern,
495        );
496
497        if contexts.is_empty() {
498            return Err(errors::GardenError::GardenNotFound {
499                garden: garden_name.to_string(),
500            });
501        }
502
503        ctx.garden = garden.map(|value| value.to_string());
504        let mut found = false;
505        for current_ctx in &contexts {
506            if current_ctx.tree == ctx.tree {
507                found = true;
508                break;
509            }
510        }
511
512        if !found {
513            return Err(errors::GardenError::InvalidGardenArgument {
514                tree: tree.to_string(),
515                garden: garden_name.to_string(),
516            });
517        }
518    }
519
520    Ok(ctx)
521}
522
523pub fn find_tree(
524    app_context: &model::ApplicationContext,
525    id: model::ConfigId,
526    tree: &str,
527    garden: Option<&str>,
528) -> Result<model::TreeContext, errors::GardenError> {
529    {
530        let config = app_context.get_config(id);
531        if let Some(graft_name) = syntax::graft_basename(tree) {
532            if syntax::is_graft(tree) && config.contains_graft(graft_name) {
533                let graft = config.get_graft(graft_name)?;
534                let graft_id = graft
535                    .get_id()
536                    .ok_or(errors::GardenError::ConfigurationError(format!(
537                        "invalid graft: {graft_name}"
538                    )))?;
539                if let Some(next_graft) = syntax::trim_graft(tree) {
540                    return find_tree(app_context, graft_id, &next_graft, garden);
541                }
542            }
543        }
544    }
545
546    let config = app_context.get_config(id);
547    tree_context(app_context, config, tree, garden)
548}
549
550/// Return a path that that is either the tree's path or the tree's shared worktree path.
551pub(crate) fn shared_worktree_path(
552    app_context: &model::ApplicationContext,
553    config: &model::Configuration,
554    ctx: &model::TreeContext,
555) -> String {
556    let config = match ctx.config {
557        Some(config_id) => app_context.get_config(config_id),
558        None => config,
559    };
560    let tree = match config.trees.get(&ctx.tree) {
561        Some(tree) => tree,
562        None => match app_context.get_root_config().trees.get(&ctx.tree) {
563            Some(tree) => tree,
564            None => return String::new(),
565        },
566    };
567    if tree.is_worktree {
568        let worktree = eval::tree_variable(
569            app_context,
570            config,
571            None,
572            &ctx.tree,
573            ctx.garden.as_ref(),
574            &tree.worktree,
575        );
576        if let Some(parent_ctx) =
577            query::tree_from_name(config, &worktree, ctx.garden.as_ref(), ctx.group.as_ref())
578        {
579            if let Some(path) = config
580                .trees
581                .get(&parent_ctx.tree)
582                .and_then(|tree| tree.path_as_ref().ok())
583            {
584                return path.to_string();
585            }
586        }
587    }
588
589    if let Ok(path) = tree.path_as_ref() {
590        return path.to_string();
591    }
592
593    tree.get_name().to_string()
594}