heddle-objects 0.15.0

An AI-native version control system
Documentation
// SPDX-License-Identifier: Apache-2.0
use std::path::Path;

use crate::blame::{BlameSliceLimits, blame_file};

use super::fixture::{principals_at, put_state_with_file, store};

#[test]
fn linear_history_credits_introducing_authors() {
    let store = store();
    let s1 = put_state_with_file(&store, "lib.rs", b"a\n", Vec::new(), "alice");
    let s2 = put_state_with_file(&store, "lib.rs", b"a\nb\n", vec![s1.id()], "bob");
    let s3 = put_state_with_file(&store, "lib.rs", b"a\nb\nc\n", vec![s2.id()], "carol");
    let provenance = blame_file(
        &store,
        &s3,
        Path::new("lib.rs"),
        BlameSliceLimits::unlimited(),
    )
    .unwrap();
    assert!(principals_at(&provenance, 0).contains(&"alice".to_string()));
    assert!(principals_at(&provenance, 1).contains(&"bob".to_string()));
    assert!(principals_at(&provenance, 2).contains(&"carol".to_string()));
}

#[test]
fn path_absence_credits_introducing_state() {
    let store = store();
    let base = put_state_with_file(&store, "other.rs", b"keep\n", Vec::new(), "alice");
    let added = put_state_with_file(&store, "lib.rs", b"new\n", vec![base.id()], "bob");
    let provenance = blame_file(
        &store,
        &added,
        Path::new("lib.rs"),
        BlameSliceLimits::unlimited(),
    )
    .unwrap();
    assert_eq!(principals_at(&provenance, 0), vec!["bob".to_string()]);
}

#[test]
fn insert_delete_replace_keep_surviving_lines() {
    let store = store();
    let base = put_state_with_file(&store, "lib.rs", b"a\nb\nc\n", Vec::new(), "alice");
    let edited = put_state_with_file(&store, "lib.rs", b"a\nX\nc\nY\n", vec![base.id()], "bob");
    let provenance = blame_file(
        &store,
        &edited,
        Path::new("lib.rs"),
        BlameSliceLimits::unlimited(),
    )
    .unwrap();
    assert!(principals_at(&provenance, 0).contains(&"alice".to_string()));
    assert_eq!(principals_at(&provenance, 1), vec!["bob".to_string()]);
    assert!(principals_at(&provenance, 2).contains(&"alice".to_string()));
    assert_eq!(principals_at(&provenance, 3), vec!["bob".to_string()]);
}

#[test]
fn a_vs_baaab_credits_the_middle_a_after_one_shift() {
    let store = store();
    let base = put_state_with_file(&store, "lib.rs", b"a\n", Vec::new(), "alice");
    let edited = put_state_with_file(
        &store,
        "lib.rs",
        b"b\na\na\na\nb\n",
        vec![base.id()],
        "bob",
    );
    let provenance = blame_file(
        &store,
        &edited,
        Path::new("lib.rs"),
        BlameSliceLimits::unlimited(),
    )
    .unwrap();
    assert_eq!(principals_at(&provenance, 0), vec!["bob".to_string()]);
    assert_eq!(principals_at(&provenance, 1), vec!["bob".to_string()]);
    assert_eq!(principals_at(&provenance, 2), vec!["alice".to_string()]);
    assert_eq!(principals_at(&provenance, 3), vec!["bob".to_string()]);
    assert_eq!(principals_at(&provenance, 4), vec!["bob".to_string()]);
}

#[test]
fn a_vs_aa_credits_the_prefix_not_the_trailing_duplicate() {
    let store = store();
    let base = put_state_with_file(&store, "lib.rs", b"a\n", Vec::new(), "alice");
    let edited = put_state_with_file(&store, "lib.rs", b"a\na\n", vec![base.id()], "bob");
    let provenance = blame_file(
        &store,
        &edited,
        Path::new("lib.rs"),
        BlameSliceLimits::unlimited(),
    )
    .unwrap();
    assert_eq!(principals_at(&provenance, 0), vec!["alice".to_string()]);
    assert_eq!(principals_at(&provenance, 1), vec!["bob".to_string()]);
}

#[test]
fn ba_vs_abcbb_credits_first_adjacent_b() {
    let store = store();
    let base = put_state_with_file(&store, "lib.rs", b"b\na\n", Vec::new(), "alice");
    let edited = put_state_with_file(&store, "lib.rs", b"a\nb\nc\nb\nb\n", vec![base.id()], "bob");
    let provenance = blame_file(
        &store,
        &edited,
        Path::new("lib.rs"),
        BlameSliceLimits::unlimited(),
    )
    .unwrap();
    assert_eq!(principals_at(&provenance, 0), vec!["bob".to_string()]);
    assert_eq!(principals_at(&provenance, 1), vec!["alice".to_string()]);
    assert_eq!(principals_at(&provenance, 2), vec!["bob".to_string()]);
    assert_eq!(principals_at(&provenance, 3), vec!["bob".to_string()]);
    assert_eq!(principals_at(&provenance, 4), vec!["bob".to_string()]);
}

