Skip to main content

swh_graph_stdlib/
lib.rs

1// Copyright (C) 2024-2026  The Software Heritage developers
2// See the AUTHORS file at the top-level directory of this distribution
3// License: GNU General Public License version 3, or any later version
4// See top-level LICENSE file for more information
5
6//! Standard library to work on Software Heritage compressed graph in Rust
7
8use anyhow::{Result, bail, ensure};
9
10use swh_graph::graph::*;
11use swh_graph::labels::{EdgeLabel, VisitStatus};
12use swh_graph::{NodeConstraint, NodeType, properties};
13
14/// Given a graph and an origin node in it, return the node id and timestamp
15/// (as a number of seconds since Epoch) of the most recent snapshot of that
16/// origin, if it exists.
17///
18/// Note: only visit with status `Full` are considered when selecting
19/// snapshots.
20pub fn find_latest_snp<G>(graph: &G, ori: NodeId) -> Result<Option<(NodeId, u64)>>
21where
22    G: SwhLabeledForwardGraph + SwhGraphWithProperties,
23    <G as SwhGraphWithProperties>::Maps: properties::Maps,
24{
25    let props = graph.properties();
26    let node_type = props.node_type(ori);
27    ensure!(
28        node_type == NodeType::Origin,
29        "Type of {ori} should be ori, but is {node_type} instead"
30    );
31    // Most recent snapshot thus far, as an optional (node_id, timestamp) pair
32    let mut latest_snp: Option<(usize, u64)> = None;
33    for (succ, labels) in graph.labeled_successors(ori) {
34        let node_type = props.node_type(succ);
35        if node_type != NodeType::Snapshot {
36            continue;
37        }
38        for label in labels {
39            if let EdgeLabel::Visit(visit) = label {
40                if visit.status() != VisitStatus::Full {
41                    continue;
42                }
43                let ts = visit.timestamp();
44                if let Some((_cur_snp, cur_ts)) = latest_snp {
45                    if ts > cur_ts {
46                        latest_snp = Some((succ, ts))
47                    }
48                } else {
49                    latest_snp = Some((succ, ts))
50                }
51            }
52        }
53    }
54    Ok(latest_snp)
55}
56
57/// Peel a node once, following the natural SWH object model.
58///
59/// The peeling rules are:
60///
61/// - Origin → first snapshot successor
62///
63/// - Snapshot → HEAD revision (as per [vcs::find_head_rev]) if it exists, first revision or release
64///   successor if not
65///
66/// - Release → follows release target (first successor of any kind)
67///
68/// - Revision → follows root directory successor
69///
70/// - Directory and Content cannot be peeled further (error)
71///
72/// Returns `Ok(Some(node_id))` on success, `Ok(None)` if no suitable successor exists.
73pub fn peel_once<G>(graph: &G, node: NodeId) -> Result<Option<NodeId>>
74where
75    G: SwhLabeledForwardGraph + SwhGraphWithProperties,
76    <G as SwhGraphWithProperties>::LabelNames: properties::LabelNames,
77    <G as SwhGraphWithProperties>::Maps: properties::Maps,
78{
79    ensure!(!graph.is_transposed(), "Cannot peel on a transposed graph.");
80    let props = graph.properties();
81    let source_type = props.node_type(node);
82
83    match source_type {
84        NodeType::Content | NodeType::Directory => {
85            bail!("Cannot peel nodes of type {source_type}");
86        }
87        NodeType::Revision => {
88            for succ in graph.successors(node) {
89                if props.node_type(succ) == NodeType::Directory {
90                    return Ok(Some(succ));
91                }
92            }
93            Ok(None)
94        }
95        NodeType::Release => {
96            if let Some(succ) = graph.successors(node).into_iter().next() {
97                return Ok(Some(succ));
98            }
99            Ok(None)
100        }
101        NodeType::Snapshot => {
102            if let Ok(Some(succ)) = vcs::find_head_rev(&graph, node) {
103                Ok(Some(succ))
104            } else {
105                for succ in graph.successors(node) {
106                    let t = props.node_type(succ);
107                    if t == NodeType::Revision || t == NodeType::Release {
108                        return Ok(Some(succ));
109                    }
110                }
111                Ok(None)
112            }
113        }
114        NodeType::Origin => {
115            find_latest_snp(&graph, node).map(|opt_node| opt_node.map(|(node, _ts)| node))
116        }
117    }
118}
119
120/// Recursively peel a node until a node of a different type is reached.
121///
122/// This is analogous to libgit2's
123/// [`git_object_peel`](https://libgit2.org/docs/reference/main/object/git_object_peel.html)
124/// with `GIT_OBJECT_ANY`.
125///
126/// See [`peel_once`] for peeling rules.
127///
128/// Returns `Ok(Some(node_id))` on success, where `node_id` is the peel result, `Ok(None)` if no
129/// suitable successor exists (e.g., a root revision with no directory successor).
130pub fn peel<G>(graph: &G, node: NodeId) -> Result<Option<NodeId>>
131where
132    G: SwhLabeledForwardGraph + SwhGraphWithProperties,
133    <G as SwhGraphWithProperties>::LabelNames: properties::LabelNames,
134    <G as SwhGraphWithProperties>::Maps: properties::Maps,
135{
136    let props = graph.properties();
137    let source_type = props.node_type(node);
138
139    let mut current = node;
140    while props.node_type(current) == source_type {
141        match peel_once(graph, current)? {
142            Some(next) => current = next,
143            None => return Ok(None),
144        }
145    }
146
147    Ok(Some(current))
148}
149
150/// Recursively peel a node until a node with a type matching the specified constraint is reached.
151///
152/// This is analogous to libgit2's
153/// [`git_object_peel`](https://libgit2.org/docs/reference/main/object/git_object_peel.html).
154///
155/// See [`peel_once`] for peeling rules.
156///
157/// See [`peel`] to peel to any type that is different from `node`'s
158/// (analogous to `GIT_OBJECT_ANY`)
159///
160/// Returns `Ok(Some(node_id))` on success, where `node_id` is the peel result, `Ok(None)` if no
161/// suitable successor exists (e.g., a root revision with no directory successor).
162pub fn peel_until<G>(graph: &G, node: NodeId, target_type: NodeConstraint) -> Result<Option<NodeId>>
163where
164    G: SwhLabeledForwardGraph + SwhGraphWithProperties,
165    <G as SwhGraphWithProperties>::LabelNames: properties::LabelNames,
166    <G as SwhGraphWithProperties>::Maps: properties::Maps,
167{
168    let props = graph.properties();
169    let source_type = props.node_type(node);
170
171    // If we already are the target type, return immediately
172    if target_type.matches(source_type) {
173        return Ok(Some(node));
174    }
175
176    // Peel once
177    let Some(mut current) = peel_once(graph, node)? else {
178        return Ok(None);
179    };
180
181    loop {
182        let current_type = props.node_type(current);
183        if target_type.matches(current_type) {
184            return Ok(Some(current));
185        }
186        if current_type == NodeType::Release || current_type == NodeType::Revision {
187            // Continue peeling through releases or revision
188            match peel_once(graph, current)? {
189                Some(next) => current = next,
190                None => return Ok(None),
191            }
192        } else {
193            bail!("Cannot peel to {target_type}: stuck at node {current} of type {current_type}");
194        }
195    }
196}
197
198pub mod collections;
199pub mod connectivity;
200pub mod diff;
201pub mod fs;
202pub mod io;
203pub mod labeling;
204pub mod origins;
205#[cfg(feature = "unstable_project_ids")]
206pub mod project_ids;
207mod root_directory;
208pub use root_directory::find_root_dir;
209pub mod vcs;
210mod visit;
211pub use visit::*;
212
213pub use deprecated::*;
214
215#[allow(deprecated)]
216mod deprecated {
217    use std::collections::HashMap;
218    use swh_graph::labels::{LabelNameId, Permission};
219
220    use super::*;
221
222    #[doc(hidden)]
223    #[deprecated(since = "10.3.0", note = "use swh_graph_stdlib::vcs::HEAD_REF_NAMES")]
224    pub const HEAD_REF_NAMES: [&str; 2] = vcs::HEAD_REF_NAMES;
225
226    #[doc(hidden)]
227    #[deprecated(since = "10.3.0", note = "use swh_graph_stdlib::vcs::find_head_rev")]
228    pub fn find_head_rev<G>(graph: &G, snp: NodeId) -> Result<Option<NodeId>>
229    where
230        G: SwhLabeledForwardGraph + SwhGraphWithProperties,
231        <G as SwhGraphWithProperties>::LabelNames: properties::LabelNames,
232        <G as SwhGraphWithProperties>::Maps: properties::Maps,
233    {
234        vcs::find_head_rev(graph, snp)
235    }
236
237    #[doc(hidden)]
238    #[deprecated(
239        since = "10.3.0",
240        note = "use swh_graph_stdlib::vcs::find_head_rev_by_refs"
241    )]
242    pub fn find_head_rev_by_refs<G>(
243        graph: &G,
244        snp: NodeId,
245        ref_name_ids: &[LabelNameId],
246    ) -> Result<Option<NodeId>>
247    where
248        G: SwhLabeledForwardGraph + SwhGraphWithProperties,
249        <G as SwhGraphWithProperties>::Maps: properties::Maps,
250        <G as SwhGraphWithProperties>::LabelNames: properties::LabelNames,
251    {
252        vcs::find_head_rev_by_refs(graph, snp, ref_name_ids)
253    }
254
255    #[doc(hidden)]
256    #[deprecated(since = "10.3.0", note = "use swh_graph_stdlib::fs::FsTree")]
257    #[derive(Debug, Default, PartialEq)]
258    pub enum FsTree {
259        #[default]
260        Content,
261        Directory(HashMap<Vec<u8>, (FsTree, Option<Permission>)>),
262        Revision(NodeId),
263    }
264
265    impl From<fs::FsTree> for FsTree {
266        fn from(tree: fs::FsTree) -> Self {
267            match tree {
268                fs::FsTree::Content => FsTree::Content,
269                fs::FsTree::Directory(entries) => FsTree::Directory(
270                    entries
271                        .into_iter()
272                        .map(|(name, (tree, perm))| (name, (tree.into(), perm)))
273                        .collect(),
274                ),
275                fs::FsTree::Revision(rev) => FsTree::Revision(rev),
276            }
277        }
278    }
279
280    #[doc(hidden)]
281    #[deprecated(since = "10.3.0", note = "use swh_graph_stdlib::fs::resolve_name")]
282    pub fn fs_resolve_name<G>(
283        graph: &G,
284        dir: NodeId,
285        name: impl AsRef<[u8]>,
286    ) -> Result<Option<NodeId>>
287    where
288        G: SwhLabeledForwardGraph + SwhGraphWithProperties,
289        <G as SwhGraphWithProperties>::LabelNames: properties::LabelNames,
290        <G as SwhGraphWithProperties>::Maps: properties::Maps,
291    {
292        fs::resolve_name(graph, dir, name)
293    }
294
295    pub fn fs_resolve_name_by_id<G>(
296        graph: &G,
297        dir: NodeId,
298        name: LabelNameId,
299    ) -> Result<Option<NodeId>>
300    where
301        G: SwhLabeledForwardGraph + SwhGraphWithProperties,
302        <G as SwhGraphWithProperties>::Maps: properties::Maps,
303    {
304        fs::resolve_name_by_id(graph, dir, name)
305    }
306
307    #[doc(hidden)]
308    #[deprecated(since = "10.3.0", note = "use swh_graph_stdlib::fs::resolve_path")]
309    pub fn fs_resolve_path<G>(
310        graph: &G,
311        dir: NodeId,
312        path: impl AsRef<[u8]>,
313    ) -> Result<Option<NodeId>>
314    where
315        G: SwhLabeledForwardGraph + SwhGraphWithProperties,
316        <G as SwhGraphWithProperties>::LabelNames: properties::LabelNames,
317        <G as SwhGraphWithProperties>::Maps: properties::Maps,
318    {
319        fs::resolve_path(graph, dir, path)
320    }
321
322    #[doc(hidden)]
323    #[deprecated(
324        since = "10.3.0",
325        note = "use swh_graph_stdlib::fs::resolve_path_by_id"
326    )]
327    pub fn fs_resolve_path_by_id<G>(
328        graph: &G,
329        dir: NodeId,
330        path: &[LabelNameId],
331    ) -> Result<Option<NodeId>>
332    where
333        G: SwhLabeledForwardGraph + SwhGraphWithProperties,
334        <G as SwhGraphWithProperties>::Maps: properties::Maps,
335    {
336        fs::resolve_path_by_id(graph, dir, path)
337    }
338
339    #[doc(hidden)]
340    #[deprecated(since = "10.3.0", note = "use swh_graph_stdlib::fs::ls_tree")]
341    pub fn fs_ls_tree<G>(graph: &G, dir: NodeId) -> Result<FsTree>
342    where
343        G: SwhLabeledForwardGraph + SwhGraphWithProperties,
344        <G as SwhGraphWithProperties>::LabelNames: properties::LabelNames,
345        <G as SwhGraphWithProperties>::Maps: properties::Maps,
346    {
347        fs::ls_tree(graph, dir).map(Into::into)
348    }
349}