monochromatic-git-policy-cli 0.5.30

Repository-owned Git policy wrapper: forwards Git commands and checks commits against configured policies.
Documentation
//! Controls for what the Git LFS upload sends, including every way the plan must fail closed.

use super::*;
use crate::test_support::{fixture, remove};
use std::path::PathBuf;

/// An argument list from words.
fn words(items: &[&str]) -> Vec<OsString> {
    return items.iter().map(OsString::from).collect();
}

/// Configuration entries from `(key, value)` pairs.
fn config(pairs: &[(&str, &str)]) -> Settings {
    return pairs
        .iter()
        .map(|(key, value)| {
            return ((*key).to_owned(), (*value).to_owned());
        })
        .collect();
}

/// What a stubbed Git answers: a query's key (its first word, or `for-each-ref:<prefix>`) and the answer, and the keys
///  whose queries fail.
struct Stub<'a> {
    /// Answers by key.
    answers: &'a [(&'a str, &'a str)],
    /// Keys that fail.
    failing: &'a [&'a str],
}

/// Plan a push in `directory` with a stubbed Git; the plan or the failure, and how many queries were made.
fn plan_in(
    directory: &Path,
    global_prefix: &[&str],
    region: &[&str],
    environment: &[(OsString, OsString)],
    settings: &Settings,
    stub: &Stub<'_>,
) -> (Result<Option<UploadPlan>, PlanError>, usize) {
    let mut asked: usize = 0;
    let mut query = |arguments: &[&str]| -> Result<String, String> {
        asked += 1;
        let key: String = if arguments[0] == "for-each-ref" {
            format!("for-each-ref:{}", arguments[arguments.len() - 1])
        } else {
            arguments[0].to_owned()
        };
        if stub.failing.contains(&key.as_str()) {
            return Err(format!("{key} failed"));
        }
        return Ok(stub
            .answers
            .iter()
            .find(|(name, _)| {
                return *name == key;
            })
            .map_or(String::new(), |(_, answer)| {
                return (*answer).to_owned();
            }));
    };
    let planned = plan_upload(words(global_prefix).as_slice(), words(region).as_slice(), environment, directory, settings.as_slice(), &mut query);
    return (planned, asked);
}

/// The same, in a directory that does not matter to the case, expecting a plan.
fn plan_of(region: &[&str], settings: &[(&str, &str)], answers: &[(&str, &str)]) -> UploadPlan {
    let (planned, _) = plan_in(Path::new("/work"), &[], region, &[], &config(settings), &Stub { answers, failing: &[] });
    return planned.expect("the plan is worked out").expect("a plan");
}

/// The same, expecting no upload.
fn no_plan(region: &[&str], settings: &[(&str, &str)], answers: &[(&str, &str)]) {
    let (planned, _) = plan_in(Path::new("/work"), &[], region, &[], &config(settings), &Stub { answers, failing: &[] });
    assert_eq!(planned.expect("the plan is worked out"), None, "{region:?}");
}

/// The first run's arguments for a destination that is not a configured remote: `push --all`, the destination, then the refs.
#[test]
fn an_unconfigured_destination_uses_all_with_the_refs_so_no_tracking_ref_hides_an_object() {
    let plan: UploadPlan = plan_of(&["origin", "main", "topic:refs/heads/other"], &[], &[]);
    assert_eq!(plan.refs, words(&["main", "topic"]));
    let mut expected: Vec<OsString> = words(&["push", "--all"]);
    expected.push(plan.remotes[0].clone());
    expected.extend(plan.refs.iter().cloned());
    assert_eq!(plan.arguments(plan.remotes[0].as_os_str()), expected);
    let (planned, asked) = plan_in(Path::new("/work"), &[], &["origin", "main"], &[], &config(&[]), &Stub { answers: &[], failing: &[] });
    assert_eq!((planned.expect("planned").is_some(), asked), (true, 0), "named refs need no Git start");
}

