use super::*;
use crate::fs_btrfs_test_support::btrfs_fixture;
use crate::test_support::{git, git_context, remove, repository};
use crate::worktree_ladder_test_support::{main_identity, plan_to, subvolume_repository};
use crate::worktree_snapshot_source::source_refusal;
use std::path::PathBuf;
fn route_of(repo: &Path) -> SlowRoute {
let identity: WorktreeIdentity = main_identity(repo);
let refusal: Refusal = source_refusal(&git_context(), &identity).expect_err("the source is refused");
return explain_refusal(&git_context(), &identity, &refusal).expect("a sentence");
}
#[test]
fn btrfs_a_modified_file_is_an_uncommitted_change() {
let Some(root) = btrfs_fixture("slow-modified") else { return };
let repo: PathBuf = subvolume_repository(root.as_path());
let positive: Result<_, _> = source_refusal(&git_context(), &main_identity(repo.as_path()));
assert!(positive.is_ok(), "positive control: the untouched source is accepted");
std::fs::write(repo.join("shared.txt"), b"edited\n").expect("edit");
let route: SlowRoute = route_of(repo.as_path());
assert_eq!(route.word(), "uncommitted-changes");
assert!(route.line().starts_with(LEAD), "{}", route.line());
assert!(route.line().contains("the main checkout has uncommitted changes in 1 path (for example shared.txt)"), "{}", route.line());
assert!(route.line().contains("Commit or stash them"), "{}", route.line());
assert!(!route.line().contains('\n'), "one line");
remove(root.as_path());
}
#[test]
fn btrfs_untracked_files_are_counted_and_their_names_made_plain() {
let Some(root) = btrfs_fixture("slow-untracked") else { return };
let repo: PathBuf = subvolume_repository(root.as_path());
std::fs::write(repo.join("a\u{1b}[31mred.txt"), b"u").expect("untracked");
std::fs::write(repo.join("b.txt"), b"u").expect("untracked");
let route: SlowRoute = route_of(repo.as_path());
assert!(route.line().contains("uncommitted changes in 2 paths (for example a?[31mred.txt)"), "{}", route.line());
assert!(!route.line().chars().any(char::is_control), "{}", route.line());
remove(root.as_path());
}
#[test]
fn btrfs_a_lock_file_is_a_running_git_command() {
let Some(root) = btrfs_fixture("slow-lock") else { return };
let repo: PathBuf = subvolume_repository(root.as_path());
std::fs::write(repo.join(".git/index.lock"), b"").expect("lock");
let route: SlowRoute = route_of(repo.as_path());
assert_eq!(route.word(), "git-running");
assert!(route.line().contains("another Git command is running in the main checkout (its index.lock exists)"), "{}", route.line());
assert!(route.line().contains("Add again when it has finished"), "{}", route.line());
remove(root.as_path());
}
#[test]
fn btrfs_a_merge_in_progress_is_an_unfinished_operation() {
let Some(root) = btrfs_fixture("slow-merge") else { return };
let repo: PathBuf = subvolume_repository(root.as_path());
let head: String = String::from_utf8_lossy(&git(repo.as_path(), &["rev-parse", "HEAD"]).stdout).trim().to_string();
std::fs::write(repo.join(".git/MERGE_HEAD"), format!("{head}\n")).expect("merge mark");
let route: SlowRoute = route_of(repo.as_path());
assert_eq!(route.word(), "operation-unfinished");
assert!(route.line().contains("MERGE_HEAD exists in its Git directory"), "{}", route.line());
assert!(route.line().contains("Finish or abort it"), "{}", route.line());
remove(root.as_path());
}
#[test]
fn btrfs_a_file_as_new_as_the_index_is_a_stale_record_and_not_a_change() {
let Some(root) = btrfs_fixture("slow-racy") else { return };
let repo: PathBuf = subvolume_repository(root.as_path());
let content: Vec<u8> = std::fs::read(repo.join("shared.txt")).expect("read");
git(repo.as_path(), &["update-index", "--refresh"]);
std::fs::write(repo.join("shared.txt"), content).expect("rewrite");
git(repo.as_path(), &["update-index", "--really-refresh"]);
let index_time = std::fs::metadata(repo.join(".git/index")).expect("index").modified().expect("time");
std::fs::File::options().write(true).open(repo.join("shared.txt")).expect("open").set_modified(index_time).expect("set time");
let route: SlowRoute = route_of(repo.as_path());
assert_eq!(route.word(), "stale-index", "{}", route.line());
assert!(route.line().contains("Run `git status` there once"), "{}", route.line());
assert!(!route.line().contains("uncommitted"), "{}", route.line());
remove(root.as_path());
}
#[test]
fn btrfs_a_source_that_is_no_subvolume_is_told_to_convert_and_another_filesystem_gets_nothing() {
let Some(root) = btrfs_fixture("slow-cheap") else { return };
let repo: PathBuf = std::fs::canonicalize(repository(root.as_path(), "ordinary")).expect("canonical");
let identity: WorktreeIdentity = main_identity(repo.as_path());
let plan = plan_to(root.join("topic"));
let reason: String = crate::worktree_snapshot_source::snapshot_refusal(&identity, &plan).expect("an ordinary directory is refused");
let route: SlowRoute = explain_cheap_refusal(&identity, reason.as_str()).expect("a sentence on btrfs");
assert_eq!(route.word(), "not-subvolume");
assert!(route.line().contains("the main checkout is not a btrfs subvolume. Run `git cli-git migrate-to-subvolume` in it"), "{}", route.line());
let linked: WorktreeIdentity = WorktreeIdentity::LinkedWorktree { common_dir: repo.join(".git"), git_dir: repo.join(".git/worktrees/x"), worktree_root: repo.clone() };
let linked_route: SlowRoute = explain_cheap_refusal(&linked, reason.as_str()).expect("a sentence");
assert!(linked_route.line().contains("this worktree is not a btrfs subvolume. Run `git cli-git convert-worktree-to-subvolume`"), "{}", linked_route.line());
let elsewhere: WorktreeIdentity = WorktreeIdentity::MainWorktree { common_dir: PathBuf::from("/proc/.git"), git_dir: PathBuf::from("/proc/.git"), worktree_root: PathBuf::from("/proc") };
assert!(explain_cheap_refusal(&elsewhere, "not btrfs").is_none(), "nothing the person does makes a snapshot on a filesystem that is not btrfs");
assert!(explain_cheap_refusal(&WorktreeIdentity::OutsideWorktree, "no worktree").is_none());
remove(root.as_path());
}
#[test]
fn unsupported_features_and_failures_are_told_in_the_checks_words() {
let unsupported: SlowRoute = explain_unsupported("core.sparseCheckout is set");
assert_eq!(unsupported.line(), format!("{LEAD}core.sparseCheckout is set."));
assert_eq!(unsupported.word(), "unsupported");
assert_eq!(unsupported.detail(), "core.sparseCheckout is set");
let failed: SlowRoute = explain_failure("the stage could not be swapped into place");
assert_eq!(failed.word(), "snapshot-failed");
assert!(failed.line().contains("the snapshot could not be finished (the stage could not be swapped into place), so Git's own checkout made the worktree."), "{}", failed.line());
for line in [unsupported.line(), failed.line()] {
assert!(!line.to_lowercase().contains("rung"), "{line}");
}
}
#[test]
fn a_failed_status_is_told_with_its_error_and_the_remedies() {
let root: PathBuf = crate::test_support::fixture("slow-status-failed");
let repo: PathBuf = repository(root.as_path(), "repo");
let identity: WorktreeIdentity = main_identity(repo.as_path());
let stub: PathBuf = root.join("failing-git");
crate::test_support::executable(stub.as_path(), b"#!/bin/sh\necho 'fatal: index file corrupt' >&2\nexit 128\n");
let failing = crate::transaction_git::GitContext { real_git: stub, ..git_context() };
let failed: Refusal = Refusal { reason: String::from("`git status` ended with exit code 128: fatal: index file corrupt"), kind: RefusalKind::StatusFailed };
let route: SlowRoute = explain_refusal(&failing, &identity, &failed).expect("a sentence");
assert_eq!(route.word(), "status-failed");
let line: String = route.line();
assert!(line.contains("the main checkout (`git status` ended with exit code 128: fatal: index file corrupt)"), "{line}");
assert!(line.contains(format!("Run `git -C {} status` to read its error", repo.display()).as_str()) && line.contains("wait if another Git command is running"), "{line}");
let racy: Refusal = Refusal { reason: String::from("a.txt is as new as the index, so its stat fields prove nothing"), kind: RefusalKind::Unclean(CleanRefusalKind::Racy) };
let from_unclean: SlowRoute = explain_refusal(&failing, &identity, &racy).expect("a sentence");
assert_eq!(from_unclean.word(), "status-failed");
assert!(from_unclean.line().contains("exit code 128"), "{}", from_unclean.line());
assert!(!from_unclean.line().contains("out of date"), "{}", from_unclean.line());
remove(root.as_path());
}