girt 0.3.3

An incremental, idiomatic Rust implementation of Git
Documentation
use rstest::rstest;

use super::*;
use crate::InitKind;
use crate::fetch::{AdvertisedRef, LocalFetchProgress};

fn name(value: &str) -> RefName {
    RefName::new(value).unwrap()
}
fn id(byte: u8) -> ObjectId {
    ObjectId::Sha1([byte; 20])
}
fn request(specs: &[&str]) -> (tempfile::TempDir, FetchRequest) {
    let root = tempfile::tempdir().unwrap();
    let repo = Repository::init(
        crate::ObjectFormat::Sha1,
        root.path().join("repo"),
        InitKind::Bare,
    )
    .unwrap();
    let specs =
        Refspecs::parse(Direction::Fetch, specs.iter().map(|spec| spec.as_bytes())).unwrap();
    let request = FetchRequest::prepare(repo, specs, BTreeSet::new(), Reflog::Preserve).unwrap();
    (root, request)
}
fn advertisement() -> Advertisement {
    Advertisement {
        refs: vec![
            AdvertisedRef {
                name: name("refs/heads/main"),
                id: id(1),
                peeled: false,
            },
            AdvertisedRef {
                name: name("refs/heads/private"),
                id: id(2),
                peeled: false,
            },
        ],
        capabilities: vec![],
    }
}

#[test]
fn wildcard_exclusion_and_source_only_keep_advertised_ids() {
    let (_root, request) = request(&[
        "refs/heads/*:refs/remotes/origin/*",
        "^refs/heads/private",
        "refs/heads/main",
    ]);
    let plan = request.plan(&advertisement()).unwrap();
    assert_eq!(plan.len(), 2);
    assert_eq!(
        plan[0].mapping.destination,
        Some(name("refs/remotes/origin/main"))
    );
    assert_eq!(plan[0].kind, FetchUpdateKind::Create);
    assert_eq!(plan[1].kind, FetchUpdateKind::SourceOnly);
    assert_eq!(plan[1].mapping.source.as_ref().unwrap().id, id(1));
}

#[rstest]
#[case::branch("refs/heads/main")]
#[case::private("refs/worktree/main")]
#[case::other("refs/custom/main")]
fn rejects_unsupported_destinations(#[case] destination: &str) {
    let spec = format!("+refs/heads/main:{destination}");
    let (_root, request) = request(&[&spec]);
    assert!(matches!(
        request.plan(&advertisement()),
        Err(FetchPlanError::Destination(_))
    ));
}

#[rstest]
#[case::create(None, false, false, FetchUpdateKind::Create)]
#[case::same(Some(id(2)), false, false, FetchUpdateKind::Unchanged)]
#[case::authorized(Some(id(1)), true, true, FetchUpdateKind::ForcedTag)]
fn tag_decisions(
    #[case] old: Option<ObjectId>,
    #[case] force: bool,
    #[case] authorized: bool,
    #[case] expected: FetchUpdateKind,
) {
    assert_eq!(
        update_kind(&name("refs/tags/v1"), old, id(2), force, authorized, true).unwrap(),
        expected
    );
}

#[rstest]
#[case::neither(false, false)]
#[case::syntax_only(true, false)]
#[case::authorization_only(false, true)]
fn tag_replacement_requires_both(#[case] force: bool, #[case] authorized: bool) {
    assert!(matches!(
        update_kind(
            &name("refs/tags/v1"),
            Some(id(1)),
            id(2),
            force,
            authorized,
            true
        ),
        Err(FetchPlanError::TagReplacement(_))
    ));
}

#[test]
fn remote_tracking_replacement_defers_object_checks() {
    assert_eq!(
        update_kind(
            &name("refs/remotes/origin/main"),
            Some(id(1)),
            id(2),
            false,
            false,
            false
        )
        .unwrap(),
        FetchUpdateKind::Replace
    );
}

#[test]
fn additional_tag_namespace_has_exact_boundary() {
    let spec = "+refs/heads/main:refs/jj/remote-tags/origin/main";
    let (_root, initial) = request(&[spec]);
    assert!(matches!(
        initial.plan(&advertisement()),
        Err(FetchPlanError::Destination(_))
    ));
    let planned = initial.with_tag_destination_namespace(name("refs/jj/remote-tags/origin"));
    assert_eq!(
        planned.plan(&advertisement()).unwrap()[0].kind,
        FetchUpdateKind::Create
    );

    let spec = "+refs/heads/main:refs/jj/remote-tags/origin-other/main";
    let (_root, request) = request(&[spec]);
    let request = request.with_tag_destination_namespace(name("refs/jj/remote-tags/origin"));
    assert!(matches!(
        request.plan(&advertisement()),
        Err(FetchPlanError::Destination(_))
    ));
}

#[test]
fn additional_tag_namespace_requires_force_and_authorization() {
    let dest = name("refs/jj/remote-tags/origin/v1");
    let (_root, mut request) = request(&["+refs/heads/main:refs/jj/remote-tags/origin/v1"]);
    request.snapshot.insert(dest.clone(), Target::Direct(id(2)));
    request = request.with_tag_destination_namespace(name("refs/jj/remote-tags/origin"));
    assert!(matches!(
        request.plan(&advertisement()),
        Err(FetchPlanError::TagReplacement(_))
    ));
    request.authorized_force.insert(dest);
    assert_eq!(
        request.plan(&advertisement()).unwrap()[0].kind,
        FetchUpdateKind::ForcedTag
    );
}

