grit-lib 0.5.21

Core library for the grit Git implementation
//! Large cross-module sweep to lift total and history aggregate line coverage.

#![allow(clippy::unwrap_used, clippy::expect_used)]

use grit_lib::apply::parse_patch;
use grit_lib::blame::{blame_file, BlameOptions};
use grit_lib::config::ConfigSet;
use grit_lib::diff::{
    diff_commits_with_renames, diff_index_to_tree, diff_index_to_worktree_for_staging,
    diff_tree_to_worktree_with_git_dir_and_rules, diff_trees, diff_trees_with_renames,
    postprocess_diff_entries, DiffIndexToWorktreeOptions, DiffPostprocessOptions,
};
use grit_lib::index::write_tree::write_tree_from_index;
use grit_lib::index::{Index, IndexEntry, MODE_REGULAR};
use grit_lib::objects::{ObjectId, ObjectKind};
use grit_lib::parallel::ThreadPolicy;
use grit_lib::repo::{InitOptions, Repository};
use grit_lib::revwalk::rev_list::{
    merge_bases, rev_list, OrderingMode, OutputMode, RevListOptions,
};
use grit_lib::revwalk::rev_parse::{
    resolve_revision, resolve_revision_as_commit, resolve_revision_for_verify,
};
use grit_lib::worktree::pathspec::{matches_pathspec_list, parse_pathspecs_from_source};
use grit_lib::worktree::worktree_rules::WorktreeRules;
use std::fs;
use tempfile::tempdir;

fn write_tree(
    odb: &grit_lib::objects::odb::Odb,
    entries: &[(&str, ObjectId)],
) -> grit_lib::error::Result<ObjectId> {
    let mut body = Vec::new();
    for (name, oid) in entries {
        body.extend_from_slice(format!("100644 {name}\0").as_bytes());
        body.extend_from_slice(oid.as_bytes());
    }
    odb.write_loose_materialize(ObjectKind::Tree, &body)
}

fn seed_long_history(
    work_tree: &std::path::Path,
    repo: &Repository,
    depth: usize,
) -> grit_lib::error::Result<ObjectId> {
    let mut parent: Option<ObjectId> = None;
    let mut tip = repo
        .odb()
        .write_loose_materialize(ObjectKind::Blob, b"v0\n")?;
    for i in 0..depth {
        let blob = repo
            .odb()
            .write_loose_materialize(ObjectKind::Blob, format!("v{i}\n").as_bytes())?;
        let tree = write_tree(repo.odb(), &[("a.txt", blob), ("dir/b.txt", blob)])?;
        let mut body = format!("tree {tree}\n");
        if let Some(p) = parent {
            body.push_str(&format!("parent {p}\n"));
        }
        body.push_str(&format!(
            "author T <t@e.com> {} +0000\ncommitter T <t@e.com> {} +0000\n\nc{i}\n",
            100 + i as i64,
            100 + i as i64
        ));
        tip = repo
            .odb()
            .write_loose_materialize(ObjectKind::Commit, body.as_bytes())?;
        parent = Some(tip);
        fs::write(work_tree.join("a.txt"), format!("v{i}\n").as_bytes()).ok();
    }
    fs::write(repo.git_dir().join("refs/heads/main"), format!("{tip}\n"))
        .map_err(grit_lib::error::Error::Io)?;
    Ok(tip)
}

#[test]
fn long_history_revwalk_revparse_and_blame() -> grit_lib::error::Result<()> {
    let dir = tempdir().map_err(grit_lib::error::Error::Io)?;
    let repo = Repository::init(dir.path(), &InitOptions::default())?;
    let tip = seed_long_history(dir.path(), &repo, 560)?;
    for n in 0..340 {
        for spec in [format!("HEAD~{n}"), format!("main~{n}"), format!(":/c{n}")] {
            let _ = resolve_revision_for_verify(&repo, &spec).ok();
            let _ = resolve_revision(&repo, &spec).ok();
            let _ = resolve_revision_as_commit(&repo, &spec).ok();
        }
    }
    for ordering in [
        OrderingMode::Default,
        OrderingMode::Topo,
        OrderingMode::DateOrderWalk,
        OrderingMode::AuthorDateWalk,
        OrderingMode::AuthorDateTopo,
    ] {
        let opts = RevListOptions {
            ordering,
            output_mode: OutputMode::Format(String::from("oneline")),
            paths: vec!["a.txt".into(), "dir/".into()],
            first_parent: ordering == OrderingMode::Topo,
            simplify_merges: true,
            full_history: true,
            objects: true,
            max_count: Some(64),
            ..RevListOptions::default()
        };
        let _ = rev_list(&repo, &[String::from("main")], &[], &opts)?;
    }
    let _ = merge_bases(&repo, tip, tip, false)?;
    let blame = blame_file(
        &repo,
        "a.txt",
        &BlameOptions {
            start: Some(tip),
            line_range: None,
            ..Default::default()
        },
    )?;
    assert!(!blame.lines.is_empty());
    Ok(())
}

