diffler-core 0.17.0

Core review engine for diffler: diffs, sessions, comments, viewed marks
Documentation
//! Git2 repo scaffolding shared by fixture builders in this crate's and
//! diffler's tests. Feature-gated so it never ships in a normal build.

// fixture helpers run outside #[test] fns, where clippy's test allowances don't reach
#![allow(clippy::expect_used)]

use std::path::Path;

pub fn init_repo(root: &Path, initial_branch: Option<&str>) -> git2::Repository {
    let repo = match initial_branch {
        Some(branch) => {
            let mut options = git2::RepositoryInitOptions::new();
            options.initial_head(branch);
            git2::Repository::init_opts(root, &options).expect("init")
        }
        None => git2::Repository::init(root).expect("init"),
    };
    let mut config = repo.config().expect("config");
    config.set_str("user.name", "test").expect("config");
    config.set_str("user.email", "test@test").expect("config");
    // pin line endings so checkout restores exact bytes across platforms
    config.set_str("core.autocrlf", "false").expect("config");
    config.set_str("core.eol", "lf").expect("config");
    repo
}

/// Give `branch` an upstream at revision `at`. The remote-tracking ref is
/// written locally, which is all git needs to count divergence.
pub fn track(repo: &git2::Repository, branch: &str, at: &str) {
    // set_upstream resolves the ref back to a remote, so one has to exist
    if repo.find_remote("origin").is_err() {
        repo.remote("origin", "https://example.invalid/repo.git")
            .expect("remote");
    }
    let oid = repo.revparse_single(at).expect("rev").id();
    repo.reference(&format!("refs/remotes/origin/{branch}"), oid, true, "track")
        .expect("remote ref");
    repo.find_branch(branch, git2::BranchType::Local)
        .expect("branch")
        .set_upstream(Some(&format!("origin/{branch}")))
        .expect("upstream");
}

pub fn commit_all(repo: &git2::Repository, message: &str, sig: &git2::Signature<'_>) {
    let mut index = repo.index().expect("index");
    index
        .add_all(["*"].iter(), git2::IndexAddOption::DEFAULT, None)
        .expect("add");
    index.write().expect("index write");
    let tree_id = index.write_tree().expect("tree");
    let tree = repo.find_tree(tree_id).expect("find tree");
    let parent = repo.head().ok().and_then(|h| h.peel_to_commit().ok());
    let parents: Vec<&git2::Commit<'_>> = parent.iter().collect();
    repo.commit(Some("HEAD"), sig, sig, message, &tree, &parents)
        .expect("commit");
}

/// Colocate the git repo at `root` with jj. jj keeps an identity apart from
/// git's and warns on every write without one, so it gets the mock one here.
pub fn colocate_jj(root: &Path) {
    jj(root, &["git", "init", "--colocate"]);
    jj(root, &["config", "set", "--repo", "user.name", "reviewer"]);
    jj(
        root,
        &[
            "config",
            "set",
            "--repo",
            "user.email",
            "reviewer@example.com",
        ],
    );
}

/// Run a jj subcommand in `root` and return its trimmed stdout, panicking on
/// failure so a broken setup step fails at its call site. `HOME` points at a
/// throwaway directory beside `root`, isolated from the developer's own jj
/// config (`ui.editor`, `snapshot.auto-track`, signing, aliases): a test
/// must not change behaviour depending on who runs it. Placed beside `root`,
/// so `.config/jj/` never shows up as untracked content in the very tree the
/// test is reviewing.
pub fn jj(root: &Path, args: &[&str]) -> String {
    let home = root.parent().expect("root has a parent").join("jjhome");
    std::fs::create_dir_all(&home).expect("jj home");
    let output = std::process::Command::new("jj")
        .current_dir(root)
        .env("HOME", &home)
        .args(args)
        .output()
        .expect("run jj");
    assert!(
        output.status.success(),
        "jj {args:?} failed: {}",
        String::from_utf8_lossy(&output.stderr)
    );
    String::from_utf8_lossy(&output.stdout).trim().to_owned()
}