#[test]
fn additional_tag_namespace_prunes_missing_sources_only() {
    let (_root, mut request) = request(&[
        "+refs/heads/*:refs/jj/remote-tags/origin/*",
        "^refs/heads/private",
    ]);
    request.snapshot.insert(
        name("refs/jj/remote-tags/origin/old"),
        Target::Direct(id(3)),
    );
    request
        .snapshot
        .insert(name("refs/jj/remote-tags/other/old"), Target::Direct(id(4)));
    request = request
        .with_tag_destination_namespace(name("refs/jj/remote-tags/origin"))
        .with_prune();
    let plan = request.plan(&advertisement()).unwrap();
    assert!(plan.iter().any(|update| {
        update.mapping.destination.as_ref() == Some(&name("refs/jj/remote-tags/origin/old"))
            && update.kind == FetchUpdateKind::Prune
    }));
    assert!(!plan.iter().any(|update| {
        update.mapping.destination.as_ref() == Some(&name("refs/jj/remote-tags/other/old"))
    }));
}

#[test]
fn retained_missing_exact_source_keeps_only_its_cached_destination() {
    let (_root, mut request) = request(&[
        "+refs/heads/main:refs/remotes/origin/main",
        "+refs/tags/v1:refs/jj/remote-tags/origin/v1",
        "+refs/tags/v2:refs/jj/remote-tags/origin/v2",
    ]);
    for tag in ["v1", "v2"] {
        request.snapshot.insert(
            name(&format!("refs/jj/remote-tags/origin/{tag}")),
            Target::Direct(id(3)),
        );
    }
    let request = request
        .with_tag_destination_namespace(name("refs/jj/remote-tags/origin"))
        .with_prune()
        .with_retained_missing_sources([name("refs/tags/v1")]);
    let plan = request.plan(&advertisement()).unwrap();
    assert!(plan.iter().any(|update| {
        update.mapping.destination == Some(name("refs/remotes/origin/main"))
            && update.kind == FetchUpdateKind::Create
    }));
    assert!(!plan.iter().any(|update| {
        update.mapping.destination == Some(name("refs/jj/remote-tags/origin/v1"))
    }));
    assert!(plan.iter().any(|update| {
        update.mapping.destination == Some(name("refs/jj/remote-tags/origin/v2"))
            && update.kind == FetchUpdateKind::Prune
    }));
    let mut advertised = advertisement();
    advertised.refs.push(AdvertisedRef {
        name: name("refs/tags/v1"),
        id: id(3),
        peeled: false,
    });
    let plan = request.plan(&advertised).unwrap();
    assert!(plan.iter().any(|update| {
        update.mapping.destination == Some(name("refs/jj/remote-tags/origin/v1"))
            && update.kind == FetchUpdateKind::Unchanged
    }));
}

#[test]
fn symbolic_destination_cannot_redirect_to_branch() {
    let (_root, mut request) = request(&["refs/heads/main:refs/remotes/origin/main"]);
    request.snapshot.insert(
        name("refs/remotes/origin/main"),
        Target::Symbolic(name("refs/heads/main")),
    );
    assert!(matches!(
        request.plan(&advertisement()),
        Err(FetchPlanError::Symbolic(_))
    ));
}

#[test]
fn changed_advertisement_replans_mapping() {
    let (_root, request) = request(&["refs/heads/main:refs/remotes/origin/main"]);
    let preview = request.plan(&advertisement()).unwrap();
    let mut current = advertisement();
    current.refs[0].id = id(3);
    let actual = request.plan(&current).unwrap();
    assert_eq!(preview[0].mapping.source.as_ref().unwrap().id, id(1));
    assert_eq!(actual[0].mapping.source.as_ref().unwrap().id, id(3));
}

#[test]
fn mapping_error_selects_nothing_and_preserves_error() {
    let mut saved = None;
    let wants = select(
        Err(FetchPlanError::Destination(name("refs/heads/main"))),
        &mut saved,
    );
    let transfer: Result<(), FetchError> = Err(FetchError::Protocol("later error"));
    assert!(wants.is_empty());
    assert!(matches!(
        selected(saved, &transfer),
        Err(FetchWorkflowError::Plan(FetchPlanError::Destination(_)))
    ));
}

#[test]
fn empty_refspec_list_is_a_noop_plan() {
    let (_root, request) = request(&[]);
    assert!(request.plan(&advertisement()).unwrap().is_empty());
}

#[test]
fn mismatched_known_shallow_roots_fail_before_transfer() {
    let (_root, request) = request(&["refs/heads/main:refs/remotes/origin/main"]);
    let mut known = KnownHistory::default();
    known.shallow.push(id(3));
    assert!(matches!(
        request.check_known(&known),
        Err(FetchError::Unsupported(
            "known shallow boundaries differ from destination"
        ))
    ));
}

#[test]
fn local_progress_plan_failure_has_no_completion() {
    let (root, request) = request(&["refs/heads/main:refs/heads/main"]);
    let source = Repository::init(
        crate::ObjectFormat::Sha1,
        root.path().join("source"),
        InitKind::Bare,
    )
    .unwrap();
    let id = source.loose_objects().write_blob(b"source").unwrap();
    std::fs::write(source.git_dir().join("refs/heads/main"), format!("{id}\n")).unwrap();
    let mut events = Vec::new();
    let result = request.receive_local_with_progress(
        source.git_dir(),
        &KnownHistory::default(),
        FetchLimits::default(),
        TransportControl::new(&AtomicBool::new(false)),
        |_| ControlFlow::Continue(()),
        |event| events.push(event),
    );
    assert!(matches!(
        result,
        Err(FetchWorkflowError::Plan(FetchPlanError::Destination(_)))
    ));
    assert!(!events.contains(&LocalFetchProgress::Complete));
    assert_eq!(events, [LocalFetchProgress::Reading { objects: 0 }]);
}