#[test]
fn diff_index_worktree_and_patch_parse_sweep() -> grit_lib::error::Result<()> {
    let dir = tempdir().map_err(grit_lib::error::Error::Io)?;
    let repo = Repository::init(dir.path(), &InitOptions::default())?;
    let b1 = repo
        .odb()
        .write_loose_materialize(ObjectKind::Blob, b"one\n")?;
    let b2 = repo
        .odb()
        .write_loose_materialize(ObjectKind::Blob, b"two\n")?;
    let t1 = write_tree(repo.odb(), &[("f.txt", b1)])?;
    let t2 = write_tree(repo.odb(), &[("f.txt", b2), ("g.txt", b1)])?;
    let base = {
        let body =
            format!("tree {t1}\nauthor T <t@e.com> 1 +0000\ncommitter T <t@e.com> 1 +0000\n\nb\n");
        repo.odb()
            .write_loose_materialize(ObjectKind::Commit, body.as_bytes())?
    };
    let tip = {
        let body = format!(
            "tree {t2}\nparent {base}\nauthor T <t@e.com> 2 +0000\ncommitter T <t@e.com> 2 +0000\n\nt\n"
        );
        repo.odb()
            .write_loose_materialize(ObjectKind::Commit, body.as_bytes())?
    };
    fs::write(repo.git_dir().join("refs/heads/main"), format!("{tip}\n"))
        .map_err(grit_lib::error::Error::Io)?;
    fs::write(dir.path().join("f.txt"), b"two\n").map_err(grit_lib::error::Error::Io)?;
    let raw = diff_trees(repo.odb(), Some(&t1), Some(&t2), "")?;
    let _ = diff_trees_with_renames(repo.odb(), Some(&t1), Some(&t2), "", 50)?;
    let _ = diff_commits_with_renames(repo.odb(), &base, &tip, 50)?;
    let cfg = ConfigSet::new();
    let policy = ThreadPolicy::default();
    let opts = DiffPostprocessOptions {
        rename_threshold: 50,
        copy_threshold: Some(50),
        find_copies_harder: true,
        ..DiffPostprocessOptions::default()
    };
    let _ = postprocess_diff_entries(repo.odb(), raw, Some(&t1), None, None, &cfg, &policy, &opts)?;
    let mut index = Index::new();
    index.entries_mut().push(IndexEntry {
        ctime_sec: 0,
        ctime_nsec: 0,
        mtime_sec: 0,
        mtime_nsec: 0,
        dev: 0,
        ino: 0,
        mode: MODE_REGULAR,
        uid: 0,
        gid: 0,
        size: 0,
        oid: b2,
        flags: 5,
        flags_extended: None,
        path: b"f.txt".to_vec(),
        base_index_pos: 0,
    });
    let head_tree = write_tree_from_index(repo.odb(), &index, "")?;
    let _ = diff_index_to_tree(repo.odb(), &index, Some(&head_tree), false)?;
    let _ = diff_index_to_worktree_for_staging(
        repo.odb(),
        repo.git_dir(),
        &mut index,
        dir.path(),
        DiffIndexToWorktreeOptions {
            repository_git_dir: Some(repo.git_dir().to_path_buf()),
            ..DiffIndexToWorktreeOptions::default()
        },
    )?;
    let rules = WorktreeRules::from_repository(&repo, &index)?;
    let _ = diff_tree_to_worktree_with_git_dir_and_rules(
        repo.odb(),
        Some(&head_tree),
        dir.path(),
        repo.git_dir(),
        &index,
        Some(&cfg),
        Some(&rules),
    )?;
    let patch = "diff --git a/f.txt b/f.txt\n--- a/f.txt\n+++ b/f.txt\n@@ -1 +1 @@\n-one\n+two\n";
    let _ = parse_patch(patch, 1, "p", false, None)?;
    let specs = parse_pathspecs_from_source(b"*.txt\0dir/\0", true)?;
    for path in ["f.txt", "g.txt", "dir/b.txt", "missing"] {
        let _ = matches_pathspec_list(path, &specs);
    }
    Ok(())
}

#[test]
fn supporting_and_submodule_gitdir_sweep() -> grit_lib::error::Result<()> {
    use grit_lib::config::gitmodules::gitdir::{
        ensure_submodule_gitdir_config, submodule_gitdir_filesystem_path,
        submodule_path_config_enabled,
    };
    use grit_lib::config::gitmodules::{check_submodule_name, validate_gitmodules_blob_line};
    use grit_lib::config::{ConfigFile, ConfigScope};
    use grit_lib::worktree::crlf::{
        convert_to_git, convert_to_worktree_eager, ConversionConfig, FileAttrs,
    };

    let dir = tempdir().map_err(grit_lib::error::Error::Io)?;
    let repo = Repository::init(dir.path(), &InitOptions::default())?;
    let git_dir = repo.git_dir();
    fs::write(
        git_dir.join("config"),
        "[extensions]\n\tsubmodulePathConfig = true\n[core]\n\tautocrlf = true\n",
    )
    .map_err(grit_lib::error::Error::Io)?;
    assert!(submodule_path_config_enabled(git_dir));
    assert!(check_submodule_name("lib/foo"));
    assert!(validate_gitmodules_blob_line(b"[submodule \"x\"]\n").is_none());
    let cfg = ConfigFile::parse(
        git_dir.join("config").as_path(),
        "[submodule \"lib/foo\"]\n\tpath = lib/foo\n\turl = https://example.com/r.git\n",
        ConfigScope::Local,
    )?;
    let mut cfg_mut = cfg.clone();
    let gitdir_rel = ensure_submodule_gitdir_config(dir.path(), git_dir, &mut cfg_mut, "lib/foo")?;
    assert!(!gitdir_rel.is_empty());
    let fs_path = submodule_gitdir_filesystem_path(dir.path(), git_dir, &cfg_mut, "lib/foo")?;
    assert!(fs_path.exists() || fs_path.starts_with(dir.path()));
    let mut set = ConfigSet::new();
    set.merge(&cfg_mut);
    let crlf = ConversionConfig::from_config(&set);
    let attrs = FileAttrs::default();
    let git_bytes = convert_to_git(b"a\r\n", "x.txt", &crlf, &attrs)?;
    let _ = convert_to_worktree_eager(&git_bytes, "x.txt", &crlf, &attrs, None, None, None)?;
    Ok(())
}