rskit-git 0.2.0-alpha.2

Composable git repository interfaces backed by libgit2
Documentation
use crate::helpers;

use rskit_errors::ErrorCode;
use rskit_git::cli::GitCli;
use rskit_git::types::ResetMode;
use rskit_git::{
    CheckoutManager, CheckoutOptions, CherryPickOptions, CherryPicker, Executor, GrepOptions,
    IgnoreReader, Inspector, MergeOptions, Merger, RebaseOptions, Rebaser, Resetter, Stasher,
};

#[test]
fn cli_merge_reports_conflicts_covers_squash_and_abort_paths() {
    let repo = helpers::TestRepo::init();
    let main_branch = repo.current_branch();
    let cli = GitCli::new(repo.path().to_path_buf());

    cli.checkout(
        "HEAD",
        Some(&CheckoutOptions {
            create_branch: Some("squash-source".to_string()),
            ..Default::default()
        }),
    )
    .unwrap();
    repo.commit_file("squash.txt", "squash\n", "add squash input");
    cli.checkout(&main_branch, None).unwrap();

    let before_squash = repo.rev_parse("HEAD");
    let squash = cli
        .merge(
            "squash-source",
            Some(&MergeOptions {
                squash: true,
                ..Default::default()
            }),
        )
        .unwrap();
    assert_eq!(squash.head.unwrap().to_string(), before_squash);
    assert!(squash.conflicts.is_empty());
    cli.reset("HEAD", ResetMode::Hard).unwrap();

    cli.checkout(
        "HEAD",
        Some(&CheckoutOptions {
            create_branch: Some("conflict-source".to_string()),
            ..Default::default()
        }),
    )
    .unwrap();
    repo.commit_file("conflict.txt", "source\n", "source conflict");
    cli.checkout(&main_branch, None).unwrap();
    repo.commit_file("conflict.txt", "main\n", "main conflict");

    let conflict = cli.merge("conflict-source", None).unwrap();
    assert_eq!(conflict.head, None);
    assert!(!conflict.fast_forward);
    assert_eq!(conflict.conflicts, vec!["conflict.txt"]);

    cli.abort_merge().unwrap();

    let err = cli.checkout("missing-branch", None).unwrap_err();
    assert_eq!(err.code(), ErrorCode::ExternalService);
    assert_eq!(err.message(), "external service error (git)");
    assert!(
        err.cause()
            .expect("preserve git checkout diagnostics")
            .to_string()
            .contains("missing-branch")
    );
}

#[test]
fn cli_rebase_covers_interactive_continue_conflict_success_and_abort_paths() {
    let interactive_repo = helpers::TestRepo::init();
    interactive_repo.config_set("sequence.editor", ":");
    interactive_repo.config_set("core.editor", ":");
    let interactive_main = interactive_repo.current_branch();
    let interactive_cli = GitCli::new(interactive_repo.path().to_path_buf());
    let interactive = interactive_cli
        .rebase(
            &interactive_main,
            Some(&RebaseOptions {
                interactive: true,
                ..Default::default()
            }),
        )
        .unwrap();
    assert!(interactive.head.is_some());
    assert_eq!(interactive.applied, 0);
    assert!(interactive.conflicts.is_empty());

    let repo = helpers::TestRepo::init();
    repo.config_set("core.editor", ":");
    repo.commit_file("a.txt", "base a\n", "base a");
    repo.commit_file("b.txt", "base b\n", "base b");
    let main_branch = repo.current_branch();
    let cli = GitCli::new(repo.path().to_path_buf());

    cli.checkout(
        "HEAD",
        Some(&CheckoutOptions {
            create_branch: Some("rebase-source".to_string()),
            ..Default::default()
        }),
    )
    .unwrap();
    repo.commit_file("a.txt", "source a\n", "source a");
    repo.commit_file("b.txt", "source b\n", "source b");
    cli.checkout(&main_branch, None).unwrap();
    repo.commit_file("a.txt", "main a\n", "main a");
    repo.commit_file("b.txt", "main b\n", "main b");
    cli.checkout("rebase-source", None).unwrap();

    let first_conflict = cli.rebase(&main_branch, None).unwrap();
    assert_eq!(first_conflict.head, None);
    assert_eq!(first_conflict.applied, 0);
    assert_eq!(first_conflict.conflicts, vec!["a.txt"]);

    std::fs::write(repo.path().join("a.txt"), "resolved a\n").unwrap();
    cli.exec(&["add", "a.txt"]).unwrap();
    let second_conflict = cli.continue_rebase().unwrap();
    assert_eq!(second_conflict.head, None);
    assert_eq!(second_conflict.applied, 0);
    assert_eq!(second_conflict.conflicts, vec!["b.txt"]);

    std::fs::write(repo.path().join("b.txt"), "resolved b\n").unwrap();
    cli.exec(&["add", "b.txt"]).unwrap();
    let completed = cli.continue_rebase().unwrap();
    assert!(completed.head.is_some());
    assert!(completed.conflicts.is_empty());

    let abort_repo = helpers::TestRepo::init();
    abort_repo.commit_file("abort.txt", "base\n", "base abort");
    let abort_main = abort_repo.current_branch();
    let abort_cli = GitCli::new(abort_repo.path().to_path_buf());
    abort_cli
        .checkout(
            "HEAD",
            Some(&CheckoutOptions {
                create_branch: Some("abort-source".to_string()),
                ..Default::default()
            }),
        )
        .unwrap();
    abort_repo.commit_file("abort.txt", "source\n", "source abort");
    abort_cli.checkout(&abort_main, None).unwrap();
    abort_repo.commit_file("abort.txt", "main\n", "main abort");
    abort_cli.checkout("abort-source", None).unwrap();
    assert_eq!(
        abort_cli.rebase(&abort_main, None).unwrap().conflicts,
        vec!["abort.txt"]
    );
    abort_cli.abort_rebase().unwrap();
}

