use super::*;
use crate::test_support::{fixture, git, git_output, git_text, remove, repository};
use std::path::PathBuf;
use std::process::Output;
pub(crate) struct Probe {
pub(crate) git_dir: PathBuf,
pub(crate) common_dir: PathBuf,
pub(crate) object_format: String,
pub(crate) ref_format: RefFormat,
}
pub(crate) fn probe(directory: &Path) -> Probe {
let git_dir: String = git_text(directory, &["rev-parse", "--absolute-git-dir"]);
let common: String = git_text(directory, &["rev-parse", "--path-format=absolute", "--git-common-dir"]);
let reftable: bool = git_text(directory, &["rev-parse", "--show-ref-format"]) == "reftable";
return Probe {
git_dir: PathBuf::from(git_dir),
common_dir: PathBuf::from(common),
object_format: git_text(directory, &["rev-parse", "--show-object-format"]),
ref_format: if reftable { RefFormat::Reftable } else { RefFormat::Files },
};
}
pub(crate) fn oracle(directory: &Path) -> Result<SymbolicHead, Option<i32>> {
let output: Output = git_output(directory, &["symbolic-ref", "--quiet", "HEAD"]);
return match output.status.code() {
Some(0) => Ok(SymbolicHead::Branch(
String::from_utf8(output.stdout).expect("UTF-8").trim().to_string(),
)),
Some(1) => Ok(SymbolicHead::Detached),
other => Err(other),
};
}
fn compared(directory: &Path, found: &Probe, label: &str) -> Option<SymbolicHead> {
let layout: RefFilesLayout<'_> = RefFilesLayout {
git_dir: found.git_dir.as_path(),
common_dir: found.common_dir.as_path(),
ref_format: found.ref_format,
object_format: found.object_format.as_str(),
};
let mine: Option<SymbolicHead> = read_symbolic_head(&layout);
if let Some(answer) = &mine {
assert_eq!(&oracle(directory), &Ok(answer.clone()), "{label}");
}
return mine;
}
fn write_head(found: &Probe, bytes: &[u8]) {
std::fs::write(found.git_dir.join("HEAD"), bytes).expect("fixture");
}
#[test]
fn plain_spellings_are_answered_as_git_answers() {
let root: PathBuf = fixture("ref-files-plain");
let repo: PathBuf = repository(root.as_path(), "repo");
let found: Probe = probe(repo.as_path());
let head: String = git_text(repo.as_path(), &["rev-parse", "HEAD"]);
assert_eq!(compared(repo.as_path(), &found, "born main"), Some(SymbolicHead::Branch(String::from("refs/heads/main"))));
for name in ["feature/deep/name", "release-1.2_x", "a.b", "HEAD", "-dash"] {
let full: String = format!("refs/heads/{name}");
git(repo.as_path(), &["update-ref", full.as_str(), head.as_str()]);
write_head(&found, format!("ref: {full}\n").as_bytes());
assert_eq!(compared(repo.as_path(), &found, name), Some(SymbolicHead::Branch(full)));
}
write_head(&found, b"ref: refs/heads/not-born-yet\n");
assert_eq!(
compared(repo.as_path(), &found, "unborn"),
Some(SymbolicHead::Branch(String::from("refs/heads/not-born-yet")))
);
write_head(&found, format!("{head}\n").as_bytes());
assert_eq!(compared(repo.as_path(), &found, "detached"), Some(SymbolicHead::Detached));
remove(root.as_path());
}
#[test]
fn a_packed_only_branch_is_answered() {
let root: PathBuf = fixture("ref-files-packed");
let repo: PathBuf = repository(root.as_path(), "repo");
let found: Probe = probe(repo.as_path());
git(repo.as_path(), &["pack-refs", "--all", "--prune"]);
assert!(!probe(repo.as_path()).common_dir.join("refs/heads/main").exists());
assert_eq!(compared(repo.as_path(), &found, "packed"), Some(SymbolicHead::Branch(String::from("refs/heads/main"))));
remove(root.as_path());
}
#[test]
fn other_spellings_are_left_to_git() {
let root: PathBuf = fixture("ref-files-other");
let repo: PathBuf = repository(root.as_path(), "repo");
let found: Probe = probe(repo.as_path());
let head: String = git_text(repo.as_path(), &["rev-parse", "HEAD"]);
let upper: String = head.to_uppercase();
let cases: Vec<(&str, Vec<u8>)> = vec![
("tag target", b"ref: refs/tags/t\n".to_vec()),
("remote target", b"ref: refs/remotes/o/x\n".to_vec()),
("no newline", b"ref: refs/heads/main".to_vec()),
("carriage return", b"ref: refs/heads/main\r\n".to_vec()),
("trailing space", b"ref: refs/heads/main \n".to_vec()),
("two newlines", b"ref: refs/heads/main\n\n".to_vec()),
("no space", b"ref:refs/heads/main\n".to_vec()),
("two spaces", b"ref: refs/heads/main\n".to_vec()),
("lock suffix", b"ref: refs/heads/a.lock\n".to_vec()),
("dot component", b"ref: refs/heads/.hidden\n".to_vec()),
("double dot", b"ref: refs/heads/a..b\n".to_vec()),
("trailing slash", b"ref: refs/heads/a/\n".to_vec()),
("empty component", b"ref: refs/heads/a//b\n".to_vec()),
("trailing dot", b"ref: refs/heads/a.\n".to_vec()),
("space in name", b"ref: refs/heads/a b\n".to_vec()),
("bare prefix", b"ref: refs/heads/\n".to_vec()),
("not a ref", b"ref: main\n".to_vec()),
("upper hex", format!("{upper}\n").into_bytes()),
("short hex", format!("{}\n", &head[..39]).into_bytes()),
("long hex", format!("{head}0\n").into_bytes()),
("sha256 length in sha1", format!("{head}{}\n", &head[..24]).into_bytes()),
("null name", format!("{}\n", "0".repeat(40)).into_bytes()),
("detached no newline", head.clone().into_bytes()),
("detached trailing space", format!("{head} \n").into_bytes()),
("empty", Vec::new()),
("garbage", vec![0xff, 0xfe, 0x00, b'\n']),
];
for (label, bytes) in cases {
write_head(&found, bytes.as_slice());
assert_eq!(compared(repo.as_path(), &found, label), None, "{label} must be left to Git");
}
remove(root.as_path());
}
#[test]
fn a_symbolic_branch_is_left_to_git() {
let root: PathBuf = fixture("ref-files-chain");
let repo: PathBuf = repository(root.as_path(), "repo");
let found: Probe = probe(repo.as_path());
git(repo.as_path(), &["symbolic-ref", "refs/heads/alias", "refs/heads/main"]);
write_head(&found, b"ref: refs/heads/alias\n");
assert_eq!(
oracle(repo.as_path()),
Ok(SymbolicHead::Branch(String::from("refs/heads/main"))),
"the oracle follows the chain"
);
assert_eq!(compared(repo.as_path(), &found, "alias"), None);
remove(root.as_path());
}
#[test]
fn an_unusual_loose_branch_is_left_to_git() {
let root: PathBuf = fixture("ref-files-loose");
let repo: PathBuf = repository(root.as_path(), "repo");
let found: Probe = probe(repo.as_path());
let common: PathBuf = found.common_dir.clone();
std::fs::write(common.join("refs/heads/garbage"), "not-an-object\n").expect("fixture");
write_head(&found, b"ref: refs/heads/garbage\n");
assert_eq!(compared(repo.as_path(), &found, "garbage branch"), None);
git(repo.as_path(), &["update-ref", "refs/heads/dir/inner", "main"]);
write_head(&found, b"ref: refs/heads/dir\n");
assert_eq!(compared(repo.as_path(), &found, "directory branch"), None);
#[cfg(unix)]
{
std::os::unix::fs::symlink("main", common.join("refs/heads/link")).expect("fixture");
write_head(&found, b"ref: refs/heads/link\n");
assert_eq!(compared(repo.as_path(), &found, "linked branch"), None);
}
remove(root.as_path());
}
#[test]
#[cfg(unix)]
fn a_linked_head_is_left_to_git() {
let root: PathBuf = fixture("ref-files-link");
let repo: PathBuf = repository(root.as_path(), "repo");
let found: Probe = probe(repo.as_path());
let git_dir: PathBuf = found.git_dir.clone();
std::fs::write(root.join("elsewhere"), "ref: refs/heads/main\n").expect("fixture");
std::fs::remove_file(git_dir.join("HEAD")).expect("fixture");
std::os::unix::fs::symlink(root.join("elsewhere"), git_dir.join("HEAD")).expect("fixture");
assert_eq!(compared(repo.as_path(), &found, "linked HEAD"), None);
remove(root.as_path());
}
#[test]
fn a_linked_worktree_reads_its_own_head() {
let root: PathBuf = fixture("ref-files-linked");
let repo: PathBuf = repository(root.as_path(), "repo");
let found: Probe = probe(repo.as_path());
let linked: PathBuf = root.join("linked");
git(repo.as_path(), &["worktree", "add", "--quiet", "-b", "feature", linked.to_str().expect("fixture")]);
let linked_found: Probe = probe(linked.as_path());
assert_eq!(compared(linked.as_path(), &linked_found, "linked"), Some(SymbolicHead::Branch(String::from("refs/heads/feature"))));
assert_eq!(compared(repo.as_path(), &found, "main"), Some(SymbolicHead::Branch(String::from("refs/heads/main"))));
git(linked.as_path(), &["checkout", "--quiet", "--detach"]);
assert_eq!(compared(linked.as_path(), &linked_found, "linked detached"), Some(SymbolicHead::Detached));
assert_eq!(compared(repo.as_path(), &found, "main still"), Some(SymbolicHead::Branch(String::from("refs/heads/main"))));
remove(root.as_path());
}
#[test]
fn a_sha256_repository_is_answered_with_its_own_name_length() {
let root: PathBuf = fixture("ref-files-sha256");
let repo: PathBuf = root.join("repo");
std::fs::create_dir(&repo).expect("fixture");
git(repo.as_path(), &["init", "--quiet", "--initial-branch=main", "--object-format=sha256"]);
git(repo.as_path(), &["commit", "--quiet", "--allow-empty", "--message=initial"]);
let head: String = git_text(repo.as_path(), &["rev-parse", "HEAD"]);
let found: Probe = probe(repo.as_path());
assert_eq!(head.len(), 64);
assert_eq!(compared(repo.as_path(), &found, "sha256 branch"), Some(SymbolicHead::Branch(String::from("refs/heads/main"))));
write_head(&found, format!("{head}\n").as_bytes());
assert_eq!(compared(repo.as_path(), &found, "sha256 detached"), Some(SymbolicHead::Detached));
write_head(&found, format!("{}\n", &head[..40]).as_bytes());
assert_eq!(compared(repo.as_path(), &found, "sha1 length in sha256"), None);
remove(root.as_path());
}
#[test]
fn a_reftable_repository_is_left_to_git() {
let root: PathBuf = fixture("ref-files-reftable");
let repo: PathBuf = root.join("repo");
std::fs::create_dir(&repo).expect("fixture");
git(repo.as_path(), &["init", "--quiet", "--initial-branch=trunk", "--ref-format=reftable"]);
git(repo.as_path(), &["commit", "--quiet", "--allow-empty", "--message=initial"]);
let found: Probe = probe(repo.as_path());
assert_eq!(found.ref_format, RefFormat::Reftable);
let placeholder: String = std::fs::read_to_string(found.git_dir.join("HEAD")).expect("fixture");
assert_eq!(placeholder, "ref: refs/heads/.invalid\n");
assert_eq!(compared(repo.as_path(), &found, "reftable"), None);
assert_eq!(oracle(repo.as_path()), Ok(SymbolicHead::Branch(String::from("refs/heads/trunk"))));
remove(root.as_path());
}
#[test]
fn the_backend_check_refuses_without_the_placeholder() {
let root: PathBuf = fixture("ref-files-backend");
let repo: PathBuf = repository(root.as_path(), "repo");
let found: Probe = probe(repo.as_path());
let layout: RefFilesLayout<'_> = RefFilesLayout {
git_dir: found.git_dir.as_path(),
common_dir: found.common_dir.as_path(),
ref_format: RefFormat::Reftable,
object_format: found.object_format.as_str(),
};
assert_eq!(read_symbolic_head(&layout), None);
remove(root.as_path());
}
#[test]
fn an_unknown_object_format_is_left_to_git() {
let root: PathBuf = fixture("ref-files-format");
let repo: PathBuf = repository(root.as_path(), "repo");
let found: Probe = probe(repo.as_path());
let layout: RefFilesLayout<'_> = RefFilesLayout {
git_dir: found.git_dir.as_path(),
common_dir: found.common_dir.as_path(),
ref_format: RefFormat::Files,
object_format: "sha3",
};
assert_eq!(read_symbolic_head(&layout), None);
remove(root.as_path());
}
#[test]
fn every_call_reads_the_file_again() {
let root: PathBuf = fixture("ref-files-fresh");
let repo: PathBuf = repository(root.as_path(), "repo");
let found: Probe = probe(repo.as_path());
let head: String = git_text(repo.as_path(), &["rev-parse", "HEAD"]);
git(repo.as_path(), &["update-ref", "refs/heads/other", head.as_str()]);
assert_eq!(compared(repo.as_path(), &found, "first"), Some(SymbolicHead::Branch(String::from("refs/heads/main"))));
write_head(&found, b"ref: refs/heads/other\n");
assert_eq!(compared(repo.as_path(), &found, "second"), Some(SymbolicHead::Branch(String::from("refs/heads/other"))));
write_head(&found, format!("{head}\n").as_bytes());
assert_eq!(compared(repo.as_path(), &found, "third"), Some(SymbolicHead::Detached));
remove(root.as_path());
}
#[test]
fn the_plain_name_rule_matches_git_check_ref_format() {
let root: PathBuf = fixture("ref-files-names");
let accepted: [&str; 6] = ["refs/heads/a", "refs/heads/a/b", "refs/heads/a.b", "refs/heads/A_b-c", "refs/heads/-a", "refs/heads/a.lockx"];
let refused: [&str; 12] = [
"refs/heads/", "refs/heads/a/", "refs/heads/a//b", "refs/heads/.a", "refs/heads/a/.b", "refs/heads/a..b",
"refs/heads/a.", "refs/heads/a.lock", "refs/heads/a/b.lock", "refs/tags/a", "heads/a", "refs/heads/a b",
];
for name in accepted {
assert!(is_plain_branch_name(name), "{name}");
let output: Output = git_output(root.as_path(), &["check-ref-format", name]);
assert!(output.status.success(), "Git must accept {name}");
}
for name in refused {
assert!(!is_plain_branch_name(name), "{name}");
}
remove(root.as_path());
}