girt 0.14.0

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

use super::*;

#[rstest]
#[case::first_and_all(b"url=one\nurl=two\n", Some(b"one".as_slice()), vec![b"one".as_slice(), b"two"])]
#[case::push_override(b"url=one\npushurl=p\npushurl=q\n", Some(b"one".as_slice()), vec![b"p".as_slice(), b"q"])]
#[case::url_reset(b"url=old\nurl=\nurl=new\n", Some(b"new".as_slice()), vec![b"new".as_slice()])]
#[case::push_reset_fallback(b"url=one\npushurl=old\npushurl=\n", Some(b"one".as_slice()), vec![b"one".as_slice()])]
#[case::reset_then_append(b"url=one\npushurl=old\npushurl=\npushurl=new\n", Some(b"one".as_slice()), vec![b"new".as_slice()])]
#[case::all_reset(b"url=old\nurl=\n", None, vec![])]
#[case::push_only(b"pushurl=p\n", None, vec![b"p".as_slice()])]
#[case::duplicate(b"url=a\nurl=a\n", Some(b"a".as_slice()), vec![b"a".as_slice(), b"a"])]
#[case::bytes(b"url=\xff\n", Some(b"\xff".as_slice()), vec![b"\xff".as_slice()])]
#[case::missing_urls(b"fetch=refs/heads/main\n", None, vec![])]
fn url_order_and_fallback(
    #[case] entries: &[u8],
    #[case] fetch: Option<&[u8]>,
    #[case] push: Vec<&[u8]>,
) {
    let bytes = [b"[remote \"origin\"]\n".as_slice(), entries].concat();
    let config = Config::parse(&bytes).unwrap();
    let remote = Remote::find(&config, b"origin").unwrap().unwrap();
    assert_eq!(remote.fetch_url(), fetch);
    assert_eq!(remote.push_urls(), push);
    let urls = RemoteUrls::find(&config, b"origin").unwrap().unwrap();
    assert_eq!(urls.fetch_url(), fetch);
    assert_eq!(urls.push_urls(), remote.push_urls());
    assert_eq!(
        urls.fetch_display_url(&config).unwrap(),
        fetch.map(<[u8]>::to_vec)
    );
    assert_eq!(
        urls.push_display_url(&config).unwrap(),
        push.first().map(|url| url.to_vec())
    );
}

#[rstest]
#[case::url(b"url\n", "url")]
#[case::pushurl(b"pushurl\n", "pushurl")]
#[case::fetch(b"fetch\n", "fetch")]
#[case::push(b"push\n", "push")]
fn rejects_implicit_values(#[case] entry: &[u8], #[case] key: &str) {
    let bytes = [b"[remote \"o\"]\n".as_slice(), entry].concat();
    let config = Config::parse(&bytes).unwrap();
    let error = Remote::find(&config, b"o").unwrap_err();
    assert!(
        matches!(error, RemoteError::MissingValue { key: actual, occurrence: 1 } if key == actual)
    );
}

#[rstest]
#[case::empty("fetch=", "fetch", 1)]
#[case::second("fetch=refs/heads/main\nfetch=main", "fetch", 2)]
#[case::other_direction("url=valid\nfetch=refs/heads/main\npush=^refs/heads/main", "push", 1)]
fn diagnoses_bad_refspec_occurrences(
    #[case] entries: &str,
    #[case] key: &str,
    #[case] occurrence: usize,
) {
    let config = Config::parse(format!("[remote \"o\"]\n{entries}\n").as_bytes()).unwrap();
    let error = Remote::find(&config, b"o").unwrap_err();
    assert!(
        matches!(error, RemoteError::Refspec { key: actual, occurrence: index, .. } if actual == key && index == occurrence)
    );
}

#[test]
fn remote_names_and_repeated_sections_are_exact() {
    let config = Config::parse(b"[remote \"Origin\"]\nURL=first\nfetch=refs/heads/main\n[remote \"origin\"]\nunknown=yes\n[REMOTE \"Origin\"]\nurl=second\nfetch=refs/heads/main\n[remote \"\xff\"]\nunknown=yes\n[remote \"empty\"]\n").unwrap();
    assert_eq!(
        Remote::names(&config),
        vec![b"Origin".as_slice(), b"origin", b"\xff"]
    );
    assert_eq!(RemoteUrls::names(&config), Remote::names(&config));
    let remote = Remote::find(&config, b"Origin").unwrap().unwrap();
    assert_eq!(remote.name(), b"Origin");
    assert_eq!(remote.urls(), vec![b"first".to_vec(), b"second".to_vec()]);
    assert_eq!(remote.fetch_refspecs().specs().len(), 2);
    assert!(remote.push_refspecs().specs().is_empty());
    assert!(Remote::find(&config, b"missing").unwrap().is_none());
    assert!(
        Remote::find(&config, b"origin")
            .unwrap()
            .unwrap()
            .urls()
            .is_empty()
    );
}

#[test]
fn url_only_snapshot_ignores_malformed_refspecs_and_retains_bytes() {
    let config = Config::parse(
        b"[remote \"r\"]\nurl=old\nurl=\nurl=\xff\nurl=second\npushurl=old-push\npushurl=\npushurl=first-push\npushurl=second-push\nfetch=^invalid\npush=^invalid\n",
    )
    .unwrap();
    let remote = RemoteUrls::find(&config, b"r").unwrap().unwrap();
    assert_eq!(remote.name(), b"r");
    assert_eq!(remote.urls(), [b"\xff".to_vec(), b"second".to_vec()]);
    assert_eq!(remote.fetch_url(), Some(b"\xff".as_slice()));
    assert_eq!(
        remote.configured_push_urls(),
        [b"first-push".to_vec(), b"second-push".to_vec()]
    );
    assert_eq!(remote.push_urls(), remote.configured_push_urls());
    assert!(matches!(
        Remote::find(&config, b"r"),
        Err(RemoteError::Refspec { .. })
    ));
    assert!(RemoteUrls::find(&config, b"missing").unwrap().is_none());
}

#[test]
fn url_only_snapshot_reports_url_errors() {
    let config = Config::parse(b"[remote \"r\"]\nurl=first\nurl\nfetch=^invalid\n").unwrap();
    assert!(matches!(
        RemoteUrls::find(&config, b"r"),
        Err(RemoteError::MissingValue {
            key: "url",
            occurrence: 2
        })
    ));
}

#[test]
fn url_only_snapshot_debug_redacts_values() {
    let config = Config::parse(b"[remote \"r\"]\nurl=https://secret@example.test/repo\n").unwrap();
    let remote = RemoteUrls::find(&config, b"r").unwrap().unwrap();
    assert!(!format!("{remote:?}").contains("secret"));
}