mergiraf 0.19.1

A syntax-aware merge driver for Git
Documentation
#![allow(dead_code, reason = "the functions do get used in integration tests")]

use core::str;
use std::fs::{self, read_to_string};
use std::path::{Path, PathBuf};
use std::process::{Command, Output};

use assert_cmd::{pkg_name, prelude::*};
use itertools::Itertools;
use mergiraf::lang_profile::LangProfile;

pub const DEFAULT_FILE_FOR_SOLVE: &str =
    "<<<<<<< LEFT\n[1, 2, 3, 4]\n||||||| BASE\n[1, 2, 3]\n=======\n[0, 1, 2, 3]\n>>>>>>> RIGHT\n";

pub(crate) fn run_git(args: &[&str], repo_dir: &Path) -> Output {
    let command_str = format!("git {}", args.iter().format(" "));
    let mut command = git_command(args, repo_dir);
    let output = command.output().expect("Failed to execute git command");
    if !output.status.success() {
        panic!(
            "git command failed: {command_str}\n{}",
            str::from_utf8(&output.stdout).unwrap()
        );
    }
    output
}

pub(crate) fn git_command(args: &[&str], repo_dir: &Path) -> Command {
    let mut command = Command::new("git");
    command.current_dir(repo_dir);
    command.args(args);
    // Run using a minimal environment to isolate the test better.
    command
        .env_clear()
        .envs(std::env::vars().filter(|(var, _)| var == "PATH"));
    command
}

/// Given a path to a repo, set up mergiraf as merge driver
pub(crate) fn setup_mergiraf(repo_path: &Path) {
    let mergiraf_command = Command::cargo_bin(pkg_name!()).unwrap();
    let mergiraf_binary = mergiraf_command.get_program().to_string_lossy();

    run_git(&["config", "user.email", "test@example.com"], repo_path);
    run_git(&["config", "user.name", "Test User"], repo_path);
    run_git(&["config", "merge.mergiraf.name", "mergiraf"], repo_path);
    run_git(
        &[
            "config",
            "merge.mergiraf.driver",
            &format!("{mergiraf_binary} merge --git %O %A %B -s %S -x %X -y %Y -p %P -l %L"),
        ],
        repo_path,
    );

    fs::write(repo_path.join(".git/info/attributes"), "* merge=mergiraf\n")
        .expect("failed to write .gitattributes");
}

pub(crate) fn write_file_from_rev(
    repo_dir: &Path,
    test_dir: &Path,
    revision: &str,
    suffix: &str,
) -> PathBuf {
    let file_name = format!("file{suffix}");
    let fname_base = test_dir.join(format!("{revision}{suffix}"));
    let contents = fs::read_to_string(&fname_base).expect("Unable to read left file");
    fs::write(repo_dir.join(&file_name), contents)
        .expect("failed to write test file to git repository");
    PathBuf::from(file_name)
}

/// Detect the suffix (including period) used by the revision files in a test case,
/// if any. Test files without extensions should declare the language to use in
/// a separate `language` file and just use bare `Base`, `Left` and `Right`
/// revision files (and similarly for expected outputs).
pub(crate) fn detect_test_suffix(test_dir: &Path) -> String {
    mergiraf::utils::detect_suffix(test_dir)
}

/// Returns the language name specified in a test case (if any).
/// This is the contents of the `language` file in the test directory.
pub(crate) fn language_override_for_test(test_dir: &Path) -> Option<&'static str> {
    let contents = read_to_string(test_dir.join("language")).ok()?;
    let language_name = contents.trim();
    let lang_profile = LangProfile::find_by_name(language_name)
        .unwrap_or_else(|| panic!("Invalid identifier in 'language' file: '{language_name:?}'"));
    Some(lang_profile.name)
}

#[track_caller]
pub fn merge() -> Command {
    let mut cmd = Command::cargo_bin(pkg_name!()).unwrap();
    cmd.arg("merge");
    cmd
}

#[track_caller]
pub fn solve() -> Command {
    let mut cmd = Command::cargo_bin(pkg_name!()).unwrap();
    cmd.arg("solve");
    cmd
}

pub fn create_file_for_solve(repo_path: &Path, contents: impl AsRef<[u8]>) -> PathBuf {
    let test_file_name = "test.txt";
    let test_file_abs_path = repo_path.join(test_file_name);
    fs::write(&test_file_abs_path, contents).expect("failed to write test file to git repository");

    test_file_abs_path
}

pub fn create_files_for_merge(
    repo_path: &Path,
    base_contents: impl AsRef<[u8]>,
    left_contents: impl AsRef<[u8]>,
    right_contents: impl AsRef<[u8]>,
) -> (PathBuf, PathBuf, PathBuf, PathBuf) {
    let base_file_name = "base.txt";
    let left_file_name = "left.txt";
    let right_file_name = "right.txt";
    let output_file_name = "output.txt";

    let base_file_abs_path = repo_path.join(base_file_name);
    fs::write(&base_file_abs_path, base_contents)
        .expect("failed to write test base file to git repository");
    let left_file_abs_path = repo_path.join(left_file_name);
    fs::write(&left_file_abs_path, left_contents)
        .expect("failed to write test left file to git repository");
    let right_file_abs_path = repo_path.join(right_file_name);
    fs::write(&right_file_abs_path, right_contents)
        .expect("failed to write test right file to git repository");
    let output_file_abs_path = repo_path.join(output_file_name);

    (
        base_file_abs_path,
        left_file_abs_path,
        right_file_abs_path,
        output_file_abs_path,
    )
}