1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
use std::{
    collections::BTreeMap,
    path::{Path, PathBuf},
    sync::Mutex,
};

pub use bstr;
use bstr::{BStr, ByteSlice};
use nom::error::VerboseError;
use once_cell::sync::Lazy;
pub use tempfile;

static SCRIPT_IDENTITY: Lazy<Mutex<BTreeMap<PathBuf, u32>>> = Lazy::new(|| Mutex::new(BTreeMap::new()));

pub fn hex_to_id(hex: &str) -> git_hash::ObjectId {
    git_hash::ObjectId::from_hex(hex.as_bytes()).expect("40 bytes hex")
}

pub fn fixture_path(path: impl AsRef<str>) -> PathBuf {
    PathBuf::from("tests").join("fixtures").join(path.as_ref())
}
pub fn scripted_fixture_repo_read_only(script_name: &str) -> std::result::Result<PathBuf, Box<dyn std::error::Error>> {
    scripted_fixture_repo_read_only_with_args(script_name, None)
}

pub fn scripted_fixture_repo_writable(
    script_name: &str,
) -> std::result::Result<tempfile::TempDir, Box<dyn std::error::Error>> {
    scripted_fixture_repo_writable_with_args(script_name, None)
}

pub fn scripted_fixture_repo_writable_with_args(
    script_name: &str,
    args: impl IntoIterator<Item = &'static str>,
) -> std::result::Result<tempfile::TempDir, Box<dyn std::error::Error>> {
    let ro_dir = scripted_fixture_repo_read_only_with_args(script_name, args)?;
    let dst = tempfile::TempDir::new()?;
    fs_extra::copy_items(
        &std::fs::read_dir(ro_dir)?
            .map(|e| e.map(|e| e.path()))
            .collect::<Result<Vec<_>, _>>()?,
        dst.path(),
        &fs_extra::dir::CopyOptions {
            overwrite: false,
            skip_exist: false,
            copy_inside: false,
            content_only: false,
            ..Default::default()
        },
    )
    .map_err(|err| std::io::Error::new(std::io::ErrorKind::Other, err))?;
    Ok(dst)
}

/// Returns the directory at which the data is present
pub fn scripted_fixture_repo_read_only_with_args(
    script_name: &str,
    args: impl IntoIterator<Item = &'static str>,
) -> std::result::Result<PathBuf, Box<dyn std::error::Error>> {
    let script_path = fixture_path(script_name);

    // keep this lock to assure we don't return unfinished directories for threaded callers
    let args: Vec<String> = args.into_iter().map(Into::into).collect();
    let mut map = SCRIPT_IDENTITY.lock().unwrap();
    let script_identity = map
        .entry(args.iter().fold(script_path.clone(), |p, a| p.join(a)))
        .or_insert_with(|| {
            let crc_value = crc::Crc::<u32>::new(&crc::CRC_32_CKSUM);
            let mut crc_digest = crc_value.digest();
            crc_digest.update(&std::fs::read(&script_path).expect("file can be read entirely"));
            for arg in args.iter() {
                crc_digest.update(arg.as_bytes());
            }
            crc_digest.finalize()
        })
        .to_owned();
    let script_result_directory = fixture_path(
        Path::new("generated")
            .join(format!("{}", script_identity))
            .to_string_lossy(),
    );
    if !script_result_directory.is_dir() {
        std::fs::create_dir_all(&script_result_directory)?;
        let script_absolute_path = std::env::current_dir()?.join(script_path);
        let output = std::process::Command::new("bash")
            .arg(script_absolute_path)
            .args(args)
            .stdout(std::process::Stdio::piped())
            .stderr(std::process::Stdio::piped())
            .current_dir(&script_result_directory)
            .env_remove("GIT_DIR")
            .env("GIT_AUTHOR_DATE", "2000-01-01 00:00:00 +0000")
            .env("GIT_AUTHOR_EMAIL", "author@example.com")
            .env("GIT_AUTHOR_NAME", "author")
            .env("GIT_COMMITTER_DATE", "2000-01-02 00:00:00 +0000")
            .env("GIT_COMMITTER_EMAIL", "committer@example.com")
            .env("GIT_COMMITTER_NAME", "committer")
            .output()?;
        assert!(
            output.status.success(),
            "repo script failed: stdout: {}\nstderr: {}",
            output.stdout.as_bstr(),
            output.stderr.as_bstr()
        );
    }
    Ok(script_result_directory)
}

pub fn to_bstr_err(err: nom::Err<VerboseError<&[u8]>>) -> VerboseError<&BStr> {
    let err = match err {
        nom::Err::Error(err) | nom::Err::Failure(err) => err,
        nom::Err::Incomplete(_) => unreachable!("not a streaming parser"),
    };
    VerboseError {
        errors: err.errors.into_iter().map(|(i, v)| (i.as_bstr(), v)).collect(),
    }
}