swh-graph-stdlib 14.0.1

Library of algorithms and data structures for swh-graph
Documentation
// Copyright (C) 2024-2026  The Software Heritage developers
// See the AUTHORS file at the top-level directory of this distribution
// License: GNU General Public License version 3, or any later version
// See top-level LICENSE file for more information

use anyhow::Result;

use swh_graph::graph::NodeId;
use swh_graph::graph_builder::GraphBuilder;
use swh_graph::swhid;
use swh_graph_stdlib::*;

mod data;

/// Add to `builder` a linear history d(400) -> c(300) -> b(200) -> a(100), where arrows are
/// rev->rev arcs (i.e., "has parent") and numbers are committer timestamps. Return the node ids
/// as (a, b, c, d).
fn build_linear_history(builder: &mut GraphBuilder) -> Result<(NodeId, NodeId, NodeId, NodeId)> {
    let a = builder
        .node(swhid!(swh:1:rev:0000000000000000000000000000000000000010))?
        .committer_timestamp(100, 0)
        .done();
    let b = builder
        .node(swhid!(swh:1:rev:0000000000000000000000000000000000000020))?
        .committer_timestamp(200, 0)
        .done();
    let c = builder
        .node(swhid!(swh:1:rev:0000000000000000000000000000000000000030))?
        .committer_timestamp(300, 0)
        .done();
    let d = builder
        .node(swhid!(swh:1:rev:0000000000000000000000000000000000000040))?
        .committer_timestamp(400, 0)
        .done();
    builder.arc(d, c);
    builder.arc(c, b);
    builder.arc(b, a);
    Ok((a, b, c, d))
}

#[test]
fn test_find_head_rev() -> Result<()> {
    let mut builder = GraphBuilder::default();
    let snp0 = builder
        .node(swhid!(swh:1:snp:0000000000000000000000000000000000000000))?
        .done();
    let rev1 = builder
        .node(swhid!(swh:1:rev:0000000000000000000000000000000000000001))?
        .done();
    let rev2 = builder
        .node(swhid!(swh:1:rev:0000000000000000000000000000000000000002))?
        .done();
    let rev3 = builder
        .node(swhid!(swh:1:rev:0000000000000000000000000000000000000003))?
        .done();
    builder.snp_arc(snp0, rev1, "refs/heads/bug");
    builder.snp_arc(snp0, rev2, "refs/heads/main");
    builder.snp_arc(snp0, rev3, "refs/heads/new-sux");
    let graph = builder.done()?;

    assert_eq!(vcs::find_head_rev(&graph, snp0)?, Some(rev2));
    Ok(())
}

#[test]
fn test_list_root_revisions() -> Result<()> {
    // Graph structure (arrows = rev->rev arcs, ie. "has parent"):
    //   a <- b <- d
    //        c <-
    //   e <- f
    //
    // Root revisions (no parent): a, c, e
    let mut builder = GraphBuilder::default();
    let a = builder
        .node(swhid!(swh:1:rev:0000000000000000000000000000000000000010))?
        .done();
    let b = builder
        .node(swhid!(swh:1:rev:0000000000000000000000000000000000000020))?
        .done();
    let c = builder
        .node(swhid!(swh:1:rev:0000000000000000000000000000000000000030))?
        .done();
    let d = builder
        .node(swhid!(swh:1:rev:0000000000000000000000000000000000000040))?
        .done();
    let e = builder
        .node(swhid!(swh:1:rev:0000000000000000000000000000000000000050))?
        .done();
    let f = builder
        .node(swhid!(swh:1:rev:0000000000000000000000000000000000000060))?
        .done();
    // Non-rev node to check it's excluded
    let dir = builder
        .node(swhid!(swh:1:dir:0000000000000000000000000000000000000070))?
        .done();

    builder.arc(b, a);
    builder.arc(d, b);
    builder.arc(d, c);
    builder.arc(f, e);
    builder.arc(b, dir); // rev -> dir, ignored by the rev-only subgraph

    let graph = builder.done()?;
    let mut roots = vcs::list_root_revisions(&graph)?;
    roots.sort();

    assert_eq!(roots, vec![a, c, e]);

    Ok(())
}

