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 match matches.as_slice() {
107 [] => Err(format!("node not found: {label}")),
108 [(index, _)] => Ok(NodeIndex::new(
109 u32::try_from(*index).map_err(|_| "node index overflow")?,
110 )),
111 _ => Err(format!(
112 "ambiguous node label {label:?}; use one of: {}",
113 matches
114 .iter()
115 .take(8)
116 .map(|(_, node)| node.id.as_str())
117 .collect::<Vec<_>>()
118 .join(", ")
119 )),
120 }
121 }
122
123 pub(crate) fn node(&self, index: NodeIndex) -> std::result::Result<&Node, String> {
124 self.graph
125 .node_at(index)
126 .ok_or_else(|| format!("node index out of range: {}", index.index()))
127 }
128}
129
130#[must_use]
134pub fn git_head(root: &Path) -> Option<String> {
135 let mut git_dir = root.join(".git");
136 if git_dir.is_file() {
137 let text = std::fs::read_to_string(&git_dir).ok()?;
138 let relative = text.strip_prefix("gitdir:")?.trim();
139 let candidate = Path::new(relative);
140 git_dir = if candidate.is_absolute() {
141 candidate.to_path_buf()
142 } else {
143 root.join(candidate)
144 };
145 }
146 let head = std::fs::read_to_string(git_dir.join("HEAD")).ok()?;
147 let head = head.trim();
148 let Some(reference) = head.strip_prefix("ref:") else {
149 return Some(head.to_owned());
150 };
151 let reference = reference.trim();
152 let common = std::fs::read_to_string(git_dir.join("commondir"))
153 .map_or_else(|_| git_dir.clone(), |dir| git_dir.join(dir.trim()));
154 for base in [&git_dir, &common] {
155 if let Ok(hash) = std::fs::read_to_string(base.join(reference)) {
156 return Some(hash.trim().to_owned());
157 }
158 }
159 let packed = std::fs::read_to_string(common.join("packed-refs")).ok()?;
160 packed
161 .lines()
162 .filter_map(|line| line.split_once(' '))
163 .find(|(_, name)| *name == reference)
164 .map(|(hash, _)| hash.trim().to_owned())
165}
166
167fn coupled_components(graph: &Graph) -> Vec<Vec<NodeIndex>> {
172 fn find(parent: &mut [usize], node: usize) -> usize {
173 let mut root = node;
174 while parent[root] != root {
175 root = parent[root];
176 }
177 let mut current = node;
178 while parent[current] != root {
179 let next = parent[current];
180 parent[current] = root;
181 current = next;
182 }
183 root
184 }
185 let nodes = graph.nodes();
186 let mut parent = (0..nodes.len()).collect::<Vec<_>>();
187 let is_package = |slot: usize| {
188 nodes
189 .get(slot)
190 .is_some_and(|node| node.kind == NodeKind::Package)
191 };
192 for slot in 0..nodes.len() {
193 let index = NodeIndex::new(u32::try_from(slot).unwrap_or(u32::MAX));
194 for edge in graph.outgoing_at(index) {
195 if matches!(edge.kind, EdgeKind::Contains | EdgeKind::Method) {
196 continue;
197 }
198 let Some(target) = graph.node_index(edge.target.as_str()) else {
199 continue;
200 };
201 if is_package(slot) || is_package(target.index()) {
202 continue;
203 }
204 let left = find(&mut parent, slot);
205 let right = find(&mut parent, target.index());
206 if left != right {
207 parent[left.max(right)] = left.min(right);
208 }
209 }
210 }
211 let mut groups = std::collections::BTreeMap::<usize, Vec<NodeIndex>>::new();
212 for slot in 0..nodes.len() {
213 let root = find(&mut parent, slot);
214 groups
215 .entry(root)
216 .or_default()
217 .push(NodeIndex::new(u32::try_from(slot).unwrap_or(u32::MAX)));
218 }
219 let mut components = groups
220 .into_values()
221 .filter(|members| members.len() > 1)
222 .collect::<Vec<_>>();
223 components.sort_by_key(|members| {
224 (
225 std::cmp::Reverse(members.len()),
226 members.first().map_or(0, |index| index.index()),
227 )
228 });
229 components
230}