Skip to main content

backhopper_core/compat/
target_tree_index.rs

1// Copyright (C) 2026 Michael S. Klishin and Contributors
2// SPDX-License-Identifier: Apache-2.0 OR MIT
3// See LICENSE-APACHE and LICENSE-MIT for details.
4
5//! Built-once-per-invocation index of every blob path in a target
6//! tree. Backs the `--target-repo-dir-path` cross-branch backport
7//! analyser. Pure data: construction from a live repository lives in
8//! `backhopper-git`.
9
10use std::collections::{BTreeMap, BTreeSet};
11use std::path::{Path, PathBuf};
12use std::str::FromStr;
13
14use crate::model::names::{CommitSha, GitRef, ModuleName};
15
16#[derive(Debug, Clone)]
17pub struct TargetTreeIndex {
18    target_repo: PathBuf,
19    target_ref: GitRef,
20    resolved_commit: CommitSha,
21    present_paths: BTreeSet<PathBuf>,
22    erl_modules: BTreeMap<ModuleName, PathBuf>,
23}
24
25impl TargetTreeIndex {
26    #[must_use]
27    pub fn from_parts(
28        target_repo: PathBuf,
29        target_ref: GitRef,
30        resolved_commit: CommitSha,
31        present_paths: BTreeSet<PathBuf>,
32    ) -> Self {
33        // Erlang fixes module name to file basename. A basename seen
34        // twice is a clash Erlang itself rejects, so record neither.
35        let mut by_module: BTreeMap<ModuleName, Option<PathBuf>> = BTreeMap::new();
36        for path in &present_paths {
37            if path.extension().and_then(|e| e.to_str()) == Some("erl")
38                && let Some(stem) = path.file_stem().and_then(|s| s.to_str())
39                && let Ok(module) = ModuleName::from_str(stem)
40            {
41                by_module
42                    .entry(module)
43                    .and_modify(|slot| *slot = None)
44                    .or_insert_with(|| Some(path.clone()));
45            }
46        }
47        let erl_modules = by_module
48            .into_iter()
49            .filter_map(|(m, p)| p.map(|p| (m, p)))
50            .collect();
51        Self {
52            target_repo,
53            target_ref,
54            resolved_commit,
55            present_paths,
56            erl_modules,
57        }
58    }
59
60    pub fn target_repo(&self) -> &Path {
61        &self.target_repo
62    }
63
64    pub fn target_ref(&self) -> &GitRef {
65        &self.target_ref
66    }
67
68    pub fn resolved_commit(&self) -> &CommitSha {
69        &self.resolved_commit
70    }
71
72    pub fn contains_path(&self, p: &Path) -> bool {
73        self.present_paths.contains(p)
74    }
75
76    pub fn present_paths(&self) -> &BTreeSet<PathBuf> {
77        &self.present_paths
78    }
79
80    /// The `.erl` file that defines `module`, by basename, or `None`
81    /// when the tree has no such file or two files share the basename.
82    pub fn module_erl_path(&self, module: &ModuleName) -> Option<&Path> {
83        self.erl_modules.get(module).map(PathBuf::as_path)
84    }
85}