/// A configured remote that is fetched from and pushed to the same place uses git-lfs's bounded form: no `--all`, so the
///  objects that the remote's tracking refs reach are skipped and a push that adds none sends no batch request.
#[test]
fn a_configured_remote_with_one_url_uses_the_bounded_form() {
    let configured: &[(&str, &str)] = &[("remote.origin.url", "https://example.invalid/r.git")];
    let plan: UploadPlan = plan_of(&["origin", "main", "topic:refs/heads/other"], configured, &[]);
    assert!(!plan.full_scan);
    assert_eq!(plan.arguments(plan.remotes[0].as_os_str()), words(&["push", "origin", "main", "topic"]));
    let bare: UploadPlan = plan_of(&[], configured, &[("symbolic-ref", "refs/heads/work")]);
    assert_eq!(bare.arguments(bare.remotes[0].as_os_str()), words(&["push", "origin", "refs/heads/work"]));
    let ignored: UploadPlan = plan_of(&["origin", "main"], &[configured[0], ("remote.origin.fetch", "+refs/heads/*:refs/remotes/origin/*")], &[]);
    assert!(!ignored.full_scan, "settings the plan does not read change nothing");
}

/// Every destination whose tracking refs may not describe it keeps the full scan: a push URL (alone or several), a
///  `pushInsteadOf` rewrite, a URL or path that is not a configured remote, a mirror, and a remote nobody configured.
#[test]
fn a_destination_that_tracking_refs_may_not_describe_keeps_the_full_scan() {
    let url: (&str, &str) = ("remote.origin.url", "https://example.invalid/r.git");
    let full = |region: &[&str], settings: &[(&str, &str)]| -> Vec<OsString> {
        let plan: UploadPlan = plan_of(region, settings, &[]);
        assert!(plan.full_scan, "{region:?} {settings:?}");
        return plan.arguments(plan.remotes[0].as_os_str());
    };
    assert_eq!(full(&["origin", "main"], &[url, ("remote.origin.pushurl", "https://other.invalid/r.git")])[..2], words(&["push", "--all"])[..]);
    assert_eq!(full(&["origin", "main"], &[url, ("remote.origin.pushurl", "https://example.invalid/r.git")])[..2], words(&["push", "--all"])[..]);
    let two: UploadPlan = plan_of(&["origin", "main"], &[url, ("remote.origin.pushurl", "https://a.invalid/r.git"), ("remote.origin.pushurl", "https://b.invalid/r.git")], &[]);
    assert!(two.full_scan);
    assert_eq!(two.arguments(two.remotes[1].as_os_str()), words(&["push", "--all", "https://b.invalid/r.git", "main"]));
    assert_eq!(full(&["origin", "main"], &[url, ("url.https://other.invalid/.pushinsteadof", "https://example.invalid/")])[..2], words(&["push", "--all"])[..]);
    assert_eq!(full(&["https://example.invalid/r.git", "main"], &[url])[..3], words(&["push", "--all", "https://example.invalid/r.git"])[..]);
    assert_eq!(full(&["origin", "main"], &[("remote.origin.pushurl", "https://other.invalid/r.git")])[..2], words(&["push", "--all"])[..]);
    assert_eq!(full(&["elsewhere", "main"], &[url])[..3], words(&["push", "--all", "elsewhere"])[..]);
    assert_eq!(full(&["--mirror", "origin"], &[url]), words(&["push", "--all", "origin"]));
    assert_eq!(full(&["origin"], &[url, ("remote.origin.mirror", "true")]), words(&["push", "--all", "origin"]));
}

/// A forced refspec uploads its source, a deletion uploads nothing, `tag <name>` is the tag's ref.
#[test]
fn forced_deleting_and_tag_refspecs_read_as_git_reads_them() {
    assert_eq!(plan_of(&["origin", "+HEAD:refs/heads/x"], &[], &[]).refs, words(&["HEAD"]));
    no_plan(&["origin", ":gone"], &[], &[("symbolic-ref", "refs/heads/work")]);
    no_plan(&["--delete", "origin", "gone"], &[], &[]);
    assert_eq!(plan_of(&["origin", "tag", "v1"], &[], &[]).refs, words(&["refs/tags/v1"]));
}

/// Git splits a refspec at its last colon, so a source that holds a colon survives.
#[test]
fn a_source_with_a_colon_is_split_at_the_last_colon() {
    let plan: UploadPlan = plan_of(&["origin", ":/unique message:refs/heads/new"], &[], &[]);
    assert_eq!(plan.refs, words(&[":/unique message"]));
    assert_eq!(plan_of(&["origin", "HEAD^{/a:b}:refs/heads/x"], &[], &[]).refs, words(&["HEAD^{/a:b}"]));
}

