Skip to main content

swh_graph_stdlib/
vcs.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//! Version control system (VCS) functions.
7
8use anyhow::{Result, ensure};
9use dsi_progress_logger::{ProgressLog, concurrent_progress_logger};
10use rayon::prelude::*;
11
12use swh_graph::NodeType;
13use swh_graph::graph::*;
14use swh_graph::labels::{EdgeLabel, LabelNameId};
15use swh_graph::properties;
16use swh_graph::views::Subgraph;
17
18/// Names of references ("branches") that are considered to be pointing to the
19/// HEAD revision in a VCS, by [find_head_rev] below. Names are tried in order,
20/// when attempting to identify the HEAD revision.
21pub const HEAD_REF_NAMES: [&str; 2] = ["refs/heads/main", "refs/heads/master"];
22
23/// Given a graph and a snapshot node in it, return the node id of the revision
24/// pointed by the HEAD branch, it it exists.
25pub fn find_head_rev<G>(graph: &G, snp: NodeId) -> Result<Option<NodeId>>
26where
27    G: SwhLabeledForwardGraph + SwhGraphWithProperties,
28    <G as SwhGraphWithProperties>::LabelNames: properties::LabelNames,
29    <G as SwhGraphWithProperties>::Maps: properties::Maps,
30{
31    let props = graph.properties();
32    let head_ref_name_ids: Vec<LabelNameId> = HEAD_REF_NAMES
33        .into_iter()
34        .filter_map(|name| props.label_name_id(name.as_bytes()).ok())
35        // Note, we ignore errors on purpose here, because some ref names that
36        // do exist on the SWH graph might be missing in user-provided graphs,
37        // and will fail [label_name_id] call.
38        .collect();
39    find_head_rev_by_refs(graph, snp, &head_ref_name_ids)
40}
41
42/// Same as [find_head_rev], but with the ability to configure which branch
43/// names correspond to the HEAD revision.
44///
45/// Note: this function is also more efficient than [find_head_rev], because
46/// branch names are pre-resolved to integers once (before calling this
47/// function) and compared as integers.  See the source code of [find_head_rev]
48/// for an example of how to translate a given set of branch names (like
49/// [HEAD_REF_NAMES]) to label-name IDs for this function.
50pub fn find_head_rev_by_refs<G>(
51    graph: &G,
52    snp: NodeId,
53    ref_name_ids: &[LabelNameId],
54) -> Result<Option<NodeId>>
55where
56    G: SwhLabeledForwardGraph + SwhGraphWithProperties,
57    <G as SwhGraphWithProperties>::Maps: properties::Maps,
58    <G as SwhGraphWithProperties>::LabelNames: properties::LabelNames,
59{
60    let props = graph.properties();
61    let node_type = props.node_type(snp);
62    ensure!(
63        node_type == NodeType::Snapshot,
64        "Type of {snp} should be snp, but is {node_type} instead"
65    );
66    for (succ, labels) in graph.labeled_successors(snp) {
67        let node_type = props.node_type(succ);
68        if node_type != NodeType::Revision && node_type != NodeType::Release {
69            continue;
70        }
71        for label in labels {
72            #[allow(clippy::collapsible_if)]
73            if let EdgeLabel::Branch(branch) = label {
74                if ref_name_ids.contains(&branch.label_name_id()) {
75                    return Ok(Some(succ));
76                }
77            }
78        }
79    }
80    Ok(None)
81}
82
83/// Returns all revisions that have no parent revision.
84pub fn list_root_revisions<G>(graph: &G) -> Result<Vec<NodeId>>
85where
86    G: SwhForwardGraph + SwhGraphWithProperties<Maps: properties::Maps> + Sync,
87{
88    let graph = Subgraph::with_node_constraint(graph, "rev".parse().unwrap());
89
90    let mut pl = concurrent_progress_logger!(
91        item_name = "rev",
92        display_memory = false,
93        expected_updates = graph.actual_num_nodes().ok(),
94    );
95    pl.start("Listing initial revs...");
96    Ok(graph
97        .par_iter_nodes(pl)
98        .filter(|&node| graph.successors(node).next().is_none())
99        .collect())
100}