use super::*;
use crate::repo_read_object::from_hex;
use crate::test_support::{fixture, git, git_output, remove};
use std::path::{Path, PathBuf};
fn oid(n: u8) -> String {
return format!("{n:02x}").repeat(32);
}
fn pointer(n: u8) -> String {
return format!("version https://git-lfs.github.com/spec/v1\noid sha256:{}\nsize {}\n", oid(n), 100 + u32::from(n));
}
fn commit(repo: &Path, files: &[(&str, &str)], message: &str) -> String {
for (path, text) in files {
let target: PathBuf = repo.join(path);
std::fs::create_dir_all(target.parent().expect("a parent")).expect("directories");
std::fs::write(target, text).expect("file");
}
git(repo, &["add", "--all"]);
git(repo, &["commit", "--quiet", "--allow-empty", &format!("--message={message}")]);
return name_of(repo, "HEAD");
}
fn name_of(repo: &Path, revision: &str) -> String {
return String::from_utf8(git_output(repo, &["rev-parse", revision]).stdout).expect("UTF-8").trim().to_owned();
}
fn history(root: &Path) -> (PathBuf, [String; 4]) {
let repo: PathBuf = root.join("repo");
std::fs::create_dir(&repo).expect("dir");
git(repo.as_path(), &["init", "--quiet", "--initial-branch=main"]);
let base: String = commit(repo.as_path(), &[("docs/readme.txt", "one\n"), ("assets/one.bin", pointer(1).as_str())], "base");
let added: String = commit(repo.as_path(), &[("assets/two.bin", pointer(2).as_str())], "added");
std::fs::remove_file(repo.join("assets/two.bin")).expect("remove");
let removed: String = commit(repo.as_path(), &[("docs/readme.txt", "two\n")], "removed");
let tip: String = commit(repo.as_path(), &[("docs/other.txt", "three\n")], "tip");
return (repo, [base, added, removed, tip]);
}
fn scan(repo: &Path, include: &[&str], stop: &[&str]) -> Option<Vec<String>> {
let format = ObjectFormat::Sha1;
let database = ObjectDatabase::open(repo.join(".git").as_path(), None, format).expect("plain database");
let names = |tips: &[&str]| -> Vec<Vec<u8>> {
return tips.iter().map(|tip| { return from_hex(tip.as_bytes(), format).expect("name"); }).collect();
};
return new_pointer_objects(&database, format, names(include).as_slice(), names(stop).as_slice(), SCAN_BUDGET);
}
#[test]
fn commits_without_a_pointer_introduce_none() {
let root = fixture("lfs-scan-none");
let (repo, [base, _added, removed, tip]) = history(root.as_path());
assert_eq!(scan(repo.as_path(), &[tip.as_str()], &[removed.as_str()]), Some(Vec::new()));
assert_eq!(scan(repo.as_path(), &[tip.as_str()], &[tip.as_str()]), Some(Vec::new()), "nothing is ahead of itself");
assert_eq!(scan(repo.as_path(), &[removed.as_str()], &[base.as_str()]).map(|found| { return found.len(); }), Some(1), "the control: the range holding the intermediate commit finds one");
remove(root.as_path());
}
#[test]
fn a_new_pointer_is_named_and_a_known_one_is_not() {
let root = fixture("lfs-scan-one");
let (repo, [base, added, _removed, _tip]) = history(root.as_path());
assert_eq!(scan(repo.as_path(), &[added.as_str()], &[base.as_str()]), Some(vec![oid(2)]));
remove(root.as_path());
}
#[test]
fn a_pointer_added_and_removed_inside_the_range_is_still_named() {
let root = fixture("lfs-scan-transient");
let (repo, [base, _added, _removed, tip]) = history(root.as_path());
assert_eq!(scan(repo.as_path(), &[tip.as_str()], &[base.as_str()]), Some(vec![oid(2)]));
remove(root.as_path());
}
#[test]
fn without_known_tips_the_whole_history_is_scanned() {
let root = fixture("lfs-scan-all");
let (repo, [_base, _added, _removed, tip]) = history(root.as_path());
assert_eq!(scan(repo.as_path(), &[tip.as_str()], &[]), Some(vec![oid(1), oid(2)]));
remove(root.as_path());
}
#[test]
fn a_merge_names_what_neither_parent_holds_and_each_object_once() {
let root = fixture("lfs-scan-merge");
let (repo, [base, _added, _removed, tip]) = history(root.as_path());
git(repo.as_path(), &["checkout", "--quiet", "-b", "side", base.as_str()]);
let side: String = commit(repo.as_path(), &[("assets/three.bin", pointer(3).as_str())], "side");
git(repo.as_path(), &["checkout", "--quiet", "main"]);
git(repo.as_path(), &["merge", "--quiet", "--no-ff", "--message=merge", "side"]);
let merge: String = name_of(repo.as_path(), "HEAD");
assert_eq!(scan(repo.as_path(), &[merge.as_str()], &[tip.as_str()]), Some(vec![oid(3)]), "pointer 1 sits behind the known tip and is not dragged in");
assert_eq!(scan(repo.as_path(), &[merge.as_str()], &[tip.as_str(), side.as_str()]), Some(Vec::new()), "both parents known: the merge adds nothing");
remove(root.as_path());
}
fn dated(repo: &Path, arguments: &[&str], date: &str) {
let output = std::process::Command::new(crate::test_support::REAL_GIT)
.current_dir(repo)
.env_clear()
.env("PATH", "/usr/bin:/bin")
.env("GIT_CONFIG_NOSYSTEM", "1")
.env("GIT_CONFIG_GLOBAL", "/nonexistent-global-config")
.env("GIT_AUTHOR_NAME", "Fixture")
.env("GIT_AUTHOR_EMAIL", "fixture@example.invalid")
.env("GIT_COMMITTER_NAME", "Fixture")
.env("GIT_COMMITTER_EMAIL", "fixture@example.invalid")
.env("GIT_AUTHOR_DATE", date)
.env("GIT_COMMITTER_DATE", date)
.args(arguments)
.output()
.expect("git starts");
assert!(output.status.success(), "git {arguments:?}: {}", String::from_utf8_lossy(output.stderr.as_slice()));
}
#[test]
fn an_old_side_branch_merged_late_drags_no_known_history() {
let root = fixture("lfs-scan-order");
let repo: PathBuf = root.join("repo");
std::fs::create_dir(&repo).expect("dir");
git(repo.as_path(), &["init", "--quiet", "--initial-branch=main"]);
std::fs::create_dir_all(repo.join("assets")).expect("assets");
std::fs::write(repo.join("assets/one.bin"), pointer(1)).expect("pointer");
git(repo.as_path(), &["add", "--all"]);
dated(repo.as_path(), &["commit", "--quiet", "--message=old"], "1000000000 +0000");
dated(repo.as_path(), &["checkout", "--quiet", "-b", "side"], "1000000000 +0000");
std::fs::write(repo.join("side.txt"), "side\n").expect("side");
git(repo.as_path(), &["add", "--all"]);
dated(repo.as_path(), &["commit", "--quiet", "--message=side"], "1000001000 +0000");
dated(repo.as_path(), &["checkout", "--quiet", "main"], "1000000000 +0000");
for step in 0..8 {
dated(repo.as_path(), &["commit", "--quiet", "--allow-empty", &format!("--message=chain {step}")], &format!("{} +0000", 1_000_002_000 + step));
}
let known: String = name_of(repo.as_path(), "HEAD");
dated(repo.as_path(), &["merge", "--quiet", "--no-ff", "--no-edit", "side"], "1000003000 +0000");
let merge: String = name_of(repo.as_path(), "HEAD");
assert_eq!(scan(repo.as_path(), &[merge.as_str()], &[known.as_str()]), Some(Vec::new()), "the old pointer is behind the known tip");
assert_eq!(scan(repo.as_path(), &[merge.as_str()], &[]), Some(vec![oid(1)]), "the control: with nothing known it is found");
remove(root.as_path());
}
#[test]
fn history_far_behind_the_known_tip_is_never_read() {
let root = fixture("lfs-scan-deep");
let repo: PathBuf = root.join("repo");
std::fs::create_dir(&repo).expect("dir");
git(repo.as_path(), &["init", "--quiet", "--initial-branch=main"]);
std::fs::create_dir_all(repo.join("assets")).expect("assets");
std::fs::write(repo.join("assets/one.bin"), pointer(1)).expect("pointer");
git(repo.as_path(), &["add", "--all"]);
let mut names: Vec<String> = Vec::new();
for step in 0..30_u32 {
let date: String = format!("{} +0000", 1_000_000_000 + step * 10);
dated(repo.as_path(), &["commit", "--quiet", "--allow-empty", &format!("--message=known {step}")], &date);
names.push(name_of(repo.as_path(), "HEAD"));
}
let hole: &String = &names[3];
std::fs::remove_file(repo.join(".git/objects").join(&hole[..2]).join(&hole[2..])).expect("the commit is stored loose");
let known: &String = names.last().expect("a tip");
std::fs::write(repo.join("docs.txt"), "new\n").expect("file");
git(repo.as_path(), &["add", "--all"]);
dated(repo.as_path(), &["commit", "--quiet", "--message=new"], "1000001000 +0000");
let top: String = name_of(repo.as_path(), "HEAD");
assert_eq!(scan(repo.as_path(), &[top.as_str()], &[known.as_str()]), Some(Vec::new()));
remove(root.as_path());
}
#[test]
fn lookalikes_and_large_files_are_not_named() {
let root = fixture("lfs-scan-lookalike");
let (repo, [_base, _added, _removed, tip]) = history(root.as_path());
let lookalike: String = pointer(9).replace("size", "length");
let large: String = format!("{}{}", pointer(8), "x".repeat(2000));
let top: String = commit(repo.as_path(), &[("notes/mention.txt", "see git-lfs docs\n"), ("lookalike.bin", lookalike.as_str()), ("large.bin", large.as_str())], "lookalikes");
assert_eq!(scan(repo.as_path(), &[top.as_str()], &[tip.as_str()]), Some(Vec::new()));
remove(root.as_path());
}
#[test]
fn what_the_readers_cannot_answer_declines() {
let root = fixture("lfs-scan-decline");
let (repo, [_base, _added, _removed, tip]) = history(root.as_path());
let missing: String = "ab".repeat(20);
assert_eq!(scan(repo.as_path(), &[missing.as_str()], &[]), None, "an unknown tip");
let format = ObjectFormat::Sha1;
let database = ObjectDatabase::open(repo.join(".git").as_path(), None, format).expect("database");
let tip_name: Vec<Vec<u8>> = vec![from_hex(tip.as_bytes(), format).expect("name")];
assert_eq!(new_pointer_objects(&database, format, tip_name.as_slice(), &[], Duration::ZERO), None, "a spent budget");
let extended: String = format!("version https://git-lfs.github.com/spec/v1\next-0-foo sha256:{}\noid sha256:{}\nsize 9\n", oid(4), oid(5));
let top: String = commit(repo.as_path(), &[("ext.bin", extended.as_str())], "extension");
assert_eq!(scan(repo.as_path(), &[top.as_str()], &[tip.as_str()]), None, "an extension line");
remove(root.as_path());
}