/// The refspec `:` (and `+:`, and the configured `push = :`) means every matching branch, not a deletion.
#[test]
fn the_matching_refspec_uploads_the_branches_and_is_not_a_deletion() {
    let heads: &[(&str, &str)] = &[("for-each-ref:refs/heads", "refs/heads/main\nrefs/heads/topic")];
    for spec in [":", "+:"] {
        assert_eq!(plan_of(&["origin", spec], &[], heads).refs, words(&["refs/heads/main", "refs/heads/topic"]), "{spec}");
    }
    let configured = plan_of(&["origin"], &[("remote.origin.push", ":")], heads);
    assert_eq!(configured.refs, words(&["refs/heads/main", "refs/heads/topic"]));
}

/// A pattern is matched as a refspec matches it: the star crosses `/`, and the text around it must match.
#[test]
fn a_star_crosses_slashes_as_a_refspec_does() {
    let heads: &[(&str, &str)] = &[(
        "for-each-ref:refs/heads",
        "refs/heads/main\nrefs/heads/feature/nested\nrefs/heads/feature/deep/er\nrefs/heads/other",
    )];
    let all: UploadPlan = plan_of(&["origin", "refs/heads/*:refs/heads/*"], &[], heads);
    assert_eq!(all.refs.len(), 4);
    let nested: UploadPlan = plan_of(&["origin", "refs/heads/feature/*:refs/heads/f/*"], &[], &[("for-each-ref:refs/heads/feature", "refs/heads/feature/nested\nrefs/heads/feature/deep/er")]);
    assert_eq!(nested.refs, words(&["refs/heads/feature/nested", "refs/heads/feature/deep/er"]));
    let suffixed: UploadPlan = plan_of(&["origin", "refs/heads/*-ok:refs/heads/*-ok"], &[], &[("for-each-ref:refs/heads", "refs/heads/a-ok\nrefs/heads/b-no\nrefs/heads/c/d-ok")]);
    assert_eq!(suffixed.refs, words(&["refs/heads/a-ok", "refs/heads/c/d-ok"]));
}

/// `--all` and `--tags` upload the refs they stand for, never everything; `--mirror` uploads everything.
#[test]
fn branch_and_tag_options_expand_and_mirror_uploads_everything() {
    let heads: &[(&str, &str)] = &[("for-each-ref:refs/heads", "refs/heads/main\nrefs/heads/other"), ("for-each-ref:refs/tags", "refs/tags/v1")];
    assert_eq!(plan_of(&["--all", "origin"], &[], heads).refs, words(&["refs/heads/main", "refs/heads/other"]));
    let tags: UploadPlan = plan_of(&["--tags", "origin"], &[], heads);
    assert_eq!((tags.refs, tags.everything), (words(&["refs/tags/v1"]), false));
    no_plan(&["--tags", "origin"], &[], &[]);
    let mirror: UploadPlan = plan_of(&["--mirror", "origin"], &[], &[]);
    assert!(mirror.everything);
    assert_eq!(mirror.arguments(mirror.remotes[0].as_os_str()), words(&["push", "--all", "origin"]));
}

/// A bare push takes the remote and branch from Git, and a configured mirror, `remote.<name>.push` or
///  `push.default=matching` decides what it sends.
#[test]
fn a_bare_push_resolves_its_remote_and_refs_the_way_git_does() {
    let branch: &[(&str, &str)] = &[("symbolic-ref", "refs/heads/work"), ("for-each-ref:refs/heads", "refs/heads/work\nrefs/heads/side")];
    let remote_of = |pairs: &[(&str, &str)]| -> OsString {
        return plan_of(&[], pairs, branch).remotes[0].clone();
    };
    let layered: &[(&str, &str)] = &[
        ("branch.work.remote", "upstream"),
        ("remote.pushdefault", "shared"),
        ("branch.work.pushremote", "mine"),
    ];
    assert_eq!(remote_of(layered), OsString::from("mine"));
    assert_eq!(remote_of(&layered[..2]), OsString::from("shared"));
    assert_eq!(remote_of(&layered[..1]), OsString::from("upstream"));
    assert_eq!(remote_of(&[("branch.other.remote", "elsewhere")]), OsString::from("origin"));
    assert_eq!(plan_of(&[], &[], branch).refs, words(&["refs/heads/work"]));
    assert_eq!(plan_of(&[], &[], &[("symbolic-ref", "refs/tags/work")]).refs, words(&["HEAD"]), "a tag is not a branch");
    assert_eq!(plan_of(&[], &[], &[]).refs, words(&["HEAD"]), "a detached HEAD");
    let matching = plan_of(&[], &[("push.default", "matching")], branch);
    assert_eq!(matching.refs, words(&["refs/heads/work", "refs/heads/side"]));
    let configured = plan_of(&["origin"], &[("remote.origin.push", "refs/heads/*:refs/heads/*"), ("remote.other.push", "x:y")], branch);
    assert_eq!(configured.refs, words(&["refs/heads/work", "refs/heads/side"]));
    let mapped = plan_of(&["origin"], &[("remote.origin.push", "other:refs/heads/mapped")], branch);
    assert_eq!(mapped.refs, words(&["other"]), "the source side is uploaded");
    for spelling in ["true", "yes", "on", "1", ""] {
        let mirrored = plan_of(&["origin"], &[("remote.origin.mirror", spelling)], branch);
        assert!(mirrored.everything, "mirror={spelling:?}");
    }
    assert!(!plan_of(&["origin"], &[("remote.origin.mirror", "false")], branch).everything);
    let explicit = plan_of(&["origin", "main"], &[("remote.origin.push", "other:y"), ("remote.origin.mirror", "true")], branch);
    assert_eq!(explicit.refs, words(&["main"]), "a refspec typed on the command line wins over the configuration");
}

