use std::collections::{HashMap, HashSet};
use std::path::{Path, PathBuf};
use knot_types::{Oid, RepoDid};
use super::*;
const DID: &str = "did:plc:gitreadsfixture00000000";
struct Fixture {
_root: tempfile::TempDir,
repo: PathBuf,
served: PathBuf,
shared: PathBuf,
generation: PathBuf,
}
fn git(dir: &Path, args: &[&str], at: u32) -> String {
let date = format!("{} +0000", 1_700_000_000 + at * 100);
let output = crate::test_git()
.current_dir(dir)
.args(args)
.env("GIT_AUTHOR_DATE", &date)
.env("GIT_COMMITTER_DATE", &date)
.output()
.unwrap();
assert!(output.status.success(), "git {args:?}: {output:?}");
String::from_utf8(output.stdout).unwrap().trim().to_string()
}
fn commit(dir: &Path, path: &str, contents: &str, message: &str, at: u32) -> String {
let file = dir.join(path);
std::fs::create_dir_all(file.parent().unwrap()).unwrap();
std::fs::write(&file, contents).unwrap();
git(dir, &["add", "-A"], at);
git(dir, &["commit", "-q", "-m", message], at);
git(dir, &["rev-parse", "HEAD"], at)
}
fn fixture() -> Fixture {
let root = tempfile::tempdir().unwrap();
let repo = root.path().join("repo");
std::fs::create_dir_all(&repo).unwrap();
git(&repo, &["init", "-q", "-b", "main"], 0);
commit(&repo, "a.txt", "one\ntwo\n", "add a", 1);
commit(&repo, "src/lib.rs", "fn main() {}\n", "add lib", 2);
commit(&repo, "dir/nested/deep.md", "# deep\n", "add deep", 3);
git(&repo, &["tag", "v1"], 3);
commit(&repo, "a.txt", "one\ntwo\nthree\n", "grow a", 4);
git(&repo, &["tag", "-a", "v2", "-m", "second release"], 5);
git(&repo, &["checkout", "-q", "-b", "feature"], 6);
commit(
&repo,
"src/lib.rs",
"fn main() { println!(); }\n",
"feature work",
7,
);
git(&repo, &["checkout", "-q", "main"], 8);
Fixture {
served: root.path().join("repos/key/served"),
shared: root.path().join("git-xrpc/blobs"),
generation: root.path().join("generation"),
repo,
_root: root,
}
}
impl Fixture {
fn rev(&self, spec: &str) -> String {
git(&self.repo, &["rev-parse", spec], 0)
}
fn input(&self, patches: Vec<(Oid, Oid)>) -> RepoInput {
RepoInput {
did: RepoDid::new(DID).unwrap(),
name: "fixture".into(),
export_dir: self.repo.clone(),
served: RepoStore::served(&self.served.parent().unwrap().to_path_buf()),
shared_blobs: self.shared.clone(),
patches: Some(patches),
refs_digest: self.rev("main") + &self.rev("feature"),
verify: false,
}
}
}
struct Served {
maps: HashMap<String, HashMap<String, String>>,
root: PathBuf,
laid: PathBuf,
dir: String,
}
fn words(line: &str) -> Vec<String> {
let mut out = Vec::new();
let mut chars = line.chars().peekable();
while let Some(&c) = chars.peek() {
if c.is_whitespace() {
chars.next();
continue;
}
let mut word = String::new();
if c == '"' {
chars.next();
while let Some(c) = chars.next() {
match c {
'\\' => word.push(chars.next().unwrap()),
'"' => break,
_ => word.push(c),
}
}
} else {
while let Some(&c) = chars.peek() {
if c.is_whitespace() || c == ';' {
break;
}
word.push(c);
chars.next();
}
}
if chars.peek() == Some(&';') {
chars.next();
}
out.push(word);
}
out
}
impl Served {
fn load(generation: &Path) -> Self {
let text = std::fs::read_to_string(generation.join("nginx/maps").join(MAPS_FILE)).unwrap();
let text: String = text
.lines()
.map(|line| match line.trim().strip_prefix("include ") {
Some(path) => std::fs::read_to_string(path.trim_end_matches(';')).unwrap(),
None => format!("{line}\n"),
})
.collect();
let mut maps: HashMap<String, HashMap<String, String>> = HashMap::new();
let mut current: Option<String> = None;
for line in text.lines() {
let line = line.trim();
if let Some(head) = line.strip_prefix("map ") {
let parts = words(head.trim_end_matches('{'));
current = Some(parts[1].clone());
maps.entry(parts[1].clone()).or_default();
} else if line == "}" {
current = None;
} else if let Some(var) = ¤t {
let parts = words(line);
if parts.len() == 2 && parts[0] != "default" {
let previous = maps
.get_mut(var)
.unwrap()
.insert(parts[0].clone(), parts[1].clone());
assert!(previous.is_none(), "{var} has {} twice", parts[0]);
}
}
}
Self {
maps,
root: generation.join(GIT_XRPC_DIR),
laid: generation.join(paths::WWW),
dir: crate::plan::repo_token(DID),
}
}
fn var(&self, var: &str, key: &str) -> Option<String> {
self.maps[var].get(key).cloned()
}
fn get(&self, nsid: &str, query: &[(&str, &str)]) -> Option<Vec<u8>> {
let arg = |name: &str| {
query
.iter()
.find(|(key, _)| *key == name)
.map(|(_, value)| value.to_string())
.unwrap_or_default()
};
let repo_param = match nsid {
"getMergeBase" => "repos",
_ => "repo",
};
assert_eq!(arg(repo_param), DID);
let repo = &self.dir;
let commit = |name: &str| self.var("$ks_git_commit", &format!("{repo}|{}", arg(name)));
let file = match nsid {
"listRefs" | "listBranches" | "listTags" => {
let var = match nsid {
"listRefs" => "$ks_git_refs",
"listBranches" => "$ks_git_branches",
_ => "$ks_git_tags",
};
self.var(var, &format!("{repo}|{}|{}", arg("limit"), arg("cursor")))?
}
"getBranch" => self.var("$ks_git_branch", &format!("{repo}|{}", arg("name")))?,
"getTag" => self.var("$ks_git_tag", &format!("{repo}|{}", arg("tag")))?,
"getDiff" | "getMergeBase" => {
let base = self.var("$ks_git_base", &format!("{repo}|{}", arg("base")))?;
let head = self.var("$ks_git_head", &format!("{repo}|{}", arg("head")))?;
let var = match nsid {
"getDiff" => "$ks_git_diff",
_ => "$ks_git_merge_base",
};
self.var(var, &format!("{base}|{head}"))?
}
_ => {
let at = commit("ref")?;
let (var, key) = match nsid {
"getTree" => ("$ks_git_tree", format!("{at}|{}", arg("path"))),
"getEntry" => ("$ks_git_entry", format!("{at}|{}", arg("path"))),
"getBlob" => ("$ks_git_blob", format!("{at}|{}", arg("path"))),
"getBlame" => ("$ks_git_blame", format!("{at}|{}", arg("path"))),
"listLanguages" => ("$ks_git_langs", at.clone()),
"getArchive" => ("$ks_git_archive", format!("{at}|{}", arg("format"))),
"listCommits" => (
"$ks_git_log",
format!("{at}|{}|{}|{}", arg("path"), arg("limit"), arg("cursor")),
),
_ => panic!("unknown {nsid}"),
};
let laid = paths::Kind::ALL
.into_iter()
.find(|kind| var == format!("$ks_git_{}", kind.name()));
let decoded = match arg("path").as_str() {
"" => Some(String::new()),
path => paths::decode(path),
};
match (self.var(var, &key), laid, decoded) {
(Some(file), _, _) => file,
(None, Some(kind), Some(decoded)) => {
let (limit, cursor) = (arg("limit"), arg("cursor"));
let name = kind.file(&decoded, Some((&limit, &cursor)));
return std::fs::read(self.laid.join(&at).join(name)).ok();
}
_ => return None,
}
}
};
Some(std::fs::read(self.root.join(file)).unwrap())
}
fn json(&self, nsid: &str, query: &[(&str, &str)]) -> serde_json::Value {
let body = self
.get(nsid, query)
.unwrap_or_else(|| panic!("{nsid} {query:?} has no answer"));
serde_json::from_slice(&body).unwrap()
}
}
fn oid(hex: &str) -> Oid {
Oid::from_hex(hex).unwrap()
}
#[test]
fn every_git_read_answers_from_the_pool() {
let fixture = fixture();
let (main, feature) = (fixture.rev("main"), fixture.rev("feature"));
let parent = fixture.rev("main~1");
let sealed = seal(&fixture.input(vec![(oid(&parent), oid(&feature))])).unwrap();
emit(&fixture.generation, std::slice::from_ref(&sealed)).unwrap();
let served = Served::load(&fixture.generation);
let repo = ("repo", DID);
let refs = served.json("listRefs", &[repo]);
let names: Vec<&str> = refs["refs"]
.as_array()
.unwrap()
.iter()
.map(|r| r["ref"].as_str().unwrap())
.collect();
assert_eq!(
names,
[
"refs/heads/feature",
"refs/heads/main",
"refs/tags/v1",
"refs/tags/v2"
]
);
assert_eq!(refs["defaultBranch"]["ref"], "refs/heads/main");
assert_eq!(refs["defaultBranch"]["head"], main);
let branches = served.json("listBranches", &[repo, ("limit", "")]);
assert_eq!(branches["total"], 2);
assert_eq!(branches["defaultBranch"], "main");
assert_eq!(branches["branches"][0]["name"], "feature", "newest first");
assert!(branches.get("cursor").is_none());
let three = served.json("listBranches", &[repo, ("limit", "3"), ("cursor", "0")]);
assert_eq!(three["branches"].as_array().unwrap().len(), 2);
for name in ["feature", "refs/heads/feature", "refs%2Fheads%2Ffeature"] {
let branch = served.json("getBranch", &[repo, ("name", name)]);
assert_eq!(branch["hash"], feature, "{name}");
assert_eq!(branch["totalCommits"], 5);
assert_eq!(branch["message"], "feature work\n");
}
let tags = served.json("listTags", &[repo]);
assert_eq!(tags["total"], 2);
let v2 = &tags["tags"][0];
assert_eq!(v2["name"], "v2");
assert_eq!(v2["oid"], main, "the peeled commit");
assert_eq!(v2["annotation"]["oid"], fixture.rev("v2"), "the tag object");
assert_eq!(v2["annotation"]["message"], "second release");
assert!(tags["tags"][1].get("annotation").is_none());
assert_eq!(
served.json("getTag", &[repo, ("tag", "refs/tags/v1")])["name"],
"v1"
);
let root = served.json("getTree", &[repo, ("ref", "main"), ("path", "")]);
let entries: Vec<&str> = root["entries"]
.as_array()
.unwrap()
.iter()
.map(|e| e["name"].as_str().unwrap())
.collect();
assert_eq!(entries, ["a.txt", "dir", "src"]);
assert_eq!(root["lastCommit"]["hash"], main);
assert_eq!(root["entries"][2]["lastCommit"]["message"], "add lib\n");
for path in ["dir/nested", "dir%2Fnested"] {
let nested = served.json("getTree", &[repo, ("ref", "HEAD"), ("path", path)]);
assert_eq!(nested["entries"][0]["name"], "deep.md");
assert_eq!(nested["entries"][0]["lastCommit"]["message"], "add deep\n");
}
let at_feature = served.json(
"getTree",
&[repo, ("ref", "refs/heads/feature"), ("path", "src")],
);
assert_eq!(at_feature["entries"][0]["lastCommit"]["hash"], feature);
let entry = served.json("getEntry", &[repo, ("ref", "main"), ("path", "src/lib.rs")]);
assert_eq!(entry["oid"], fixture.rev("main:src/lib.rs"));
assert_eq!(entry["mode"], "0100644");
assert_eq!(entry["size"], 13);
assert_eq!(entry["lastCommit"]["message"], "add lib\n");
let tagged = served.json("getEntry", &[repo, ("ref", "v1"), ("path", "a.txt")]);
assert_eq!(tagged["oid"], fixture.rev("v1:a.txt"));
assert_eq!(
served
.get(
"getBlob",
&[repo, ("ref", &feature), ("path", "src/lib.rs")]
)
.unwrap(),
b"fn main() { println!(); }\n"
);
assert_eq!(
served
.get("getBlob", &[repo, ("ref", "v1"), ("path", "a.txt")])
.unwrap(),
b"one\ntwo\n"
);
let log = served.json("listCommits", &[repo, ("ref", "main"), ("limit", "")]);
let commits = log["commits"].as_array().unwrap();
assert_eq!(commits.len(), 4);
assert_eq!(commits[0]["hash"], main);
assert_eq!(commits[0]["oid"], main);
assert_eq!(commits[0]["parents"][0], parent);
assert!(log.get("cursor").is_none());
let first = served.json("listCommits", &[repo, ("ref", "main"), ("limit", "1")]);
assert_eq!(first["cursor"], main);
let next = served.json(
"listCommits",
&[repo, ("ref", "main"), ("limit", "50"), ("cursor", "0")],
);
assert_eq!(next["commits"].as_array().unwrap().len(), 4);
let at = served.json("listCommits", &[repo, ("ref", &parent), ("limit", "1")]);
assert_eq!(at["commits"][0]["hash"], parent);
let linear = served.json("listCommits", &[repo, ("ref", &parent), ("limit", "")]);
assert_eq!(linear["commits"].as_array().unwrap().len(), 3);
let single = served.json("getDiff", &[repo, ("base", &parent), ("head", &main)]);
assert_eq!(single["stats"]["filesChanged"], 1);
assert_eq!(single["diffs"][0]["rhsSrc"]["path"], "a.txt");
assert_eq!(single["stats"]["insertions"], 1);
let root_commit = fixture.rev("main~3");
let born = served.json(
"getDiff",
&[repo, ("base", EMPTY_TREE), ("head", &root_commit)],
);
assert_eq!(born["diffs"][0]["lhsSrc"]["path"], "");
let branch = served.json("getDiff", &[repo, ("base", "main"), ("head", "feature")]);
assert_eq!(branch["diffs"][0]["rhsSrc"]["path"], "src/lib.rs");
let patch = served.json("getDiff", &[repo, ("base", &parent), ("head", &feature)]);
assert_eq!(
patch["stats"]["filesChanged"], 2,
"grow a and the feature work"
);
let old = served
.get("getBlob", &[repo, ("ref", &parent), ("path", "a.txt")])
.unwrap();
assert_eq!(old, b"one\ntwo\n");
assert!(
served
.get("getEntry", &[repo, ("ref", &parent), ("path", "a.txt")])
.is_some()
);
let blame = served.json("getBlame", &[repo, ("ref", "main"), ("path", "a.txt")]);
assert_eq!(blame["commit"], main);
let runs = blame["runs"].as_array().unwrap();
assert_eq!(
runs.iter()
.map(|r| r["count"].as_u64().unwrap())
.sum::<u64>(),
3
);
assert_eq!(blame["commits"].as_array().unwrap().len(), 2);
let languages = served.json("listLanguages", &[repo, ("ref", "main")]);
assert!(
languages["languages"]
.as_array()
.unwrap()
.iter()
.any(|l| l["name"] == "Rust"),
"{languages}"
);
let tarball = served
.get("getArchive", &[repo, ("ref", "main"), ("format", "tar.gz")])
.unwrap();
assert_eq!(&tarball[..2], &[0x1f, 0x8b]);
let zip = served
.get("getArchive", &[repo, ("ref", "main"), ("format", "zip")])
.unwrap();
assert_eq!(&zip[..2], b"PK");
assert!(
served
.get("getArchive", &[repo, ("ref", "feature"), ("format", "zip")])
.is_none(),
"archives are for the head only"
);
let mut named: Vec<&String> = served.maps["$ks_git_archive_name"].values().collect();
named.sort();
assert_eq!(
named,
[
"attachment; filename=\"fixture-main.tar.gz\"",
"attachment; filename=\"fixture-main.zip\""
]
);
assert!(
served
.get("getTree", &[repo, ("ref", "nope"), ("path", "")])
.is_none()
);
}
const EMPTY_TREE: &str = "4b825dc642cb6eb9a060e54bf8d69288fbee4904";
#[test]
fn an_unchanged_repo_reuses_its_seal_and_a_moved_one_its_tips() {
let fixture = fixture();
let first = seal(&fixture.input(Vec::new())).unwrap();
let again = seal(&fixture.input(Vec::new())).unwrap();
assert_eq!(first.key, again.key);
assert_eq!(
serde_json::to_value(first.rows()).unwrap(),
serde_json::to_value(again.rows()).unwrap()
);
let main = fixture.rev("main");
git(&fixture.repo, &["checkout", "-q", "feature"], 9);
let moved = commit(&fixture.repo, "b.txt", "b\n", "more feature", 10);
let next = seal(&fixture.input(Vec::new())).unwrap();
assert_ne!(next.key, first.key);
assert_eq!(
first.rows().tree.get(&format!("{main}|")),
next.rows().tree.get(&format!("{main}|")),
"main's tree answers carry over"
);
assert!(next.rows().tree.contains_key(&format!("{moved}|")));
let tree: serde_json::Value = serde_json::from_slice(
&std::fs::read(next.pool.join(&next.rows().tree[&format!("{moved}|")])).unwrap(),
)
.unwrap();
let names: Vec<(&str, &str)> = tree["entries"]
.as_array()
.unwrap()
.iter()
.map(|e| {
(
e["name"].as_str().unwrap(),
e["lastCommit"]["message"].as_str().unwrap(),
)
})
.collect();
assert_eq!(
names,
[
("a.txt", "grow a\n"),
("b.txt", "more feature\n"),
("dir", "add deep\n"),
("src", "feature work\n")
]
);
}
#[test]
fn a_log_past_the_window_ends_at_it_without_a_cursor() {
let fixture = fixture();
for at in 10..10 + project::LOG_WINDOW as u32 {
git(
&fixture.repo,
&["commit", "-q", "--allow-empty", "-m", "more"],
at,
);
}
let sealed = seal(&fixture.input(Vec::new())).unwrap();
emit(&fixture.generation, std::slice::from_ref(&sealed)).unwrap();
let served = Served::load(&fixture.generation);
let repo = ("repo", DID);
let hundredth = fixture.rev("main~99");
let tail = served.json(
"listCommits",
&[
repo,
("ref", "main"),
("limit", "100"),
("cursor", &hundredth),
],
);
let commits = tail["commits"].as_array().unwrap();
assert_eq!(commits.len(), project::LOG_WINDOW - 100);
assert!(tail.get("cursor").is_none());
assert!(
!commits.last().unwrap()["parents"]
.as_array()
.unwrap()
.is_empty()
);
let at_end = served.json(
"listCommits",
&[
repo,
("ref", "main"),
("limit", "60"),
("cursor", &fixture.rev("main~119")),
],
);
assert_eq!(at_end["commits"].as_array().unwrap().len(), 60);
assert!(at_end.get("cursor").is_none(), "{at_end}");
assert!(
served
.get("listCommits", &[repo, ("ref", "main"), ("path", "a.txt")])
.is_none(),
"a path untouched in the window has no answer"
);
}
fn messages(log: &serde_json::Value) -> Vec<&str> {
log["commits"]
.as_array()
.unwrap()
.iter()
.map(|c| c["message"].as_str().unwrap().trim_end())
.collect()
}
#[test]
fn a_path_log_lists_the_commits_that_touch_it() {
let fixture = fixture();
let sealed = seal(&fixture.input(Vec::new())).unwrap();
emit(&fixture.generation, std::slice::from_ref(&sealed)).unwrap();
let served = Served::load(&fixture.generation);
let repo = ("repo", DID);
let log = |at: &str, path: &str| {
served.json(
"listCommits",
&[repo, ("ref", at), ("path", path), ("limit", "50")],
)
};
assert_eq!(messages(&log("main", "a.txt")), ["grow a", "add a"]);
assert_eq!(messages(&log("main", "dir")), ["add deep"]);
assert_eq!(
messages(&log("main", "dir%2Fnested%2Fdeep.md")),
["add deep"]
);
assert_eq!(messages(&log("main", "src")), ["add lib"]);
let whole = log("main", "a.txt");
assert!(whole.get("cursor").is_none());
for (at, path) in [("main", "nope"), ("feature", "src")] {
assert!(
served
.get("listCommits", &[repo, ("ref", at), ("path", path)])
.is_none(),
"{at} {path}"
);
}
}
#[test]
fn paths_differing_only_by_case_answer_apart_or_neither() {
let fixture = fixture();
commit(&fixture.repo, "Readme.md", "hi\n", "add readme", 9);
git(&fixture.repo, &["mv", "Readme.md", "README.md"], 10);
git(&fixture.repo, &["commit", "-q", "-m", "shout"], 10);
let sealed = seal(&fixture.input(Vec::new())).unwrap();
emit(&fixture.generation, std::slice::from_ref(&sealed)).unwrap();
let served = Served::load(&fixture.generation);
for (var, keys) in &served.maps {
let mut lower = HashSet::new();
for key in keys.keys() {
assert!(lower.insert(key.to_ascii_lowercase()), "{var} {key}");
}
}
let log = |path: &str| {
served
.get(
"listCommits",
&[("repo", DID), ("ref", "main"), ("path", path)],
)
.map(|body| {
let log: serde_json::Value = serde_json::from_slice(&body).unwrap();
messages(&log)
.into_iter()
.map(String::from)
.collect::<Vec<_>>()
})
};
// a case-folding disk can't hold both spellings, a case-sensitive one answers each apart
if paths::folds(&sealed.pool).unwrap() {
assert_eq!((log("Readme.md"), log("README.md")), (None, None));
} else {
assert_eq!(log("Readme.md").unwrap(), ["shout", "add readme"]);
assert_eq!(log("README.md").unwrap(), ["shout"]);
}
assert_eq!(
served.json(
"getEntry",
&[("repo", DID), ("ref", "main"), ("path", "README.md")]
)["name"],
"README.md"
);
}
#[test]
fn a_path_log_pages_by_the_last_commit() {
let fixture = fixture();
for at in 10..130 {
commit(
&fixture.repo,
"x.txt",
&at.to_string(),
&format!("x {at}"),
at,
);
git(
&fixture.repo,
&["commit", "-q", "--allow-empty", "-m", "noise"],
at,
);
}
let sealed = seal(&fixture.input(Vec::new())).unwrap();
emit(&fixture.generation, std::slice::from_ref(&sealed)).unwrap();
let served = Served::load(&fixture.generation);
let repo = ("repo", DID);
let page = |limit: &str, cursor: &str| {
served.json(
"listCommits",
&[
repo,
("ref", "main"),
("path", "x.txt"),
("limit", limit),
("cursor", cursor),
],
)
};
let first = page("", "");
assert_eq!(messages(&first).len(), 50);
assert_eq!(messages(&first)[0], "x 129");
let second = page("50", first["cursor"].as_str().unwrap());
assert_eq!(messages(&second).len(), 40, "the window holds 90 of them");
assert_eq!(messages(&second)[0], "x 79");
assert!(second.get("cursor").is_none());
let hundred = page("100", "");
assert_eq!(messages(&hundred).len(), 90);
assert_eq!(messages(&hundred).last(), Some(&"x 40"));
}
#[test]
fn shared_blobs_link_into_every_pool() {
use std::os::unix::fs::MetadataExt as _;
let fixture = fixture();
let sealed = seal(&fixture.input(Vec::new())).unwrap();
let blob = fixture.rev("main:a.txt");
let pooled = sealed.pool.join("blobs").join(&blob);
let shared = fixture.shared.join(&blob);
assert_eq!(std::fs::read(&pooled).unwrap(), b"one\ntwo\nthree\n");
assert_eq!(
std::fs::metadata(&pooled).unwrap().ino(),
std::fs::metadata(&shared).unwrap().ino()
);
crate::cas::sealed::assert_sealed(&sealed.pool);
}
#[test]
fn patch_ranges_read_the_patch_records() {
let body = |collection: &str| {
let value = serde_json::json!({
"$type": collection,
"base": { "oid": "a".repeat(40) },
"head": { "oid": "b".repeat(40) },
});
Some(knot_types::RecordBody::new(serde_ipld_dagcbor::to_vec(&value).unwrap()).unwrap())
};
let mut cobs = crate::atproto::CobState::default();
for (key, collection) in [
("org.tangled.review.patch/3lubrptx57d22", PATCH_COLLECTION),
(
"org.tangled.track.ticket/3lubrptx57d23",
"org.tangled.track.ticket",
),
] {
cobs.records.insert(
knot_types::MstKey::new(key.to_string()).unwrap(),
body(collection),
);
}
assert_eq!(
patch_ranges(&cobs),
[(oid(&"a".repeat(40)), oid(&"b".repeat(40)))]
);
}
#[test]
fn an_older_round_logs_down_to_its_base() {
let fixture = fixture();
let (main, round) = (fixture.rev("main"), fixture.rev("feature"));
git(&fixture.repo, &["checkout", "-q", "feature"], 9);
commit(&fixture.repo, "b.txt", "b\n", "next round", 10);
let sealed = seal(&fixture.input(vec![(oid(&main), oid(&round))])).unwrap();
emit(&fixture.generation, std::slice::from_ref(&sealed)).unwrap();
let served = Served::load(&fixture.generation);
let log = served.json(
"listCommits",
&[("repo", DID), ("ref", &round), ("limit", "100")],
);
assert_eq!(messages(&log), ["feature work", "grow a"]);
assert!(log.get("cursor").is_none());
}
#[test]
fn merge_bases_answer_for_the_default_branch_and_each_branch() {
let fixture = fixture();
let main = fixture.rev("main");
git(&fixture.repo, &["checkout", "-q", "--orphan", "lone"], 9);
commit(&fixture.repo, "lone.txt", "lone\n", "lone root", 10);
git(&fixture.repo, &["checkout", "-q", "main"], 11);
let sealed = seal(&fixture.input(Vec::new())).unwrap();
emit(&fixture.generation, std::slice::from_ref(&sealed)).unwrap();
let served = Served::load(&fixture.generation);
let repo = ("repos", DID);
for (base, head) in [
("main", "feature"),
("refs%2Fheads%2Fmain", "refs/heads/feature"),
(main.as_str(), "feature"),
("main", "main"),
] {
let found = served.json("getMergeBase", &[repo, ("base", base), ("head", head)]);
assert_eq!(
found,
serde_json::json!({ "commit": main }),
"{base} {head}"
);
}
let key = format!(
"{}/{main}|{}/{}",
served.dir,
served.dir,
fixture.rev("lone")
);
assert_eq!(
served.var("$ks_git_merge_base", &key),
Some(format!("{}/{NO_MERGE_BASE}", served.dir))
);
assert!(
served
.get(
"getMergeBase",
&[repo, ("base", "feature"), ("head", "main")]
)
.is_none(),
"only the default branch is a base"
);
}
#[test]
fn locations_route_all_thirteen_reads() {
let limits = crate::config::ServeLimits::default();
let conf: String = locations("/gen", &limits)
.iter()
.map(|block| block.to_string())
.collect();
for nsid in [
"listRefs",
"listBranches",
"getBranch",
"listTags",
"getTag",
"getTree",
"getEntry",
"getBlob",
"getBlame",
"listCommits",
"getDiff",
"listLanguages",
"getArchive",
] {
assert!(
conf.contains(&format!("location = /xrpc/org.tangled.git.{nsid} {{")),
"{nsid}"
);
}
assert!(conf.contains("location = /xrpc/org.tangled.temp.git.getMergeBase {"));
assert!(conf.contains("add_header X-Content-Type-Options nosniff always;"));
assert!(conf.contains("alias /gen/www/_git_xrpc/;"));
}
/// gix-blame panics on a file whose path was a folder in the parent.
#[test]
fn a_file_that_was_a_folder_seals_without_its_blame() {
let fixture = fixture();
commit(&fixture.repo, "was/inner.txt", "inner\n", "a folder", 9);
git(&fixture.repo, &["rm", "-q", "-r", "was"], 10);
commit(&fixture.repo, "was", "now a file\n", "a file", 10);
let sealed = seal(&fixture.input(Vec::new())).unwrap();
emit(&fixture.generation, std::slice::from_ref(&sealed)).unwrap();
let served = Served::load(&fixture.generation);
let blame = |path: &str| {
served.get(
"getBlame",
&[("repo", DID), ("ref", "main"), ("path", path)],
)
};
assert!(blame("was").is_none());
assert!(blame("a.txt").is_some());
}
#[test]
fn prepare_lays_out_the_maps_a_seal_from_an_older_binary_lacks() {
let fixture = fixture();
let sealed = seal(&fixture.input(Vec::new())).unwrap();
let written: Vec<(PathBuf, Vec<u8>)> = std::fs::read_dir(&sealed.maps)
.unwrap()
.map(|entry| {
let path = entry.unwrap().path();
let bytes = std::fs::read(&path).unwrap();
(path, bytes)
})
.collect();
std::fs::remove_dir_all(&sealed.maps).unwrap();
let served = fixture.input(Vec::new()).served;
let did = RepoDid::new(DID).unwrap();
assert!(super::prepare_repo(&served, &did).unwrap());
for (path, bytes) in &written {
assert_eq!(&std::fs::read(path).unwrap(), bytes, "{}", path.display());
}
assert!(
!super::prepare_repo(&served, &did).unwrap(),
"a second run writes nothing"
);
}
#[test]
fn gc_sweeps_a_pool_again_only_once_its_seals_change() {
let fixture = fixture();
let repo_dir = fixture.served.parent().unwrap().to_path_buf();
let first = seal(&fixture.input(Vec::new())).unwrap();
let junk = first.pool.join("json/junk");
let collect = |key: &str| {
crate::generate::gc::collect_git_reads(&repo_dir, BTreeSet::from([key.to_string()]))
.unwrap()
};
std::fs::write(&junk, "x").unwrap();
assert_eq!(collect(&first.key), 1);
assert!(!junk.exists());
std::fs::write(&junk, "x").unwrap();
assert_eq!(collect(&first.key), 0, "the same seals were swept already");
assert!(junk.exists());
git(&fixture.repo, &["checkout", "-q", "feature"], 9);
commit(&fixture.repo, "b.txt", "b\n", "more feature", 10);
let next = seal(&fixture.input(Vec::new())).unwrap();
assert!(collect(&next.key) >= 1);
assert!(!junk.exists(), "new seals sweep the pool again");
}
#[test]
fn a_fragments_entries_come_from_its_carry_and_prepare_backfills_them() {
let fixture = fixture();
let sealed = seal(&fixture.input(Vec::new())).unwrap();
let mut carried = HashMap::new();
for (_, variable) in sources() {
let path = sealed.maps.join(fragment(variable));
assert_eq!(
fragment_entries(&path, &mut carried).unwrap(),
counted(&path).unwrap(),
"{variable}"
);
}
let carry_path = sealed.maps.join(CARRY_FILE);
let mut carry: serde_json::Value =
serde_json::from_slice(&std::fs::read(&carry_path).unwrap()).unwrap();
let written = carry["entries"].take();
carry.as_object_mut().unwrap().remove("entries");
std::fs::write(&carry_path, serde_json::to_vec(&carry).unwrap()).unwrap();
let served = fixture.input(Vec::new()).served;
let did = RepoDid::new(DID).unwrap();
assert!(super::prepare_repo(&served, &did).unwrap());
let backfilled: serde_json::Value =
serde_json::from_slice(&std::fs::read(&carry_path).unwrap()).unwrap();
assert_eq!(backfilled["entries"], written);
}