weavatrix_rust/engine/
repository_state.rs1use super::RepositoryState;
2use crate::analyzer::Analyzer;
3use crate::model::{Error, Result, Snapshot};
4use std::path::Path;
5use std::sync::{Arc, OnceLock};
6use std::time::{Duration, Instant};
7use weavatrix_graph::{EdgeKind, Graph, Node, NodeIndex, NodeKind};
8use weavatrix_scan::ScanReport;
9
10impl RepositoryState {
11 pub(crate) fn build(analyzer: &Analyzer, root: impl AsRef<Path>) -> Result<Self> {
12 let started = Instant::now();
13 let (snapshot, scan) = analyzer.analyze_with_report(root)?;
14 let graph = Graph::try_from_sorted_parts(snapshot.nodes.clone(), snapshot.edges.clone())?;
15 let snapshot_root = std::path::PathBuf::from(&snapshot.repository);
16 let root = snapshot_root
17 .canonicalize()
18 .map_err(|source| Error::io(&snapshot_root, source))?;
19 Ok(Self {
20 root,
21 snapshot,
22 graph: Arc::new(graph),
23 scan,
24 build_time: started.elapsed(),
25 built_at: Instant::now(),
26 weak_components: Arc::new(OnceLock::new()),
27 })
28 }
29
30 pub(super) fn from_scan(analyzer: &Analyzer, root: &Path, scan: ScanReport) -> Result<Self> {
31 let started = Instant::now();
32 let snapshot = analyzer.analyze_report(root, &scan)?;
33 let graph = Graph::try_from_sorted_parts(snapshot.nodes.clone(), snapshot.edges.clone())?;
34 Ok(Self {
35 root: root.to_path_buf(),
36 snapshot,
37 graph: Arc::new(graph),
38 scan,
39 build_time: started.elapsed(),
40 built_at: Instant::now(),
41 weak_components: Arc::new(OnceLock::new()),
42 })
43 }
44
45 #[must_use]
47 pub fn graph_age_seconds(&self) -> u64 {
48 self.built_at.elapsed().as_secs()
49 }
50
51 #[must_use]
52 pub fn root(&self) -> &Path {
53 &self.root
54 }
55
56 #[must_use]
57 pub const fn snapshot(&self) -> &Snapshot {
58 &self.snapshot
59 }
60
61 #[must_use]
62 pub fn graph(&self) -> &Graph {
63 self.graph.as_ref()
64 }
65
66 #[must_use]
67 pub const fn build_time(&self) -> Duration {
68 self.build_time
69 }
70
71 #[must_use]
72 pub const fn scan_report(&self) -> &ScanReport {
73 &self.scan
74 }
75
76 pub(crate) fn coupled_components(&self) -> &[Vec<NodeIndex>] {
77 self.weak_components
78 .get_or_init(|| coupled_components(self.graph.as_ref()))
79 .as_slice()
80 }
81
82 pub fn warm_communities(&self) {
84 if self.weak_components.get().is_some() {
85 return;
86 }
87 let graph = Arc::clone(&self.graph);
88 let destination = Arc::clone(&self.weak_components);
89 std::thread::spawn(move || {
90 std::thread::sleep(Duration::from_millis(10));
91 destination.get_or_init(|| coupled_components(graph.as_ref()));
92 });
93 }
94
95 pub(crate) fn resolve_node(&self, label: &str) -> std::result::Result<NodeIndex, String> {
96 if let Some(index) = self.graph.node_index(label) {
97 return Ok(index);
98 }
99 let matches = self
100 .graph
101 .nodes()
102 .iter()
103 .enumerate()
104 .filter(|(_, node)| node.label == label)
105 .collect::<Vec<_>>();
106 let candidates = prefer_non_test_only(&matches);
107 match candidates.as_slice() {
108 [] => Err(format!("node not found: {label}")),
109 [(index, _)] => Ok(NodeIndex::new(
110 u32::try_from(*index).map_err(|_| "node index overflow")?,
111 )),
112 _ => Err(format!(
113 "ambiguous node label {label:?}; use one of: {}",
114 candidates
115 .iter()
116 .take(8)
117 .map(|(_, node)| node.id.as_str())
118 .collect::<Vec<_>>()
119 .join(", ")
120 )),
121 }
122 }
123
124 pub(crate) fn node(&self, index: NodeIndex) -> std::result::Result<&Node, String> {
125 self.graph
126 .node_at(index)
127 .ok_or_else(|| format!("node index out of range: {}", index.index()))
128 }
129}
130
131fn prefer_non_test_only<'a>(matches: &'a [(usize, &'a Node)]) -> Vec<(usize, &'a Node)> {
132 if matches.len() <= 1 {
133 return matches.to_vec();
134 }
135 let production = matches
136 .iter()
137 .copied()
138 .filter(|(_, node)| {
139 !matches!(
140 node.attributes.get("test_only"),
141 Some(weavatrix_graph::AttributeValue::Bool(true))
142 )
143 })
144 .collect::<Vec<_>>();
145 if production.len() == 1 {
146 production
147 } else if production.is_empty() {
148 matches.to_vec()
149 } else {
150 production
151 }
152}
153
154#[must_use]
158pub fn git_head(root: &Path) -> Option<String> {
159 let mut git_dir = root.join(".git");
160 if git_dir.is_file() {
161 let text = std::fs::read_to_string(&git_dir).ok()?;
162 let relative = text.strip_prefix("gitdir:")?.trim();
163 let candidate = Path::new(relative);
164 git_dir = if candidate.is_absolute() {
165 candidate.to_path_buf()
166 } else {
167 root.join(candidate)
168 };
169 }
170 let head = std::fs::read_to_string(git_dir.join("HEAD")).ok()?;
171 let head = head.trim();
172 let Some(reference) = head.strip_prefix("ref:") else {
173 return Some(head.to_owned());
174 };
175 let reference = reference.trim();
176 let common = std::fs::read_to_string(git_dir.join("commondir"))
177 .map_or_else(|_| git_dir.clone(), |dir| git_dir.join(dir.trim()));
178 for base in [&git_dir, &common] {
179 if let Ok(hash) = std::fs::read_to_string(base.join(reference)) {
180 return Some(hash.trim().to_owned());
181 }
182 }
183 let packed = std::fs::read_to_string(common.join("packed-refs")).ok()?;
184 packed
185 .lines()
186 .filter_map(|line| line.split_once(' '))
187 .find(|(_, name)| *name == reference)
188 .map(|(hash, _)| hash.trim().to_owned())
189}
190
191fn coupled_components(graph: &Graph) -> Vec<Vec<NodeIndex>> {
196 fn find(parent: &mut [usize], node: usize) -> usize {
197 let mut root = node;
198 while parent[root] != root {
199 root = parent[root];
200 }
201 let mut current = node;
202 while parent[current] != root {
203 let next = parent[current];
204 parent[current] = root;
205 current = next;
206 }
207 root
208 }
209 let nodes = graph.nodes();
210 let mut parent = (0..nodes.len()).collect::<Vec<_>>();
211 let is_package = |slot: usize| {
212 nodes
213 .get(slot)
214 .is_some_and(|node| node.kind == NodeKind::Package)
215 };
216 for slot in 0..nodes.len() {
217 let index = NodeIndex::new(u32::try_from(slot).unwrap_or(u32::MAX));
218 for edge in graph.outgoing_at(index) {
219 if matches!(edge.kind, EdgeKind::Contains | EdgeKind::Method) {
220 continue;
221 }
222 let Some(target) = graph.node_index(edge.target.as_str()) else {
223 continue;
224 };
225 if is_package(slot) || is_package(target.index()) {
226 continue;
227 }
228 let left = find(&mut parent, slot);
229 let right = find(&mut parent, target.index());
230 if left != right {
231 parent[left.max(right)] = left.min(right);
232 }
233 }
234 }
235 let mut groups = std::collections::BTreeMap::<usize, Vec<NodeIndex>>::new();
236 for slot in 0..nodes.len() {
237 let root = find(&mut parent, slot);
238 groups
239 .entry(root)
240 .or_default()
241 .push(NodeIndex::new(u32::try_from(slot).unwrap_or(u32::MAX)));
242 }
243 let mut components = groups
244 .into_values()
245 .filter(|members| members.len() > 1)
246 .collect::<Vec<_>>();
247 components.sort_by_key(|members| {
248 (
249 std::cmp::Reverse(members.len()),
250 members.first().map_or(0, |index| index.index()),
251 )
252 });
253 components
254}