/// Every query that fails, and a push whose refs cannot be listed, is an error and never "nothing to upload".
#[test]
fn a_failed_query_fails_closed() {
    let cases: [(&[&str], &str); 5] = [
        (&["--all", "origin"], "for-each-ref:refs/heads"),
        (&["--tags", "origin"], "for-each-ref:refs/tags"),
        (&["origin", "refs/heads/*:refs/heads/*"], "for-each-ref:refs/heads"),
        (&["origin", ":"], "for-each-ref:refs/heads"),
        (&["origin"], "symbolic-ref"),
    ];
    for (region, failing) in cases {
        let (planned, _) = plan_in(Path::new("/work"), &[], region, &[], &config(&[]), &Stub { answers: &[], failing: &[failing] });
        let error: PlanError = planned.expect_err(&format!("{region:?} must not plan on a failed query"));
        assert!(error.0.contains(failing.split(':').next().expect("a name")), "{error:?}");
    }
    let (config_failure, _) = plan_in(Path::new("/work"), &[], &["origin"], &[], &config(&[]), &Stub { answers: &[], failing: &[] });
    assert!(config_failure.is_ok());
    let mut refusing = |_arguments: &[&str]| -> Result<String, String> { return Err(String::from("no start")); };
    assert!(read_settings(&mut refusing).is_err());
}

/// A remote with several push URLs is uploaded to once per URL; one push URL or none keeps the remote's name.
#[test]
fn a_remote_with_several_push_urls_is_uploaded_to_each() {
    let two: UploadPlan = plan_of(&["origin", "main"], &[("remote.origin.pushurl", "https://a.invalid/r.git"), ("remote.origin.pushurl", "https://b.invalid/r.git")], &[]);
    assert_eq!(two.remotes, words(&["https://a.invalid/r.git", "https://b.invalid/r.git"]));
    let one: UploadPlan = plan_of(&["origin", "main"], &[("remote.origin.pushurl", "https://a.invalid/r.git")], &[]);
    assert_eq!(one.remotes, words(&["origin"]));
    assert_eq!(plan_of(&["origin", "main"], &[], &[]).remotes, words(&["origin"]));
}

/// A destination typed as a path becomes a percent-encoded `file://` URL whether or not it starts with a dot; a configured
///  remote with the same name as a directory, a URL and an scp-style address stay as typed.
#[test]
fn a_path_destination_becomes_an_encoded_file_url() {
    let root = fixture("lfs-plan-paths");
    let (backup, hashed): (PathBuf, PathBuf) = (root.join("backup.git"), root.join("hash#dir.git"));
    std::fs::create_dir(&backup).expect("backup directory");
    std::fs::create_dir(&hashed).expect("hash directory");
    let remote_of = |typed: &str, settings: &[(&str, &str)]| -> OsString {
        let (planned, _) = plan_in(root.as_path(), &[], &[typed, "main"], &[], &config(settings), &Stub { answers: &[], failing: &[] });
        return planned.expect("planned").expect("a plan").remotes[0].clone();
    };
    let prefix: String = format!("file://{}", root.display());
    assert_eq!(remote_of("backup.git", &[]), OsString::from(format!("{prefix}/backup.git")));
    assert_eq!(remote_of("./backup.git", &[]), OsString::from(format!("{prefix}/backup.git")));
    assert_eq!(remote_of("hash#dir.git", &[]), OsString::from(format!("{prefix}/hash%23dir.git")));
    assert_eq!(remote_of("backup.git", &[("remote.backup.git.url", "x")]), OsString::from("backup.git"));
    assert_eq!(remote_of("origin", &[]), OsString::from("origin"), "no such directory: a remote name");
    assert_eq!(remote_of("https://example.invalid/r.git", &[]), OsString::from("https://example.invalid/r.git"));
    assert_eq!(remote_of("git@example.invalid:r.git", &[]), OsString::from("git@example.invalid:r.git"));
    remove(root.as_path());
}