#[test]
fn cli_cherry_pick_covers_mainline_continue_and_abort_paths() {
    let repo = helpers::TestRepo::init();
    repo.config_set("core.editor", ":");
    repo.commit_file("pick.txt", "base\n", "base pick");
    let main_branch = repo.current_branch();
    let cli = GitCli::new(repo.path().to_path_buf());

    let mainline_err = cli
        .cherry_pick(
            "HEAD",
            Some(&CherryPickOptions {
                mainline: Some(1),
                ..Default::default()
            }),
        )
        .unwrap_err();
    assert_eq!(mainline_err.code(), ErrorCode::ExternalService);
    assert_eq!(mainline_err.message(), "external service error (git)");
    assert!(
        mainline_err
            .cause()
            .expect("preserve mainline diagnostics")
            .to_string()
            .contains("cherry-pick")
    );

    cli.checkout(
        "HEAD",
        Some(&CheckoutOptions {
            create_branch: Some("pick-source".to_string()),
            ..Default::default()
        }),
    )
    .unwrap();
    repo.commit_file("pick.txt", "source\n", "source pick");
    let source_commit = repo.rev_parse("HEAD");
    cli.checkout(&main_branch, None).unwrap();
    repo.commit_file("pick.txt", "main\n", "main pick");

    let conflict = cli.cherry_pick(&source_commit, None).unwrap_err();
    assert_eq!(conflict.code(), ErrorCode::ExternalService);
    assert!(
        conflict
            .cause()
            .expect("preserve cherry-pick conflict diagnostics")
            .to_string()
            .contains("CONFLICT")
    );
    std::fs::write(repo.path().join("pick.txt"), "resolved\n").unwrap();
    cli.exec(&["add", "pick.txt"]).unwrap();
    let continued = cli.cherry_pick_continue().unwrap();
    assert_eq!(continued.to_string(), repo.rev_parse("HEAD"));

    let abort_repo = helpers::TestRepo::init();
    abort_repo.commit_file("pick.txt", "base\n", "base pick");
    let abort_main = abort_repo.current_branch();
    let abort_cli = GitCli::new(abort_repo.path().to_path_buf());
    abort_cli
        .checkout(
            "HEAD",
            Some(&CheckoutOptions {
                create_branch: Some("abort-pick-source".to_string()),
                ..Default::default()
            }),
        )
        .unwrap();
    abort_repo.commit_file("pick.txt", "source\n", "source pick");
    let abort_source = abort_repo.rev_parse("HEAD");
    abort_cli.checkout(&abort_main, None).unwrap();
    abort_repo.commit_file("pick.txt", "main\n", "main pick");
    assert_eq!(
        abort_cli
            .cherry_pick(&abort_source, None)
            .unwrap_err()
            .code(),
        ErrorCode::ExternalService
    );
    abort_cli.cherry_pick_abort().unwrap();
}

#[test]
fn cli_grep_and_check_ignore_cover_no_line_numbers_no_matches_and_error_exit() {
    let repo = helpers::TestRepo::init();
    repo.commit_file(
        "docs/notes.txt",
        "Alpha\nkey: value: still content\n",
        "add notes",
    );
    let cli = GitCli::new(repo.path().to_path_buf());

    let matches = cli
        .grep(
            "key",
            "HEAD",
            Some(&GrepOptions {
                pathspecs: vec!["docs/notes.txt".to_string()],
                ..Default::default()
            }),
        )
        .unwrap();
    assert_eq!(matches.len(), 1);
    assert_eq!(matches[0].path, "docs/notes.txt");
    assert_eq!(matches[0].line_number, None);
    assert_eq!(matches[0].line, "key: value: still content");

    let no_match = cli.grep("definitely absent", "HEAD", None).unwrap_err();
    assert_eq!(no_match.code(), ErrorCode::ExternalService);
    assert_eq!(no_match.message(), "external service error (git)");
    assert!(
        no_match
            .cause()
            .expect("preserve grep no-match diagnostics")
            .to_string()
            .contains("grep")
    );

    let empty_dir = helpers::TestRepo::empty_dir();
    let outside_repo = GitCli::new(empty_dir.path().to_path_buf());
    let ignore_err = outside_repo.is_ignored("visible.txt").unwrap_err();
    assert_eq!(ignore_err.code(), ErrorCode::ExternalService);
    assert_eq!(ignore_err.message(), "external service error (git)");
    assert!(
        ignore_err
            .cause()
            .expect("preserve check-ignore diagnostics")
            .to_string()
            .contains("check-ignore")
    );
}

#[test]
fn cli_stash_clean_tree_reports_typed_git_error() {
    let repo = helpers::TestRepo::init();
    let cli = GitCli::new(repo.path().to_path_buf());

    let err = cli.stash("clean tree").unwrap_err();
    assert_eq!(err.code(), ErrorCode::ExternalService);
    assert_eq!(err.message(), "external service error (git)");
    assert!(
        err.cause()
            .expect("preserve stash lookup diagnostics")
            .to_string()
            .contains("stash@{0}")
    );
}