use std::fs;
use std::path::Path;
use std::process::Command;
use grit_lib::porcelain::status::{status, StatusOptions, UntrackedMode};
use grit_lib::progress::NeverCancel;
use grit_lib::progress::NullProgress;
use grit_lib::repo::{InitOptions, Repository};
use grit_lib::PARALLEL_STAT_MIN_ENTRIES;
fn assert_git_diff_files_clean(repo_root: &Path) {
let root = repo_root.to_str().expect("utf-8 path");
let _ = Command::new("git")
.args(["-C", root, "update-index", "--refresh"])
.status()
.expect("spawn git update-index --refresh");
let status = Command::new("git")
.args(["-C", root, "diff-files", "--quiet"])
.status()
.expect("spawn git diff-files");
if !status.success() {
let detail = Command::new("git")
.args(["-C", root, "diff-files"])
.output()
.expect("spawn git diff-files");
panic!(
"git diff-files --quiet must succeed after grit index refresh:\n{}",
String::from_utf8_lossy(&detail.stdout)
);
}
}
fn diff_paths(model: &grit_lib::porcelain::status::StatusModel) -> (Vec<String>, Vec<String>) {
let mut staged: Vec<_> = model.staged.iter().map(|e| e.path().to_owned()).collect();
let mut unstaged: Vec<_> = model.unstaged.iter().map(|e| e.path().to_owned()).collect();
staged.sort();
unstaged.sort();
(staged, unstaged)
}
fn append_repo_config(repo: &Repository, section: &str, key: &str, value: &str) {
let path = repo.git_dir().join("config");
let mut text = fs::read_to_string(&path).unwrap_or_default();
if !text.ends_with('\n') {
text.push('\n');
}
text.push_str(&format!("[{section}]\n\t{key} = {value}\n"));
fs::write(&path, text).expect("write config");
repo.reload_config().expect("reload config");
}
fn seed_repo(root: &Path, preload_index: bool) -> Repository {
let repo = Repository::init(
root,
&InitOptions::new()
.with_initial_branch("main".to_owned())
.with_ref_storage(grit_lib::RefStorageFormat::Files),
)
.expect("init");
if !preload_index {
append_repo_config(&repo, "core", "preloadIndex", "false");
}
fs::create_dir_all(root.join("mix")).expect("mkdir");
for i in 0..5000_usize {
let rel = format!("mix/{i:05}.txt");
fs::write(root.join(&rel), format!("payload {i}\n")).expect("write");
}
grit_lib::porcelain::add::stage(
&repo,
&grit_lib::porcelain::add::StageOptions::default(),
&mut NullProgress,
)
.expect("stage");
let req = grit_lib::porcelain::commit::CommitRequest {
message: "seed".into(),
author: Some("T <t@e.com> 1 +0000".parse().expect("signature")),
committer: Some("T <t@e.com> 1 +0000".parse().expect("signature")),
allow_empty: false,
sign_override: None,
amend: false,
assume_worktree_synced: false,
skip_pre_commit_tree_match: false,
staged_change_count: None,
..Default::default()
};
grit_lib::porcelain::commit::create_commit(&repo, &req, &mut NullProgress).expect("commit");
let now = filetime::FileTime::now();
for rel in ["mix/00030.txt", "mix/00040.txt", "mix/00050.txt"] {
filetime::set_file_mtime(root.join(rel), now).expect("touch");
}
repo
}
fn run_status(
repo: &Repository,
threads: usize,
) -> (grit_lib::porcelain::status::StatusModel, Vec<u8>) {
let mut opts = StatusOptions::default();
opts.untracked = UntrackedMode::No;
opts.ahead_behind = false;
opts.stat_parallel_threads = Some(threads);
let model = status(repo, &opts, &mut NullProgress, &NeverCancel).expect("status");
let index_bytes = fs::read(repo.index_handle().path()).expect("index bytes");
(model, index_bytes)
}
#[test]
fn parallel_refresh_matches_serial() {
assert!(PARALLEL_STAT_MIN_ENTRIES <= 5000);
let tmp = tempfile::tempdir().expect("tempdir");
let repo = seed_repo(tmp.path(), true);
let baseline_index = fs::read(repo.index_handle().path()).expect("baseline index");
let (serial_model, serial_index) = {
fs::write(repo.index_handle().path(), &baseline_index).expect("restore index");
run_status(&repo, 1)
};
let (parallel_model, parallel_index) = {
fs::write(repo.index_handle().path(), &baseline_index).expect("restore index");
run_status(&repo, 4)
};
assert_eq!(diff_paths(&serial_model), diff_paths(¶llel_model));
assert_eq!(serial_index, parallel_index);
}
#[test]
fn refreshed_index_matches_git_worktree() {
assert!(PARALLEL_STAT_MIN_ENTRIES <= 5000);
let tmp = tempfile::tempdir().expect("tempdir");
let repo = seed_repo(tmp.path(), true);
let _model = run_status(&repo, 4).0;
assert_git_diff_files_clean(tmp.path());
}