quinjet 0.0.59

A fast, live, keyboard-first Git source-control interface for the terminal
use super::*;
use crate::git::{StackModifyAction, StackOperation, StackRebaseAction};

fn resolves(path: &[&str]) -> bool {
    let root = Cli::command();
    let mut command = &root;
    let mut found = Vec::new();
    for name in path {
        let Some(child) = command.find_subcommand(name) else {
            return false;
        };
        found.push(child);
        command = found.last().unwrap();
    }
    true
}

macro_rules! operation_routes {
        ($($pattern:pat => $sample:expr => [$($path:literal),+];)+) => {
            const fn verb_for(operation: &GitOperation) -> &'static [&'static str] {
                match operation {
                    $($pattern => &[$($path),+],)+
                }
            }

            #[test]
            fn every_operation_the_interface_performs_has_a_verb() {
                let operations = [$($sample),+];
                for operation in &operations {
                    let path = verb_for(operation);
                    assert!(
                        resolves(path),
                        "{operation:?} names the verb {path:?}, which the command tree does not have"
                    );
                }
                let kinds: HashSet<_> = operations.iter().map(std::mem::discriminant).collect();
                assert_eq!(
                    kinds.len(),
                    operations.len(),
                    "every operation variant must have one route fixture"
                );
            }
        };
    }

operation_routes! {
    GitOperation::Stage(_) => GitOperation::Stage(Vec::new()) => ["stage"];
    GitOperation::StageAll => GitOperation::StageAll => ["stage"];
    GitOperation::Unstage(_) => GitOperation::Unstage(Vec::new()) => ["unstage"];
    GitOperation::UnstageAll => GitOperation::UnstageAll => ["unstage"];
    GitOperation::Discard(_) => GitOperation::Discard(Vec::new()) => ["discard"];
    GitOperation::Remove(_) => GitOperation::Remove(Vec::new()) => ["remove"];
    GitOperation::Commit { .. } => GitOperation::Commit { message: String::new(), amend: false } => ["commit"];
    GitOperation::Fetch => GitOperation::Fetch => ["fetch"];
    GitOperation::Pull => GitOperation::Pull => ["pull"];
    GitOperation::Push => GitOperation::Push => ["push"];
    GitOperation::Sync => GitOperation::Sync => ["sync"];
    GitOperation::Checkout(_) => GitOperation::Checkout(String::new()) => ["branch", "switch"];
    GitOperation::CreateBranch { .. } => GitOperation::CreateBranch { name: String::new(), start: None } => ["branch", "create"];
    GitOperation::RenameBranch { .. } => GitOperation::RenameBranch { old: String::new(), new: String::new() } => ["branch", "rename"];
    GitOperation::DeleteBranch(_) => GitOperation::DeleteBranch(String::new()) => ["branch", "delete"];
    GitOperation::StashPush { .. } => GitOperation::StashPush { message: String::new(), include_untracked: false, staged: false, paths: Vec::new() } => ["stash", "push"];
    GitOperation::StashApply(_) => GitOperation::StashApply(String::new()) => ["stash", "apply"];
    GitOperation::StashPop(_) => GitOperation::StashPop(None) => ["stash", "pop"];
    GitOperation::StashDrop(_) => GitOperation::StashDrop(String::new()) => ["stash", "drop"];
    GitOperation::StashClear => GitOperation::StashClear => ["stash", "clear"];
    GitOperation::ResolveConflict { .. } => GitOperation::ResolveConflict { path: PathBuf::new(), choice: ConflictChoice::Ours } => ["resolve"];
    GitOperation::CherryPick(_) => GitOperation::CherryPick(String::new()) => ["cherry-pick"];
    GitOperation::Revert(_) => GitOperation::Revert(String::new()) => ["revert"];
    GitOperation::Stack(_) => GitOperation::Stack(Box::new(StackOperation::Top)) => ["stack", "top"];
}

const fn stack_verb_for(operation: &StackOperation) -> &'static [&'static str] {
    match operation {
        StackOperation::Init { .. } => &["stack", "init"],
        StackOperation::Add { .. } => &["stack", "add"],
        StackOperation::Checkout(_) => &["stack", "checkout"],
        StackOperation::Modify(_) => &["stack", "modify"],
        StackOperation::Unstack { .. } => &["stack", "unstack"],
        StackOperation::Link { .. } => &["stack", "link"],
        StackOperation::Merge { .. } => &["stack", "merge"],
        StackOperation::Push { .. } => &["stack", "push"],
        StackOperation::Rebase(_) => &["stack", "rebase"],
        StackOperation::Submit { .. } => &["stack", "submit"],
        StackOperation::Sync { .. } => &["stack", "sync"],
        StackOperation::Bottom => &["stack", "bottom"],
        StackOperation::Down(_) => &["stack", "down"],
        StackOperation::Top => &["stack", "top"],
        StackOperation::Trunk => &["stack", "trunk"],
        StackOperation::Up(_) => &["stack", "up"],
    }
}

#[test]
fn every_stack_operation_has_a_verb() {
    let operations = [
        StackOperation::Init {
            branches: Vec::new(),
            base: None,
        },
        StackOperation::Add {
            branch: String::new(),
            all: false,
            update: false,
            message: None,
        },
        StackOperation::Checkout(String::new()),
        StackOperation::Modify(StackModifyAction::Abort),
        StackOperation::Unstack {
            stack: None,
            local: false,
        },
        StackOperation::Link {
            members: Vec::new(),
            base: None,
            open: false,
            remote: None,
        },
        StackOperation::Merge {
            target: None,
            method: PullRequestMergeMethod::Squash,
        },
        StackOperation::Push { remote: None },
        StackOperation::Rebase(StackRebaseAction::Abort),
        StackOperation::Submit {
            open: false,
            remote: None,
        },
        StackOperation::Sync {
            prune: false,
            remote: None,
        },
        StackOperation::Bottom,
        StackOperation::Down(1),
        StackOperation::Top,
        StackOperation::Trunk,
        StackOperation::Up(1),
    ];
    for operation in &operations {
        let path = stack_verb_for(operation);
        assert!(resolves(path), "stack operation is missing {path:?}");
    }
    let kinds: HashSet<_> = operations.iter().map(std::mem::discriminant).collect();
    assert_eq!(kinds.len(), operations.len());
}

#[test]
fn the_read_only_views_have_verbs_too() {
    for path in [
        ["status"].as_slice(),
        ["diff"].as_slice(),
        ["log"].as_slice(),
        ["show"].as_slice(),
        ["branch", "list"].as_slice(),
        ["branch", "compare"].as_slice(),
        ["stash", "list"].as_slice(),
        ["stash", "show"].as_slice(),
        ["worktree", "list"].as_slice(),
        ["project", "list"].as_slice(),
        ["repos"].as_slice(),
        ["pr", "view"].as_slice(),
        ["pr", "files"].as_slice(),
        ["pr", "commits"].as_slice(),
        ["pr", "diff"].as_slice(),
        ["pr", "checks"].as_slice(),
        ["pr", "conversation"].as_slice(),
        ["pr", "logs"].as_slice(),
        ["pr", "open"].as_slice(),
        ["pr", "reviews", "show"].as_slice(),
        ["stack", "view"].as_slice(),
        ["stack", "files"].as_slice(),
        ["stack", "diff"].as_slice(),
        ["tui"].as_slice(),
        ["completions"].as_slice(),
        ["man"].as_slice(),
        ["capabilities"].as_slice(),
    ] {
        assert!(resolves(path), "the command tree is missing {path:?}");
    }
}