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