/// The first positional is the repository whatever `--repo` says, and `--repo` and its abbreviations fill in only when
///  none is typed.
#[test]
fn the_repository_is_read_as_git_reads_it() {
    let positional: UploadPlan = plan_of(&["--repo=second", "origin", "main"], &[], &[]);
    assert_eq!((positional.remotes, positional.refs), (words(&["origin"]), words(&["main"])));
    for spelling in ["--repo=elsewhere", "--rep=elsewhere"] {
        let option_only: UploadPlan = plan_of(&[spelling, "--force"], &[], &[("symbolic-ref", "refs/heads/work")]);
        assert_eq!(option_only.remotes, words(&["elsewhere"]), "{spelling}");
    }
}

/// A push that recurses into submodules is marked; every way of bypassing the upload plans nothing.
#[test]
fn submodule_pushes_are_marked_and_bypasses_plan_nothing() {
    for (value, marked) in [("on-demand", true), ("only", true), ("check", false), ("no", false)] {
        let option: String = format!("--recurse-submodules={value}");
        assert_eq!(plan_of(&[option.as_str(), "origin", "main"], &[], &[]).submodules, marked, "{value}");
    }
    for region in [vec!["--no-verify", "origin", "main"], vec!["--no-ver", "origin", "main"], vec!["--dry-run", "origin", "main"]] {
        no_plan(region.as_slice(), &[], &[]);
    }
    let environment = |pairs: &[(&str, &str)]| -> Vec<(OsString, OsString)> {
        return pairs
            .iter()
            .map(|(name, value)| {
                return (OsString::from(name), OsString::from(value));
            })
            .collect();
    };
    let stub: Stub<'_> = Stub { answers: &[], failing: &[] };
    let off: &[&str] = &["-c", "hook.cli-git-lfs-pre-push.enabled=false"];
    assert_eq!(plan_in(Path::new("/work"), off, &["origin", "main"], &[], &config(&[]), &stub).0, Ok(None));
    let skip = environment(&[("GIT_LFS_SKIP_PUSH", "1")]);
    assert_eq!(plan_in(Path::new("/work"), &[], &["origin", "main"], skip.as_slice(), &config(&[]), &stub).0, Ok(None));
    let pairs = environment(&[("GIT_CONFIG_COUNT", "1"), ("GIT_CONFIG_KEY_0", "hook.cli-git-lfs-pre-push.enabled"), ("GIT_CONFIG_VALUE_0", "false")]);
    assert_eq!(plan_in(Path::new("/work"), &[], &["origin", "main"], pairs.as_slice(), &config(&[]), &stub).0, Ok(None));
    assert_eq!(plan_in(Path::new("/work"), &[], &["--no-such-option"], &[], &config(&[]), &stub).0, Ok(None), "Git refuses it");
}

/// The snapshot's keys are filtered to the ones the plan reads, with the same rule the query's pattern states.
#[test]
fn only_the_keys_the_plan_reads_are_taken_from_a_snapshot() {
    for key in [
        "branch.work.pushremote",
        "branch.a.b.remote",
        "remote.pushdefault",
        "remote.origin.push",
        "remote.origin.pushurl",
        "remote.origin.mirror",
        "remote.o.r.url",
        "push.default",
        "url.https://example.invalid/.pushinsteadof",
    ] {
        assert!(crate::lfs_upload_settings::wanted_key(key), "{key}");
    }
    for key in ["url.https://example.invalid/.insteadof", "remote.origin.fetch", "branch.work.merge", "push.followtags", "core.bare", "remote.origin.tagopt"] {
        assert!(!crate::lfs_upload_settings::wanted_key(key), "{key}");
    }
}