#[test]
fn test_list_revisions_linear() -> Result<()> {
    let mut builder = GraphBuilder::default();
    let (a, b, c, d) = build_linear_history(&mut builder)?;
    let graph = builder.done()?;

    // list_revisions from d should return [d, c, b, a] (reverse chronological)
    let revs = vcs::list_revisions(&graph, d)?;
    assert_eq!(revs, vec![d, c, b, a]);

    Ok(())
}

#[test]
fn test_list_revisions_branching() -> Result<()> {
    // Branching history:
    //   a(100) <- b(200) <- d(400)
    //          \- c(300) <-/
    let mut builder = GraphBuilder::default();
    let a = builder
        .node(swhid!(swh:1:rev:0000000000000000000000000000000000000010))?
        .committer_timestamp(100, 0)
        .done();
    let b = builder
        .node(swhid!(swh:1:rev:0000000000000000000000000000000000000020))?
        .committer_timestamp(200, 0)
        .done();
    let c = builder
        .node(swhid!(swh:1:rev:0000000000000000000000000000000000000030))?
        .committer_timestamp(300, 0)
        .done();
    let d = builder
        .node(swhid!(swh:1:rev:0000000000000000000000000000000000000040))?
        .committer_timestamp(400, 0)
        .done();
    builder.arc(b, a);
    builder.arc(d, b);
    builder.arc(d, c);
    builder.arc(c, b);
    let graph = builder.done()?;

    let revs = vcs::list_revisions(&graph, d)?;
    assert_eq!(revs, vec![d, c, b, a]);

    Ok(())
}

#[test]
fn test_list_revisions_with_exclude() -> Result<()> {
    // Exclude from b: should return [d, c]
    let mut builder = GraphBuilder::default();
    let (_a, b, c, d) = build_linear_history(&mut builder)?;
    let graph = builder.done()?;

    let revs = vcs::list_revisions_with(&graph, &[d], &[b], &Default::default())?;
    assert_eq!(revs, vec![d, c]);

    Ok(())
}

#[test]
fn test_list_revisions_with_since_until() -> Result<()> {
    let mut builder = GraphBuilder::default();
    let (_a100, b200, c300, d400) = build_linear_history(&mut builder)?;
    let graph = builder.done()?;

    // since=200, until=300: d(400) > until(300) -> not listed, but traversal continues
    // through it; c(300) and b(200) pass, a(100) < since(200) -> traversal stops.
    let opts = vcs::RevListOptions {
        since: Some(200),
        until: Some(300),
        ..Default::default()
    };
    let revs = vcs::list_revisions_with(&graph, &[d400], &[], &opts)?;
    assert_eq!(revs, vec![c300, b200]);

    // Start from c instead: same result. c(300) passes, b(200) passes, a(100) < 200 -> cutoff
    let revs = vcs::list_revisions_with(&graph, &[c300], &[], &opts)?;
    assert_eq!(revs, vec![c300, b200]);

    Ok(())
}

#[test]
fn test_list_revisions_with_max_parents() -> Result<()> {
    // a(100) <- b(200) <- d(400) <- e(500)
    //           c(300) <-/
    // d has 2 parents: b, c
    // b and e have 1 parent each
    // a and c have 0 parents each
    let mut builder = GraphBuilder::default();
    let a = builder
        .node(swhid!(swh:1:rev:0000000000000000000000000000000000000010))?
        .committer_timestamp(100, 0)
        .done();
    let b = builder
        .node(swhid!(swh:1:rev:0000000000000000000000000000000000000020))?
        .committer_timestamp(200, 0)
        .done();
    let c = builder
        .node(swhid!(swh:1:rev:0000000000000000000000000000000000000030))?
        .committer_timestamp(300, 0)
        .done();
    let d = builder
        .node(swhid!(swh:1:rev:0000000000000000000000000000000000000040))?
        .committer_timestamp(400, 0)
        .done();
    let e = builder
        .node(swhid!(swh:1:rev:0000000000000000000000000000000000000050))?
        .committer_timestamp(500, 0)
        .done();
    builder.arc(b, a);
    builder.arc(d, b);
    builder.arc(d, c);
    builder.arc(e, d);
    let graph = builder.done()?;

    // max_parents=1: d (a merge) is not listed, but traversal continues through it,
    // listing all non-merge ancestors, as git-rev-list does
    let opts = vcs::RevListOptions {
        max_parents: Some(1),
        ..Default::default()
    };
    let revs = vcs::list_revisions_with(&graph, &[e], &[], &opts)?;
    assert_eq!(revs, vec![e, c, b, a]);

    // min_parents=2: only the merge commit d is listed
    let opts = vcs::RevListOptions {
        min_parents: Some(2),
        ..Default::default()
    };
    let revs = vcs::list_revisions_with(&graph, &[e], &[], &opts)?;
    assert_eq!(revs, vec![d]);

    // max_parents=0: only the root commits a and c are listed
    let opts = vcs::RevListOptions {
        max_parents: Some(0),
        ..Default::default()
    };
    let revs = vcs::list_revisions_with(&graph, &[e], &[], &opts)?;
    assert_eq!(revs, vec![c, a]);

    Ok(())
}

