girt 0.11.1

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

use super::*;
use crate::{InitKind, PackLimits, Repository, Tree};

struct Fixture {
    root: tempfile::TempDir,
    objects: Objects,
    change: TreeChange,
    tree: ObjectId,
}

impl Fixture {
    fn new() -> Self {
        let root = tempfile::tempdir().unwrap();
        let repo = Repository::init(
            crate::ObjectFormat::Sha1,
            root.path().join("repo"),
            InitKind::Bare,
        )
        .unwrap();
        let loose = repo.loose_objects();
        let old = loose.write_blob(b"old\n").unwrap();
        let new = loose.write_blob(b"new\n").unwrap();
        let tree = loose
            .write_tree(&Tree::new(crate::ObjectFormat::Sha1, vec![]).unwrap())
            .unwrap();
        let objects = repo.objects(PackLimits::default()).unwrap();
        Self {
            root,
            objects,
            tree,
            change: TreeChange {
                path: b"file".to_vec(),
                old: Some(TreeValue {
                    mode: EntryMode::Blob,
                    id: old,
                }),
                new: Some(TreeValue {
                    mode: EntryMode::Blob,
                    id: new,
                }),
            },
        }
    }

    fn read(&self, limit: usize) -> Result<BlobContent, ContentReadError> {
        BlobContent::read(
            &self.objects,
            &self.change,
            ReadLimits::default(),
            limit,
            &AtomicBool::new(false),
        )
    }

    fn old_path(&self) -> std::path::PathBuf {
        let id = self.change.old.unwrap().id.to_string();
        self.root
            .path()
            .join("repo/objects")
            .join(&id[..2])
            .join(&id[2..])
    }
}

#[rstest]
#[case::regular(EntryMode::Blob)]
#[case::executable(EntryMode::Executable)]
#[case::symlink_bytes(EntryMode::Symlink)]
fn accepts_blob_modes_with_exact_cumulative_limit(#[case] mode: EntryMode) {
    let mut f = Fixture::new();
    f.change.old.as_mut().unwrap().mode = mode;
    f.change.new.as_mut().unwrap().mode = mode;
    let content = f.read(8).unwrap();
    assert_eq!(content.old, b"old\n");
    assert_eq!(content.new, b"new\n");
}

#[rstest]
#[case::old(None, Some(b"new\n".as_slice()), b"", b"new\n")]
#[case::new(Some(b"old\n".as_slice()), None, b"old\n", b"")]
#[case::both(None, None, b"", b"")]
fn absent_sides_are_empty(
    #[case] old: Option<&[u8]>,
    #[case] new: Option<&[u8]>,
    #[case] expected_old: &[u8],
    #[case] expected_new: &[u8],
) {
    let mut f = Fixture::new();
    f.change.old = old.map(|_| f.change.old.unwrap());
    f.change.new = new.map(|_| f.change.new.unwrap());
    let content = f.read(8).unwrap();
    assert_eq!(content.old, expected_old);
    assert_eq!(content.new, expected_new);
}

#[rstest]
#[case::tree(EntryMode::Tree)]
#[case::gitlink(EntryMode::Gitlink)]
fn rejects_non_blob_modes_before_reading_either_side(#[case] mode: EntryMode) {
    let mut f = Fixture::new();
    std::fs::remove_file(f.old_path()).unwrap();
    f.change.new.as_mut().unwrap().mode = mode;
    assert!(matches!(f.read(8), Err(ContentReadError::UnsupportedMode(found)) if found == mode));
}

#[rstest]
#[case::first_payload(3)]
#[case::combined_payload(7)]
fn bounds_decoding_before_retaining_payload(#[case] limit: usize) {
    let f = Fixture::new();
    assert!(matches!(f.read(limit), Err(ContentReadError::Read { .. })));
}

#[test]
fn per_read_limit_remains_independent() {
    let f = Fixture::new();
    let read = ReadLimits {
        max_object_bytes: 3,
        ..ReadLimits::default()
    };
    assert!(matches!(
        BlobContent::read(&f.objects, &f.change, read, 8, &AtomicBool::new(false)),
        Err(ContentReadError::Read { .. })
    ));
}

#[test]
fn missing_equal_ids_are_not_skipped() {
    let mut f = Fixture::new();
    std::fs::remove_file(f.old_path()).unwrap();
    f.change.new = f.change.old;
    assert!(
        matches!(f.read(8), Err(ContentReadError::Missing(id)) if id == f.change.old.unwrap().id)
    );
}

#[test]
fn missing_replacement_fails_after_reading_original() {
    let mut f = Fixture::new();
    let missing = ObjectId::Sha1([42; 20]);
    f.change.new.as_mut().unwrap().id = missing;
    assert!(matches!(f.read(8), Err(ContentReadError::Missing(id)) if id == missing));
}

#[test]
fn wrong_kind_retains_identity_and_actual_kind() {
    let mut f = Fixture::new();
    f.change.new.as_mut().unwrap().id = f.tree;
    assert!(
        matches!(f.read(8), Err(ContentReadError::NotBlob { id, actual: ObjectKind::Tree }) if id == f.tree)
    );
}

#[test]
fn corrupt_payload_retains_storage_source() {
    let f = Fixture::new();
    std::fs::write(f.old_path(), b"not a zlib object").unwrap();
    let error = f.read(8).unwrap_err();
    assert!(std::error::Error::source(&error).is_some());
    assert!(matches!(error, ContentReadError::Read { id, .. } if id == f.change.old.unwrap().id));
}

#[test]
fn cancellation_precedes_mode_errors_and_reads() {
    let mut f = Fixture::new();
    f.change.old.as_mut().unwrap().mode = EntryMode::Gitlink;
    assert!(matches!(
        BlobContent::read(
            &f.objects,
            &f.change,
            ReadLimits::default(),
            8,
            &AtomicBool::new(true)
        ),
        Err(ContentReadError::Cancelled)
    ));
}