//! a key the maps don't know answers 404.
use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet};
use std::path::{Path, PathBuf};
use anyhow::Context as _;
use knot_git::Repo;
use knot_types::{Oid, RepoDid};
use serde::{Deserialize, Serialize};
use crate::atproto::{Pool, short_hash};
use crate::cas::atomic_write;
use crate::nginx::cached;
use crate::nginx::conf::{Block, NONE};
use crate::store::RepoStore;
use crate::xrpc::{Refused, empty, map, reroute};
pub(crate) mod paths;
mod project;
#[cfg(test)]
mod tests;
mod wire;
pub const GIT_XRPC_DIR: &str = "www/_git_xrpc";
pub(crate) const MAPS_FILE: &str = "64_git_xrpc.conf";
/// the seal key doesn't cover the pooled files, bump this when they change.
pub(crate) const SEAL_VERSION: u32 = 9;
const POOL_KIND: &str = "git-xrpc";
const SEAL_KIND: &str = "git-xrpc-seal";
const LAST_SEALED: &str = "git-xrpc.last";
/// the seals gc last swept the pool against, a pool kept for the same seals has nothing new to sweep
const LAST_SWEPT: &str = "git-xrpc.swept";
/// per seal key: a map fragment per variable, what gc keeps, and what a carried seal needs
const MAPS_KIND: &str = "git-xrpc-maps";
const CARRY_FILE: &str = "carry.json";
const KEPT_FILE: &str = "kept.json";
pub(crate) const POOLED: [&str; 4] = ["json", "blobs", "archives", "blame"];
pub(crate) const SHARED_BLOBS: &str = "git-xrpc/blobs";
pub(crate) const NO_MERGE_BASE: &str = "none";
pub(crate) const PATCH_COLLECTION: &str = "org.tangled.review.patch";
pub struct RepoInput {
pub did: RepoDid,
pub name: String,
pub export_dir: PathBuf,
pub served: RepoStore,
pub shared_blobs: PathBuf,
/// none keeps the patches from the last seal.
pub patches: Option<Vec<(Oid, Oid)>>,
/// the seal key only hashes this and the patches. anything else the projection reads has to go in here too.
pub refs_digest: String,
pub verify: bool,
}
pub struct SealedRepo {
pub did: RepoDid,
pub key: String,
pub(crate) pool: PathBuf,
/// the map fragments the generation includes
pub(crate) maps: PathBuf,
pub(crate) paths: String,
#[cfg(test)]
served: RepoStore,
#[cfg(test)]
read_rows: std::sync::OnceLock<Rows>,
}
#[cfg(test)]
impl SealedRepo {
pub(crate) fn rows(&self) -> &Rows {
self.read_rows
.get_or_init(|| sealed_rows(&self.served, &self.key).expect("the seal reads"))
}
}
impl SealedRepo {
pub(crate) fn dir_name(&self) -> String {
crate::plan::repo_token(self.did.as_str())
}
}
#[derive(Default, Serialize, Deserialize, Clone)]
pub(crate) struct Rows {
pub(crate) commit: BTreeMap<String, String>,
pub(crate) refs: BTreeMap<String, String>,
pub(crate) branches: BTreeMap<String, String>,
pub(crate) branch: BTreeMap<String, String>,
pub(crate) tags: BTreeMap<String, String>,
pub(crate) tag: BTreeMap<String, String>,
pub(crate) tree: BTreeMap<String, String>,
pub(crate) entry: BTreeMap<String, String>,
pub(crate) blob: BTreeMap<String, String>,
pub(crate) blame: BTreeMap<String, String>,
pub(crate) log: BTreeMap<String, String>,
pub(crate) diff: BTreeMap<String, String>,
pub(crate) merge_base: BTreeMap<String, String>,
pub(crate) languages: BTreeMap<String, String>,
pub(crate) archive: BTreeMap<String, String>,
pub(crate) archive_name: BTreeMap<String, String>,
}
impl Rows {
pub(crate) fn absorb(&mut self, other: &Rows) {
let pairs = [
(&mut self.commit, &other.commit),
(&mut self.refs, &other.refs),
(&mut self.branches, &other.branches),
(&mut self.branch, &other.branch),
(&mut self.tags, &other.tags),
(&mut self.tag, &other.tag),
(&mut self.tree, &other.tree),
(&mut self.entry, &other.entry),
(&mut self.blob, &other.blob),
(&mut self.blame, &other.blame),
(&mut self.log, &other.log),
(&mut self.diff, &other.diff),
(&mut self.merge_base, &other.merge_base),
(&mut self.languages, &other.languages),
(&mut self.archive, &other.archive),
(&mut self.archive_name, &other.archive_name),
];
for (mine, theirs) in pairs {
for (key, value) in theirs {
mine.entry(key.clone()).or_insert_with(|| value.clone());
}
}
}
}
/// all a generation needs of a seal it carries unchanged
#[derive(Serialize, Deserialize)]
struct Carry {
patches: Vec<(Oid, Oid)>,
paths: String,
/// entries per fragment, which nginx sizes its map hashes by
#[serde(default)]
entries: BTreeMap<String, usize>,
}
/// the files a seal holds in the pool, which gc keeps
#[derive(Serialize, Deserialize)]
struct Kept {
files: BTreeMap<String, u64>,
paths: paths::Laid,
}
fn read_json<T: serde::de::DeserializeOwned>(path: &Path) -> Option<T> {
serde_json::from_slice(&std::fs::read(path).ok()?).ok()
}
fn seal_key(input: &RepoInput, patches: &[(Oid, Oid)]) -> anyhow::Result<String> {
Ok(format!(
"{SEAL_VERSION}-{}",
short_hash(&serde_json::to_vec(&(&input.refs_digest, patches))?)
))
}
#[derive(Serialize, Deserialize)]
pub(crate) struct Sealed {
patches: Vec<(Oid, Oid)>,
rows: Rows,
files: BTreeMap<String, u64>,
paths: paths::Laid,
memo: project::Memo,
}
/// an unchanged repo is carried by its seal key, no record of it is read
pub fn seal(input: &RepoInput) -> anyhow::Result<SealedRepo> {
let dir = input.served.pool(POOL_KIND);
let maps_root = input.served.kind_dir(MAPS_KIND);
let last = input.served.kind_dir(LAST_SEALED);
let last_key = std::fs::read_to_string(&last).ok();
let sealed_repo = |key: &str, paths: String| SealedRepo {
did: input.did.clone(),
key: key.to_string(),
pool: dir.clone(),
maps: maps_root.join(key),
paths,
#[cfg(test)]
served: input.served.clone(),
#[cfg(test)]
read_rows: Default::default(),
};
let carried = last_key
.as_deref()
.filter(|_| !input.verify)
.and_then(|key| {
Some((
key,
read_json::<Carry>(&maps_root.join(key).join(CARRY_FILE))?,
))
});
if let Some((key, carry)) = carried {
let patches = input.patches.as_deref().unwrap_or(&carry.patches);
if seal_key(input, patches)? == key {
return Ok(sealed_repo(key, carry.paths));
}
}
let read = |key: &str| {
input
.served
.read_file::<Sealed>(SEAL_KIND, key)
.filter(|_| !input.verify)
};
let prior = last_key.as_deref().and_then(read);
// the memo holds pooled pages too, it's only reused within one version.
let same_version = last_key
.as_deref()
.is_some_and(|key| key.starts_with(&format!("{SEAL_VERSION}-")));
let patches = match &input.patches {
Some(patches) => patches.clone(),
None => prior
.as_ref()
.map(|prior| prior.patches.clone())
.unwrap_or_default(),
};
let key = seal_key(input, &patches)?;
if let Some(sealed) =
read(&key).filter(|sealed| holds(&dir, &sealed.files) && sealed.paths.held(&dir))
{
let unlaid = paths::split(&sealed.rows, paths::folds(&dir)?).1;
write_maps(&input.did, &maps_root.join(&key), &sealed, &unlaid)?;
if last_key.as_deref() != Some(key.as_str()) {
atomic_write(&last, key.as_bytes())?;
}
return Ok(sealed_repo(&key, sealed.paths.seal));
}
let repo = Repo::open(&input.export_dir)
.with_context(|| format!("open export {}", input.export_dir.display()))?;
let mut pool = Pool {
dir: &dir,
verify: input.verify,
files: BTreeMap::new(),
};
let (rows, memo) = project::run(
&repo,
&mut pool,
&input.shared_blobs,
&input.name,
&patches,
prior
.as_ref()
.filter(|_| same_version)
.map(|prior| &prior.memo),
)
.with_context(|| format!("project git reads of {}", input.did))?;
let (layout, unlaid) = paths::split(&rows, paths::folds(&dir)?);
let laid = paths::publish(&dir, &layout)
.with_context(|| format!("lay out git reads of {}", input.did))?;
let sealed = Sealed {
patches,
rows,
files: pool.files,
paths: laid,
memo,
};
input.served.publish_file(SEAL_KIND, &key, &sealed)?;
write_maps(&input.did, &maps_root.join(&key), &sealed, &unlaid)?;
atomic_write(&last, key.as_bytes())?;
Ok(sealed_repo(&key, sealed.paths.seal))
}
/// the carry file goes last, a seal without one gets its fragments written again
fn write_maps(did: &RepoDid, dir: &Path, sealed: &Sealed, unlaid: &Rows) -> anyhow::Result<()> {
if dir.join(CARRY_FILE).is_file() {
return Ok(());
}
std::fs::create_dir_all(dir)?;
let mut maps = Maps::default();
maps.add(&crate::plan::repo_token(did.as_str()), &sealed.rows, unlaid);
let mut entries = BTreeMap::new();
for (_, variable) in sources() {
let rows = maps.take(variable);
entries.insert(fragment(variable), rows.len());
let text = Block::file().entries(rows).to_string();
atomic_write(&dir.join(fragment(variable)), text.as_bytes())?;
}
let kept = Kept {
files: sealed.files.clone(),
paths: sealed.paths.clone(),
};
atomic_write(&dir.join(KEPT_FILE), &serde_json::to_vec(&kept)?)?;
let carry = Carry {
patches: sealed.patches.clone(),
paths: sealed.paths.seal.clone(),
entries,
};
Ok(atomic_write(
&dir.join(CARRY_FILE),
&serde_json::to_vec(&carry)?,
)?)
}
/// writes the map fragments of every active repo's seal, which the first write after a deploy
/// would otherwise do for every repo it carries
pub fn prepare(output_root: &Path) -> anyhow::Result<usize> {
let Some(active) = crate::plan::load_active_generation(output_root)? else {
return Ok(0);
};
let mut written = 0;
for repo in &active.manifest.repos {
let did = RepoDid::new(repo.repo_did.as_str())?;
let repo_dir = output_root
.join("repos")
.join(crate::plan::repo_key(did.as_str()));
if prepare_repo(&RepoStore::served(&repo_dir), &did)
.with_context(|| format!("prepare git reads of {did}"))?
{
written += 1;
}
}
Ok(written)
}
fn prepare_repo(served: &RepoStore, did: &RepoDid) -> anyhow::Result<bool> {
let Some(key) = last_seal(served) else {
return Ok(false);
};
let dir = served.kind_dir(MAPS_KIND).join(&key);
if let Some(mut carry) = read_json::<Carry>(&dir.join(CARRY_FILE)) {
if !carry.entries.is_empty() {
return Ok(false);
}
for (_, variable) in sources() {
let name = fragment(variable);
carry
.entries
.insert(name.clone(), counted(&dir.join(name))?);
}
atomic_write(&dir.join(CARRY_FILE), &serde_json::to_vec(&carry)?)?;
return Ok(true);
}
let Some(sealed) = served.read_file::<Sealed>(SEAL_KIND, &key) else {
return Ok(false);
};
let unlaid = paths::split(&sealed.rows, paths::folds(&served.pool(POOL_KIND))?).1;
write_maps(did, &dir, &sealed, &unlaid)?;
Ok(true)
}
/// how many entries an included fragment adds to its map, from its seal's carry file
pub(crate) fn fragment_entries(
fragment: &Path,
carried: &mut HashMap<PathBuf, BTreeMap<String, usize>>,
) -> anyhow::Result<usize> {
let dir = fragment.parent().context("fragment dir")?;
let entries = carried.entry(dir.to_path_buf()).or_insert_with(|| {
read_json::<Carry>(&dir.join(CARRY_FILE))
.map(|carry| carry.entries)
.unwrap_or_default()
});
let name = fragment.file_name().and_then(|name| name.to_str());
match name.and_then(|name| entries.get(name)) {
Some(&count) => Ok(count),
None => counted(fragment),
}
}
fn counted(fragment: &Path) -> anyhow::Result<usize> {
let text = std::fs::read_to_string(fragment)
.with_context(|| format!("read {}", fragment.display()))?;
Ok(text.lines().filter(|line| !line.trim().is_empty()).count())
}
fn fragment(variable: &str) -> String {
format!("{}.conf", variable.trim_start_matches('$'))
}
pub(crate) fn sealed_rows(served: &RepoStore, key: &str) -> Option<Rows> {
#[derive(Deserialize)]
struct Only {
rows: Rows,
}
served
.read_file::<Only>(SEAL_KIND, key)
.map(|only| only.rows)
}
fn holds(dir: &Path, files: &BTreeMap<String, u64>) -> bool {
files
.iter()
.all(|(file, &len)| std::fs::metadata(dir.join(file)).is_ok_and(|meta| meta.len() == len))
}
/// the languages the seal serves for a commit, biggest first
pub(crate) fn sealed_languages(
served: &RepoStore,
key: &str,
commit: &str,
) -> Option<Vec<(String, u64)>> {
#[derive(Deserialize)]
struct Languages {
languages: Vec<wire::LanguageOut>,
}
let rows = sealed_rows(served, key)?;
let file = rows.languages.get(commit)?;
let answer: Languages = read_json(&served.pool(POOL_KIND).join(file))?;
Some(
answer
.languages
.into_iter()
.map(|language| (language.name, language.size))
.collect(),
)
}
/// insertions, deletions and files changed
pub(crate) fn diff_stat(patches: &[knot_git::FilePatch]) -> (i64, i64, i64) {
let stat = wire::DiffStatOut::of(patches);
(stat.insertions, stat.deletions, stat.files_changed)
}
pub fn patch_ranges(cobs: &crate::atproto::CobState) -> Vec<(Oid, Oid)> {
let oid = |value: &serde_json::Value, side: &str| {
value[side]["oid"]
.as_str()
.and_then(|hex| Oid::from_hex(hex).ok())
};
let mut ranges: Vec<(Oid, Oid)> = cobs
.records
.iter()
.filter(|(key, _)| {
key.as_str()
.strip_prefix(PATCH_COLLECTION)
.is_some_and(|rest| rest.starts_with('/'))
})
.filter_map(|(_, body)| {
let value: serde_json::Value =
serde_ipld_dagcbor::from_slice(body.as_ref()?.as_bytes()).ok()?;
Some((oid(&value, "base")?, oid(&value, "head")?))
})
.collect();
ranges.sort();
ranges.dedup();
ranges
}
pub fn emit(generation_dir: &Path, repos: &[SealedRepo]) -> anyhow::Result<()> {
let root = generation_dir.join(GIT_XRPC_DIR);
let laid = generation_dir.join(paths::WWW);
std::fs::create_dir_all(&root)?;
std::fs::create_dir_all(&laid)?;
let mut sorted: Vec<&SealedRepo> = repos.iter().collect();
sorted.sort_unstable_by_key(|repo| repo.did.as_str());
let mut fragments = Vec::with_capacity(sorted.len());
for repo in sorted {
std::fs::create_dir_all(&repo.pool)?;
let pool = std::fs::canonicalize(&repo.pool)
.with_context(|| format!("git reads of {}", repo.did))?;
let name = repo.dir_name();
std::os::unix::fs::symlink(&pool, root.join(&name))?;
std::os::unix::fs::symlink(paths::seal_dir(&pool, &repo.paths), laid.join(&name))?;
fragments.push(
std::fs::canonicalize(&repo.maps)
.with_context(|| format!("git read maps of {}", repo.did))?,
);
}
let mut file = Block::file().blocks(paths::maps());
for (source, variable) in sources() {
let block = fragments
.iter()
.fold(map(source, variable, Vec::new()), |block, dir| {
block.directive("include", [dir.join(fragment(variable)).display()])
});
file = file.block(block);
}
let path = generation_dir.join("nginx/maps").join(MAPS_FILE);
atomic_write(&path, file.to_string().as_bytes())
.with_context(|| format!("write {}", path.display()))
}
fn sources() -> impl Iterator<Item = (&'static str, &'static str)> {
COMMIT_SOURCES
.into_iter()
.chain(FILE_MAPS)
.chain([("$ks_git_archive", "$ks_git_archive_name")])
}
#[derive(Default)]
struct Maps {
rows: BTreeMap<&'static str, Vec<(String, String)>>,
}
/// keys this long are what `map_hash_bucket_size` is sized for, longer ones would fail the whole config.
const MAX_KEY_BYTES: usize = 400;
const COMMIT_SOURCES: [(&str, &str); 3] = [
("$ks_atp_repo|$arg_ref", "$ks_git_commit"),
("$ks_atp_repo|$arg_base", "$ks_git_base"),
("$ks_atp_repo|$arg_head", "$ks_git_head"),
];
const FILE_MAPS: [(&str, &str); 14] = [
("$ks_atp_repo|$arg_limit|$arg_cursor", "$ks_git_refs"),
("$ks_atp_repo|$arg_limit|$arg_cursor", "$ks_git_branches"),
("$ks_atp_repo|$arg_name", "$ks_git_branch"),
("$ks_atp_repo|$arg_limit|$arg_cursor", "$ks_git_tags"),
("$ks_atp_repo|$arg_tag", "$ks_git_tag"),
("$ks_git_commit|$arg_path", "$ks_git_tree"),
("$ks_git_commit|$arg_path", "$ks_git_entry"),
("$ks_git_commit|$arg_path", "$ks_git_blob"),
("$ks_git_commit|$arg_path", "$ks_git_blame"),
(
"$ks_git_commit|$arg_path|$arg_limit|$arg_cursor",
"$ks_git_log",
),
("$ks_git_base|$ks_git_head", "$ks_git_diff"),
("$ks_git_base|$ks_git_head", "$ks_git_merge_base"),
("$ks_git_commit", "$ks_git_langs"),
("$ks_git_commit|$arg_format", "$ks_git_archive"),
];
impl Maps {
fn add(&mut self, dir: &str, rows: &Rows, unlaid: &Rows) {
let commits: Vec<(String, String)> = rows
.commit
.iter()
.map(|(spelling, oid)| (format!("{dir}|{spelling}"), format!("{dir}/{oid}")))
.filter(|(key, _)| key.len() <= MAX_KEY_BYTES)
.collect();
for (_, variable) in COMMIT_SOURCES {
self.rows
.entry(variable)
.or_default()
.extend(commits.iter().cloned());
}
let kinds = [
&rows.refs,
&rows.branches,
&rows.branch,
&rows.tags,
&rows.tag,
&unlaid.tree,
&unlaid.entry,
&unlaid.blob,
&unlaid.blame,
&unlaid.log,
&rows.diff,
&rows.merge_base,
&rows.languages,
&rows.archive,
];
for ((source, variable), kind) in FILE_MAPS.iter().zip(kinds) {
let into = self.rows.entry(variable).or_default();
for (key, file) in kind {
let key = match *variable {
"$ks_git_diff" | "$ks_git_merge_base" => {
let (base, head) = key.split_once('|').unwrap_or((key, ""));
format!("{dir}/{base}|{dir}/{head}")
}
_ if source.starts_with("$ks_git_") => format!("{dir}/{key}"),
_ => format!("{dir}|{key}"),
};
if key.len() <= MAX_KEY_BYTES {
into.push((key, format!("{dir}/{file}")));
}
}
}
self.rows.entry("$ks_git_archive_name").or_default().extend(
rows.archive_name
.iter()
.map(|(file, name)| (format!("{dir}/{file}"), name.clone())),
);
}
fn take(&mut self, variable: &str) -> Vec<(String, String)> {
without_case_twins(self.rows.remove(variable).unwrap_or_default())
}
}
/// nginx folds map keys to lowercase and refuses two that differ only by case. both spellings get dropped.
fn without_case_twins(mut rows: Vec<(String, String)>) -> Vec<(String, String)> {
let mut spellings: HashMap<String, HashSet<&str>> = HashMap::new();
for (key, _) in &rows {
spellings
.entry(key.to_ascii_lowercase())
.or_default()
.insert(key);
}
let twins: HashSet<String> = spellings
.into_iter()
.filter(|(_, keys)| keys.len() > 1)
.map(|(lower, _)| lower)
.collect();
rows.retain(|(key, _)| !twins.contains(&key.to_ascii_lowercase()));
rows
}
const ROUTE: &str = "/xrpc/org.tangled.git.";
fn route(nsid: &str, checks: &[(&str, &str)], target: &str) -> Block {
reroute(checked(&format!("{ROUTE}{nsid}"), checks), target)
}
fn checked(path: &str, checks: &[(&str, &str)]) -> Block {
checks
.iter()
.fold(Block::exact(path), |location, (variable, error)| {
location.block(empty(variable, error))
})
}
const REPO: [(&str, &str); 2] = [
("$arg_repo", "invalid_request"),
("$ks_atp_repo", "git_repo_not_found"),
];
const MORE_THAN_TWO_REPOS: &str = r"(^|&)repos=.*&repos=.*&repos=";
fn at_ref<'a>(extra: &[(&'a str, &'a str)]) -> Vec<(&'a str, &'a str)> {
REPO.iter()
.copied()
.chain([("$ks_git_commit", "git_ref_not_found")])
.chain(extra.iter().copied())
.collect()
}
pub fn locations(generation_path: &str, limits: &crate::config::ServeLimits) -> Vec<Block> {
let root = format!("{generation_path}/{GIT_XRPC_DIR}/");
let json = |location: Block| {
cached(
location.block(Block::new("types", NONE).directive("application/json", ["json"])),
"application/json",
crate::nginx::REVALIDATE,
)
};
// the lexicon wants raw bytes. blobs stay octet-stream and never get sniffed.
let blob = |location: Block| {
cached(
location.block(Block::new("types", NONE)),
"application/octet-stream",
crate::nginx::REVALIDATE,
)
.directive(
"add_header",
["X-Content-Type-Options", "nosniff", "always"],
)
.directive(
"add_header",
[
"Content-Security-Policy",
"default-src 'none'; style-src 'unsafe-inline'; sandbox",
"always",
],
)
};
let internal = |prefix: &str| Block::new("location", [prefix]).directive("internal", NONE);
let by_path = |nsid: &str, checks: Vec<(&str, &str)>, kind: paths::Kind, missing: &str| {
let maps = format!("$ks_git_{}", kind.name());
let in_maps = match kind {
paths::Kind::Blob => format!("/_ks_git_blob/{maps}"),
_ => format!("/_ks_git_xrpc/{maps}"),
};
let (from, target) = paths::target(generation_path, kind);
let location = checks.iter().fold(
Block::exact(format!("{ROUTE}{nsid}")),
|location, (variable, error)| location.block(empty(variable, error)),
);
reroute(
location
.block(reroute(Block::when(&maps, "!=", ""), in_maps))
.block(empty("$ks_git_path", missing))
.blocks((kind == paths::Kind::Log).then(|| empty("$ks_git_page", missing)))
.directive("set", ["$ks_git_from", &from]),
target,
)
};
let laid_out = |kind: paths::Kind, missing: &str| {
let style = |location| match kind {
paths::Kind::Blob => blob(location),
_ => json(location),
};
crate::nginx::read_limited(
paths::serve_location(generation_path, kind, missing, style),
limits,
)
};
let archive = cached(
Block::new("location", ["/_ks_git_archive/"])
.directive("internal", NONE)
.block(
Block::new("types", NONE)
.directive("application/gzip", ["gz"])
.directive("application/zip", ["zip"]),
),
"application/octet-stream",
crate::nginx::REVALIDATE,
)
.directive(
"add_header",
["Content-Disposition", "$ks_git_archive_name", "always"],
);
let file = |nsid: &str, variable: &str, missing: &str, extra: &[(&str, &str)]| {
let mut checks = at_ref(extra);
checks.push((variable, missing));
route(nsid, &checks, &format!("/_ks_git_xrpc/${}", &variable[1..]))
};
let by_repo = |nsid: &str, variable: &str, missing: &str, extra: &[(&str, &str)]| {
let mut checks: Vec<(&str, &str)> =
REPO.iter().copied().chain(extra.iter().copied()).collect();
checks.push((variable, missing));
route(nsid, &checks, &format!("/_ks_git_xrpc/${}", &variable[1..]))
};
vec![
by_repo("listRefs", "$ks_git_refs", "invalid_request", &[]),
by_repo("listBranches", "$ks_git_branches", "invalid_request", &[]),
by_repo(
"getBranch",
"$ks_git_branch",
"git_branch_not_found",
&[("$arg_name", "invalid_request")],
),
by_repo("listTags", "$ks_git_tags", "invalid_request", &[]),
by_repo(
"getTag",
"$ks_git_tag",
"git_ref_not_found",
&[("$arg_tag", "invalid_request")],
),
by_path(
"getTree",
at_ref(&[]),
paths::Kind::Tree,
"git_path_not_found",
),
by_path(
"getEntry",
at_ref(&[("$arg_path", "invalid_request")]),
paths::Kind::Entry,
"git_entry_not_found",
),
by_path(
"getBlob",
at_ref(&[("$arg_path", "invalid_request")]),
paths::Kind::Blob,
"git_file_not_found",
),
by_path(
"getBlame",
[
("$arg_ref", "invalid_request"),
("$arg_path", "invalid_request"),
]
.into_iter()
.chain(at_ref(&[]))
.collect(),
paths::Kind::Blame,
"git_not_projected",
),
by_path(
"listCommits",
[("$arg_ref", "invalid_request")]
.into_iter()
.chain(at_ref(&[]))
.collect(),
paths::Kind::Log,
"git_not_projected",
),
reroute(
checked(
&format!("{ROUTE}getDiff"),
&[
("$arg_base", "invalid_request"),
("$arg_head", "invalid_request"),
]
.into_iter()
.chain(REPO)
.chain([
("$ks_git_base", "git_ref_not_found"),
("$ks_git_head", "git_ref_not_found"),
("$ks_git_diff", "git_not_projected"),
])
.collect::<Vec<_>>(),
)
.block(Refused::Unless("paths", "").block()),
"/_ks_git_xrpc/$ks_git_diff",
),
reroute(
checked(
"/xrpc/org.tangled.temp.git.getMergeBase",
&[("$arg_repos", "invalid_request")],
)
.block(reroute(
Block::when("$args", "~*", MORE_THAN_TWO_REPOS),
"/_ks_error/invalid_request",
))
// two different repos are a fork and its parent, and no one repo's projection holds both
.block(Refused::Distinct("repos").block())
// `$arg_*` reads the query, this set is what makes `$arg_repo` exist.
.directive(
"set",
["$args", "repo=$arg_repos&base=$arg_base&head=$arg_head"],
)
.blocks(
[
("$arg_base", "invalid_request"),
("$arg_head", "invalid_request"),
REPO[1],
("$ks_git_base", "git_ref_not_found"),
("$ks_git_head", "git_ref_not_found"),
("$ks_git_merge_base", "git_not_projected"),
]
.into_iter()
.map(|(variable, error)| empty(variable, error)),
)
.block(reroute(
Block::when("$ks_git_merge_base", "~", format!("/{NO_MERGE_BASE}$")),
"/_ks_error/git_no_merge_base",
)),
"/_ks_git_xrpc/$ks_git_merge_base",
),
file("listLanguages", "$ks_git_langs", "git_not_projected", &[]),
route(
"getArchive",
&[
("$arg_ref", "invalid_request"),
("$arg_prefix", "invalid_request"),
]
.into_iter()
.chain(at_ref(&[("$ks_git_archive", "git_not_projected")]))
.collect::<Vec<_>>(),
"/_ks_git_archive/$ks_git_archive",
),
crate::nginx::read_limited(
json(internal("/_ks_git_xrpc/")).directive("alias", [root.clone()]),
limits,
),
crate::nginx::read_limited(
blob(internal("/_ks_git_blob/")).directive("alias", [root.clone()]),
limits,
),
crate::nginx::read_limited(archive.directive("alias", [root]), limits),
paths::decode_location(),
laid_out(paths::Kind::Tree, "git_path_not_found"),
laid_out(paths::Kind::Entry, "git_entry_not_found"),
laid_out(paths::Kind::Blob, "git_file_not_found"),
laid_out(paths::Kind::Blame, "git_not_projected"),
laid_out(paths::Kind::Log, "git_not_projected"),
]
}
pub fn served_dirs(served: &RepoStore) -> (PathBuf, PathBuf, PathBuf) {
(
served.pool(POOL_KIND),
served.kind_dir(SEAL_KIND),
served.kind_dir(MAPS_KIND),
)
}
pub fn last_seal(served: &RepoStore) -> Option<String> {
std::fs::read_to_string(served.kind_dir(LAST_SEALED)).ok()
}
pub fn swept_for(served: &RepoStore, seals: &BTreeSet<String>) -> bool {
read_json::<BTreeSet<String>>(&served.kind_dir(LAST_SWEPT)).as_ref() == Some(seals)
}
pub fn mark_swept(served: &RepoStore, seals: &BTreeSet<String>) -> anyhow::Result<()> {
Ok(atomic_write(
&served.kind_dir(LAST_SWEPT),
&serde_json::to_vec(seals)?,
)?)
}
pub fn kept_served(served: &RepoStore, seals: &BTreeSet<String>) -> Option<BTreeSet<PathBuf>> {
let pool = served.pool(POOL_KIND);
let mut kept = BTreeSet::new();
let maps = served.kind_dir(MAPS_KIND);
for key in seals {
let sealed = read_json::<Kept>(&maps.join(key).join(KEPT_FILE)).or_else(|| {
let sealed = served.read_file::<Sealed>(SEAL_KIND, key)?;
Some(Kept {
files: sealed.files,
paths: sealed.paths,
})
})?;
kept.insert(served.file_path(SEAL_KIND, key));
kept.insert(maps.join(key));
kept.extend(sealed.files.keys().map(|file| pool.join(file)));
kept.extend(sealed.paths.kept(&pool));
}
Some(kept)
}