#[test]
fn test_list_revisions_release_peeling() -> Result<()> {
    // rel -> rev_b(200) -> rev_a(100)
    let mut builder = GraphBuilder::default();
    let rev_a = builder
        .node(swhid!(swh:1:rev:0000000000000000000000000000000000000010))?
        .committer_timestamp(100, 0)
        .done();
    let rev_b = builder
        .node(swhid!(swh:1:rev:0000000000000000000000000000000000000020))?
        .committer_timestamp(200, 0)
        .done();
    let rel = builder
        .node(swhid!(swh:1:rel:0000000000000000000000000000000000000030))?
        .done();
    builder.arc(rev_b, rev_a);
    builder.arc(rel, rev_b);
    let graph = builder.done()?;

    let revs = vcs::list_revisions(&graph, rel)?;
    assert_eq!(revs, vec![rev_b, rev_a]);

    Ok(())
}

#[test]
fn test_list_revisions_multiple_starts() -> Result<()> {
    // Two disconnected chains:
    //   b(200) -> a(100)
    //   d(400) -> c(300)
    let mut builder = GraphBuilder::default();
    let a = builder
        .node(swhid!(swh:1:rev:0000000000000000000000000000000000000010))?
        .committer_timestamp(100, 0)
        .done();
    let b = builder
        .node(swhid!(swh:1:rev:0000000000000000000000000000000000000020))?
        .committer_timestamp(200, 0)
        .done();
    let c = builder
        .node(swhid!(swh:1:rev:0000000000000000000000000000000000000030))?
        .committer_timestamp(300, 0)
        .done();
    let d = builder
        .node(swhid!(swh:1:rev:0000000000000000000000000000000000000040))?
        .committer_timestamp(400, 0)
        .done();
    builder.arc(b, a);
    builder.arc(d, c);
    let graph = builder.done()?;

    let revs = vcs::list_revisions_with(&graph, &[b, d], &[], &Default::default())?;
    assert_eq!(revs, vec![d, c, b, a]);

    Ok(())
}

#[test]
fn test_list_revisions_unpeelable_start() -> Result<()> {
    // A directory node cannot be peeled to a revision: expect an error
    let mut builder = GraphBuilder::default();
    let dir = builder
        .node(swhid!(swh:1:dir:0000000000000000000000000000000000000010))?
        .done();
    let cnt = builder
        .node(swhid!(swh:1:cnt:0000000000000000000000000000000000000020))?
        .done();
    builder.dir_arc(dir, cnt, swh_graph::labels::Permission::Content, "README");
    let graph = builder.done()?;

    assert!(vcs::list_revisions(&graph, dir).is_err());

    Ok(())
}

#[test]
fn test_list_revisions_exclude_release() -> Result<()> {
    // Linear history with rel -> b on top.
    // Excluding rel should peel it to b, hence return [d, c]
    let mut builder = GraphBuilder::default();
    let (_a, b, c, d) = build_linear_history(&mut builder)?;
    let rel = builder
        .node(swhid!(swh:1:rel:0000000000000000000000000000000000000050))?
        .done();
    builder.arc(rel, b);
    let graph = builder.done()?;

    let revs = vcs::list_revisions_with(&graph, &[d], &[rel], &Default::default())?;
    assert_eq!(revs, vec![d, c]);

    Ok(())
}

#[test]
fn test_list_revisions_duplicate_starts() -> Result<()> {
    // Passing the same start twice should not duplicate results
    let mut builder = GraphBuilder::default();
    let (a, b, c, _d) = build_linear_history(&mut builder)?;
    let graph = builder.done()?;

    let revs = vcs::list_revisions_with(&graph, &[c, c, b], &[], &Default::default())?;
    assert_eq!(revs, vec![c, b, a]);

    Ok(())
}