grit-lib 0.5.20

Core library for the grit Git implementation
Documentation
//! Typed errors for common embedder failures (issue #1685).

use grit_lib::blame::{self, compute_blame, BlameDiffAlgorithm};
use grit_lib::error::Error;
use grit_lib::objects::ObjectId;
use grit_lib::parallel::ThreadPolicy;
use grit_lib::porcelain::commit::{create_commit, CommitRequest};
use grit_lib::porcelain::staging::stage_worktree_changes;
use grit_lib::porcelain::status::{status, StatusOptions};
use grit_lib::progress::NullProgress;
use grit_lib::repo::{InitOptions, Repository};
use grit_lib::revwalk::rev_parse_error::RevParseError;
use std::collections::{HashMap, HashSet};
fn assert_requires_work_tree(result: Result<(), Error>) {
    assert!(
        matches!(result, Err(Error::RequiresWorkTree)),
        "expected RequiresWorkTree, got {result:?}"
    );
}

#[test]
fn bare_repo_common_failures_use_requires_work_tree() {
    let base = tempfile::tempdir().expect("tempdir");
    let bare = Repository::init(
        &base.path().join("bare"),
        &InitOptions::default().with_bare(true),
    )
    .expect("init bare");

    assert_requires_work_tree(
        status(&bare, &StatusOptions::default(), &mut NullProgress).map(|_| ()),
    );
    assert_requires_work_tree(stage_worktree_changes(&bare, &[]).map(|_| ()));

    let mut idx = bare.index_handle().load().expect("index");
    assert_requires_work_tree(
        bare.diff()
            .worktree_to_index(&mut idx, false, false)
            .map(|_| ()),
    );
}

fn repo_with_one_commit() -> (tempfile::TempDir, Repository, ObjectId) {
    let base = tempfile::tempdir().expect("tempdir");
    let repo = Repository::init(
        &base.path().join("w"),
        &InitOptions::default().with_initial_branch("main"),
    )
    .expect("init");
    std::fs::write(base.path().join("w/a"), "x\n").expect("write");
    stage_worktree_changes(&repo, &[]).expect("stage");
    let req = CommitRequest {
        message: "m\n".into(),
        author: "A <a@b> 0 +0000".into(),
        committer: "A <a@b> 0 +0000".into(),
        ..Default::default()
    };
    let outcome = create_commit(&repo, &req, &mut NullProgress).expect("commit");
    (base, repo, outcome.oid)
}

fn assert_blame_path_not_in_revision(err: Error, expected_path: &str, expected_revision: &str) {
    assert!(
        matches!(
            err,
            Error::RevParse(RevParseError::PathNotInRevision { ref path, ref revision, .. })
                if path == expected_path && revision == expected_revision
        ),
        "got {err:?}"
    );
}

#[test]
fn blame_file_missing_path_is_path_not_in_revision() {
    let (_base, repo, _head) = repo_with_one_commit();
    let err = blame::blame_file(&repo, "nope.txt", &Default::default()).unwrap_err();
    assert_blame_path_not_in_revision(err, "nope.txt", "HEAD");
}

#[test]
fn compute_blame_missing_path_is_path_not_in_revision() {
    let (_base, repo, head) = repo_with_one_commit();
    let err = compute_blame(
        repo.odb(),
        head,
        "missing.txt",
        &HashSet::new(),
        BlameDiffAlgorithm::Myers,
        None,
        false,
        0,
        false,
        &HashMap::new(),
        false,
        None,
        grit_lib::diff::WhitespaceMode::default(),
        &ThreadPolicy::default(),
    )
    .unwrap_err();
    assert_blame_path_not_in_revision(err, "missing.txt", &head.to_hex());
}

#[test]
fn deep_parent_navigation_is_revision_out_of_range() {
    let base = tempfile::tempdir().expect("tempdir");
    let repo = Repository::init(
        &base.path().join("w"),
        &InitOptions::default().with_initial_branch("main"),
    )
    .expect("init");
    std::fs::write(base.path().join("w/a"), "x\n").expect("write");
    stage_worktree_changes(&repo, &[]).expect("stage");
    let req = CommitRequest {
        message: "m\n".into(),
        author: "A <a@b> 0 +0000".into(),
        committer: "A <a@b> 0 +0000".into(),
        ..Default::default()
    };
    create_commit(&repo, &req, &mut NullProgress).expect("commit");

    let err = repo.rev_walk().resolve("main~99").unwrap_err();
    assert!(
        matches!(
            err,
            Error::RevParse(RevParseError::RevisionOutOfRange { ref spec, .. }) if spec == "main~99"
        ),
        "got {err:?}"
    );
}

#[test]
fn empty_commit_message_is_typed() {
    let base = tempfile::tempdir().expect("tempdir");
    let repo = Repository::init(
        &base.path().join("w"),
        &InitOptions::default().with_initial_branch("main"),
    )
    .expect("init");
    std::fs::write(base.path().join("w/a"), "x\n").expect("write");
    stage_worktree_changes(&repo, &[]).expect("stage");
    let req = CommitRequest {
        message: "m\n".into(),
        author: "A <a@b> 0 +0000".into(),
        committer: "A <a@b> 0 +0000".into(),
        ..Default::default()
    };
    create_commit(&repo, &req, &mut NullProgress).expect("commit");

    let empty = CommitRequest {
        message: String::new(),
        allow_empty: true,
        ..req
    };
    let err = create_commit(&repo, &empty, &mut NullProgress).unwrap_err();
    assert!(matches!(err, Error::EmptyCommitMessage), "got {err:?}");
}