#[test]
fn ba_vs_abb_credits_rightmost_new_match() {
    let store = store();
    let base = put_state_with_file(&store, "lib.rs", b"b\na\n", Vec::new(), "alice");
    let edited = put_state_with_file(&store, "lib.rs", b"a\nb\nb\n", vec![base.id()], "bob");
    let provenance = blame_file(
        &store,
        &edited,
        Path::new("lib.rs"),
        BlameSliceLimits::unlimited(),
    )
    .unwrap();
    assert_eq!(principals_at(&provenance, 0), vec!["bob".to_string()]);
    assert_eq!(principals_at(&provenance, 1), vec!["bob".to_string()]);
    assert_eq!(principals_at(&provenance, 2), vec!["alice".to_string()]);
}

#[test]
fn ba_vs_aa_credits_leftmost_new_match() {
    let store = store();
    let base = put_state_with_file(&store, "lib.rs", b"b\na\n", Vec::new(), "alice");
    let edited = put_state_with_file(&store, "lib.rs", b"a\na\n", vec![base.id()], "bob");
    let provenance = blame_file(
        &store,
        &edited,
        Path::new("lib.rs"),
        BlameSliceLimits::unlimited(),
    )
    .unwrap();
    assert_eq!(principals_at(&provenance, 0), vec!["alice".to_string()]);
    assert_eq!(principals_at(&provenance, 1), vec!["bob".to_string()]);
}

#[test]
fn repeated_lines_are_stable() {
    let store = store();
    let base = put_state_with_file(&store, "lib.rs", b"x\na\nx\n", Vec::new(), "alice");
    let edited = put_state_with_file(&store, "lib.rs", b"x\nx\nb\n", vec![base.id()], "bob");
    let first = blame_file(
        &store,
        &edited,
        Path::new("lib.rs"),
        BlameSliceLimits::unlimited(),
    )
    .unwrap();
    let second = blame_file(
        &store,
        &edited,
        Path::new("lib.rs"),
        BlameSliceLimits::unlimited(),
    )
    .unwrap();
    assert_eq!(first, second);
}

#[test]
fn merge_history_first_parent_wins_shared_lines() {
    let store = store();
    let base = put_state_with_file(&store, "lib.rs", b"fn shared() {}\n", Vec::new(), "alice");
    let ours = put_state_with_file(
        &store,
        "lib.rs",
        b"fn shared() {}\nfn from_bob() {}\n",
        vec![base.id()],
        "bob",
    );
    let theirs = put_state_with_file(
        &store,
        "lib.rs",
        b"fn shared() {}\nfn from_carol() {}\n",
        vec![base.id()],
        "carol",
    );
    let merge = put_state_with_file(
        &store,
        "lib.rs",
        b"fn shared() {}\nfn from_bob() {}\nfn from_carol() {}\n",
        vec![ours.id(), theirs.id()],
        "dave",
    );
    let provenance = blame_file(
        &store,
        &merge,
        Path::new("lib.rs"),
        BlameSliceLimits::unlimited(),
    )
    .unwrap();
    assert!(principals_at(&provenance, 0).contains(&"alice".to_string()));
    assert!(principals_at(&provenance, 1).contains(&"bob".to_string()));
    assert!(!principals_at(&provenance, 1).contains(&"dave".to_string()));
    assert!(principals_at(&provenance, 2).contains(&"carol".to_string()));
    assert!(!principals_at(&provenance, 2).contains(&"dave".to_string()));
}

#[test]
fn empty_file_has_origin_and_no_spans() {
    let store = store();
    let state = put_state_with_file(&store, "empty.txt", b"", Vec::new(), "alice");
    let provenance = blame_file(
        &store,
        &state,
        Path::new("empty.txt"),
        BlameSliceLimits::unlimited(),
    )
    .unwrap();
    provenance.validate().unwrap();
    assert_eq!(provenance.line_count, 0);
    assert!(provenance.spans.is_empty());
    assert_eq!(
        provenance.origins[0].attribution.principal.name_lossy(),
        "alice"
    );
}

#[test]
fn no_final_newline_is_one_line() {
    let store = store();
    let state = put_state_with_file(&store, "lib.rs", b"hello", Vec::new(), "alice");
    let provenance = blame_file(
        &store,
        &state,
        Path::new("lib.rs"),
        BlameSliceLimits::unlimited(),
    )
    .unwrap();
    assert_eq!(provenance.line_count, 1);
    assert_eq!(principals_at(&provenance, 0), vec!["alice".to_string()]);
}