monochromatic-git-policy-cli 0.5.15

Repository-owned Git policy wrapper: forwards Git commands and checks commits against configured policies.
//! What:
//!  Which `checkout` and `restore` command lines take one side of a conflicted file, in each spelling.
//! Why:
//!  A line that qualified by mistake would let a forced, patched or unreadable form through the guard; a line that was
//!  turned away by mistake blocks the command the owner decided to allow.
//!
//! In TS you'd write (pseudocode):
//! ```ts
//! // expect(conflictSideRequest('checkout', ['--theirs', '--', 'a.rs'])).toEqual({ paths: ['a.rs'] });
//! ```

use super::{SideRequest, conflict_side_request};
use crate::command_test_support::os_arguments;

/// The request a line gives, with the paths as text.
fn paths_of(word: &str, values: &[&str]) -> Option<Vec<String>> {
    let region = os_arguments(values);
    return conflict_side_request(word.as_bytes(), region.as_slice())
        .map(|SideRequest { paths }| { return paths.iter().map(|path| { return path.to_string_lossy().into_owned(); }).collect(); });
}

/// Each checkout form that chooses a side, names a revision, or sets how a file is written qualifies and names its paths.
#[test]
fn checkout_forms_that_take_a_side_qualify() {
    for (values, expected) in [
        (vec!["--ours", "--", "a.rs"], vec!["a.rs"]),
        (vec!["--theirs", "--", "a.rs", "dir/b.rs"], vec!["a.rs", "dir/b.rs"]),
        (vec!["-2", "--", "a.rs"], vec!["a.rs"]),
        (vec!["-3", "--", "a.rs"], vec!["a.rs"]),
        (vec!["topic", "--", "a.rs"], vec!["a.rs"]),
        (vec!["HEAD~1", "--", "a.rs"], vec!["a.rs"]),
        (vec!["-m", "--", "a.rs"], vec!["a.rs"]),
        (vec!["--merge", "--", "a.rs"], vec!["a.rs"]),
        (vec!["--conflict=diff3", "--", "a.rs"], vec!["a.rs"]),
        (vec!["-q", "--theirs", "--", "a.rs"], vec!["a.rs"]),
        (vec!["--the", "--", "a.rs"], vec!["a.rs"]),
        (vec!["--", "a.rs"], vec!["a.rs"]),
    ] {
        assert_eq!(paths_of("checkout", values.as_slice()), Some(expected.iter().map(|each| { return String::from(*each); }).collect()), "{values:?}");
    }
}

/// Anything that forces, patches, reads paths from a file, makes a branch, has no `--`, or names nothing after it does not qualify.
#[test]
fn checkout_forms_that_discard_more_or_say_less_do_not_qualify() {
    for values in [
        vec!["-f", "--", "a.rs"],
        vec!["--force", "--ours", "--", "a.rs"],
        vec!["-p", "--", "a.rs"],
        vec!["--patch", "topic", "--", "a.rs"],
        vec!["--pathspec-from-file=list", "--ours"],
        vec!["--pathspec-from-file=list", "--", "a.rs"],
        vec!["-b", "topic", "--", "a.rs"],
        vec!["-B", "topic", "--", "a.rs"],
        vec!["--orphan", "topic", "--", "a.rs"],
        vec!["-d", "--", "a.rs"],
        vec!["--ours", "a.rs"],
        vec!["topic", "a.rs"],
        vec!["--ours", "--"],
        vec!["topic", "other", "--", "a.rs"],
        vec!["--no-such-option", "--", "a.rs"],
        vec!["topic"],
        vec![],
    ] {
        assert_eq!(paths_of("checkout", values.as_slice()), None, "{values:?}");
    }
}

/// The restore forms that write worktree files from a side or a revision qualify; index-only and abbreviated ones do not.
#[test]
fn restore_forms_are_read_exactly() {
    for (values, expected) in [
        (vec!["--ours", "--", "a.rs"], Some(vec!["a.rs"])),
        (vec!["--theirs", "--", "a.rs", "b.rs"], Some(vec!["a.rs", "b.rs"])),
        (vec!["--source=topic", "--", "a.rs"], Some(vec!["a.rs"])),
        (vec!["--source", "topic", "--", "a.rs"], Some(vec!["a.rs"])),
        (vec!["-s", "HEAD~1", "--", "a.rs"], Some(vec!["a.rs"])),
        (vec!["-W", "--source=topic", "--", "a.rs"], Some(vec!["a.rs"])),
        (vec!["--worktree", "--theirs", "--", "a.rs"], Some(vec!["a.rs"])),
        (vec!["-S", "-W", "--theirs", "--", "a.rs"], Some(vec!["a.rs"])),
        (vec!["--staged", "--worktree", "--theirs", "--", "a.rs"], Some(vec!["a.rs"])),
        (vec!["-m", "--", "a.rs"], Some(vec!["a.rs"])),
        (vec!["--conflict=zdiff3", "--", "a.rs"], Some(vec!["a.rs"])),
        (vec!["--conflict", "diff3", "--", "a.rs"], Some(vec!["a.rs"])),
        (vec!["-S", "--theirs", "--", "a.rs"], None),
        (vec!["--staged", "--source=topic", "--", "a.rs"], None),
        (vec!["--theirs", "a.rs"], None),
        (vec!["--the", "--", "a.rs"], None),
        (vec!["-p", "--", "a.rs"], None),
        (vec!["--patch", "--", "a.rs"], None),
        (vec!["--pathspec-from-file=list"], None),
        (vec!["--theirs", "--"], None),
        (vec!["-sHEAD", "--", "a.rs"], None),
        (vec![], None),
    ] {
        let typed: Option<Vec<String>> = expected.map(|paths| { return paths.iter().map(|each| { return String::from(*each); }).collect(); });
        assert_eq!(paths_of("restore", values.as_slice()), typed, "{values:?}");
    }
}

/// Every other command word is not a way to take a side.
#[test]
fn other_commands_never_qualify() {
    for word in ["switch", "reset", "rm", "merge", "checkout-index", "stash", "add"] {
        assert_eq!(paths_of(word, &["--theirs", "--", "a.rs"]), None, "{word}");
    }
}