chadtmiller.com/knotless
Git hosted in your atproto account, on any stock PDS, fast enough for agents.
git clone knotless://chadtmiller.com/knotlessneeds the knotless git helper. set it up
//! A mirror's objects read in process, for pages that must be quick:
//! starting a git process costs more than these reads take. Each mirror is
//! opened once; new packs a sync brings are found when first needed.
use anyhow::{Context, Result};
use gix::revision::walk::Sorting;
use gix::traverse::commit::simple::CommitTimeOrder;
use knotless::read::{Author, Commit, Entry};
use std::collections::HashMap;
use std::path::{Path, PathBuf};
use std::sync::{LazyLock, Mutex};
static OPEN: LazyLock<Mutex<HashMap<PathBuf, gix::ThreadSafeRepository>>> =
LazyLock::new(Mutex::default);
pub struct Objects(gix::Repository);
/// The repository at `dir`, a bare mirror.
pub fn open(dir: &Path) -> Result<Objects> {
let mut open = OPEN.lock().unwrap();
let safe = match open.get(dir) {
Some(safe) => safe.clone(),
None => {
let safe = gix::ThreadSafeRepository::open(dir)?;
open.insert(dir.to_path_buf(), safe.clone());
safe
}
};
let mut repo = safe.to_thread_local();
repo.object_cache_size_if_unset(4 << 20);
Ok(Objects(repo))
}
/// The id of whatever `path` names in `tree`, if anything.
fn id_at(tree: &gix::Tree<'_>, path: &str) -> Result<Option<gix::ObjectId>> {
Ok(tree.lookup_entry_by_path(path)?.map(|e| e.oid().to_owned()))
}
impl Objects {
/// The branch HEAD names, if it names one.
pub fn head(&self) -> Option<String> {
let name = self.0.head_name().ok()??;
Some(name.shorten().to_string())
}
/// Every branch and tag, by its short name.
pub fn names(&self) -> Result<Vec<String>> {
let platform = self.0.references()?;
let mut names = Vec::new();
for r in platform.all()? {
let r = r?;
let full = r.name().as_bstr().to_string();
if let Some(short) = full
.strip_prefix("refs/heads/")
.or(full.strip_prefix("refs/tags/"))
{
names.push(short.to_string());
}
}
Ok(names)
}
fn commit(&self, rev: &str) -> Result<gix::Commit<'_>> {
let id = self
.0
.rev_parse_single(rev)
.with_context(|| format!("no revision {rev}"))?;
Ok(id.object()?.peel_to_commit()?)
}
/// The commit `rev` names, by its full id.
pub fn resolve(&self, rev: &str) -> Result<String> {
Ok(self.commit(rev)?.id.to_string())
}
pub fn tree(&self, rev: &str, path: &str) -> Result<Vec<Entry>> {
let mut tree = self.commit(rev)?.tree()?;
if !path.is_empty() {
let entry = tree
.lookup_entry_by_path(path)?
.with_context(|| format!("no {path} at {rev}"))?;
tree = entry
.object()?
.try_into_tree()
.with_context(|| format!("{path} is not a directory"))?;
}
let mut out = Vec::new();
for e in tree.iter() {
let e = e?;
let mode = e.mode();
let kind = if mode.is_tree() {
"tree"
} else if mode.is_commit() {
"commit"
} else {
"blob"
};
let size = match kind {
"blob" => Some(self.0.find_header(e.oid())?.size()),
_ => None,
};
out.push(Entry {
name: e.filename().to_string(),
mode: mode.kind().as_octal_str().to_string(),
kind: kind.into(),
sha: e.oid().to_string(),
size,
});
}
Ok(out)
}
pub fn blob(&self, rev: &str, path: &str) -> Result<Vec<u8>> {
let tree = self.commit(rev)?.tree()?;
let entry = tree
.lookup_entry_by_path(path)?
.with_context(|| format!("no {path} at {rev}"))?;
let object = entry.object()?;
anyhow::ensure!(
object.kind == gix::object::Kind::Blob,
"{path} is not a file"
);
Ok(object.detach().data)
}
/// Whether `commit` changed `path` from each of its parents: a commit
/// that left it as one parent had it is not part of its history.
fn touches(&self, commit: &gix::Commit<'_>, path: &str) -> Result<bool> {
let here = id_at(&commit.tree()?, path)?;
let mut parents = commit.parent_ids().peekable();
if parents.peek().is_none() {
return Ok(here.is_some());
}
for parent in parents {
if id_at(&parent.object()?.peel_to_commit()?.tree()?, path)? == here {
return Ok(false);
}
}
Ok(true)
}
/// History from `rev`, newest first, touching `path` if it is not
/// empty, after its first `skip` commits.
pub fn log(&self, rev: &str, path: &str, skip: usize, limit: usize) -> Result<Vec<Commit>> {
let tip = self.commit(rev)?.id;
let walk = self
.0
.rev_walk([tip])
.sorting(Sorting::ByCommitTime(CommitTimeOrder::NewestFirst))
.all()?;
let mut out = Vec::new();
let mut skipped = 0;
for info in walk {
let commit = info?.object()?;
if !path.is_empty() && !self.touches(&commit, path)? {
continue;
}
if skipped < skip {
skipped += 1;
continue;
}
out.push(summary(&commit)?);
if out.len() == limit {
break;
}
}
Ok(out)
}
/// What moving a ref to `to` brought: the commits it reaches that none
/// of `known` does, newest first, at most `limit` of them. A revision
/// in `known` that is not here is passed over.
pub fn brought(&self, to: &str, known: &[&str], limit: usize) -> Result<Vec<Commit>> {
let tip = self.commit(to)?.id;
let hidden: Vec<gix::ObjectId> = known
.iter()
.filter_map(|k| self.commit(k).ok().map(|c| c.id))
.collect();
let walk = self
.0
.rev_walk([tip])
.with_hidden(hidden)
.sorting(Sorting::ByCommitTime(CommitTimeOrder::NewestFirst))
.all()?;
let mut out = Vec::new();
for info in walk.take(limit) {
out.push(summary(&info?.object()?)?);
}
Ok(out)
}
}
fn summary(commit: &gix::Commit<'_>) -> Result<Commit> {
let author = commit.author()?;
let date = author
.time()?
.format(gix::date::time::format::ISO8601_STRICT)?;
Ok(Commit {
sha: commit.id.to_string(),
parents: commit.parent_ids().map(|p| p.to_string()).collect(),
author: Author {
name: author.name.to_string(),
email: author.email.to_string(),
date,
},
message: commit.message_raw()?.to_string().trim_end().to_string(),
})
}
#[cfg(test)]
mod tests {
use super::*;
use knotless::git::Git;
async fn git(dir: &Path, args: &[&str]) -> String {
Git::new(dir).text(args, None).await.unwrap()
}
#[tokio::test]
async fn reads_as_git_does() {
let dir = tempfile::tempdir().unwrap();
let d = dir.path();
let run = |args: Vec<&str>| {
let out = std::process::Command::new("git")
.args(args)
.current_dir(d)
.env("GIT_CONFIG_GLOBAL", "/dev/null")
.env("GIT_AUTHOR_NAME", "A")
.env("GIT_AUTHOR_EMAIL", "a@x")
.env("GIT_COMMITTER_NAME", "A")
.env("GIT_COMMITTER_EMAIL", "a@x")
.output()
.unwrap();
assert!(out.status.success(), "{out:?}");
};
run(vec!["init", "-q", "-b", "main"]);
std::fs::create_dir(d.join("src")).unwrap();
std::fs::write(d.join("src/a.rs"), "fn a() {}\n").unwrap();
std::fs::write(d.join("b"), "b\n").unwrap();
run(vec!["add", "-A"]);
run(vec!["commit", "-q", "-m", "one\n\nwith a body"]);
std::fs::write(d.join("b"), "bb\n").unwrap();
run(vec!["commit", "-qam", "two"]);
run(vec!["checkout", "-q", "-b", "side", "HEAD~1"]);
std::fs::write(d.join("src/a.rs"), "fn a() { 1 }\n").unwrap();
run(vec!["commit", "-qam", "three"]);
run(vec!["checkout", "-q", "main"]);
run(vec!["merge", "-q", "--no-edit", "side"]);
let one = git(&d.join(".git"), &["rev-parse", "HEAD~1"]).await;
run(vec![
"update-index",
"--add",
"--cacheinfo",
&format!("160000,{one},vendored"),
]);
run(vec!["commit", "-qm", "vendor"]);
run(vec!["tag", "v1"]);
run(vec!["update-ref", "refs/notes/x", "HEAD"]);
let o = open(&d.join(".git")).unwrap();
let again = open(&d.join(".git")).unwrap();
assert_eq!(o.head().as_deref(), Some("main"));
assert_eq!(
again.resolve("v1").unwrap(),
git(&d.join(".git"), &["rev-parse", "HEAD"]).await
);
let mut names = o.names().unwrap();
names.sort();
assert_eq!(names, ["main", "side", "v1"]);
let tree = o.tree("main", "").unwrap();
let shown: Vec<(&str, &str, Option<u64>)> = tree
.iter()
.map(|e| (e.name.as_str(), e.kind.as_str(), e.size))
.collect();
assert_eq!(
shown,
[
("b", "blob", Some(3)),
("src", "tree", None),
("vendored", "commit", None)
]
);
assert_eq!(tree[1].mode, "40000");
assert_eq!(o.tree("main", "src").unwrap()[0].name, "a.rs");
assert!(o.tree("main", "b").is_err());
assert!(o.tree("main", "nope").is_err());
assert!(o.tree("nope", "").is_err());
assert_eq!(o.blob("main", "src/a.rs").unwrap(), b"fn a() { 1 }\n");
assert!(o.blob("main", "src").is_err());
assert!(o.blob("main", "nope").is_err());
let subjects = |log: Vec<Commit>| {
log.iter()
.map(|c| c.message.lines().next().unwrap().to_string())
.collect::<Vec<_>>()
};
let all = o.log("main", "", 0, 10).unwrap();
assert_eq!(all.len(), 5);
assert_eq!(all[1].parents.len(), 2);
assert_eq!(all[4].message, "one\n\nwith a body");
assert_eq!(all[4].author.name, "A");
assert!(all[4].author.date.contains('T'), "{}", all[4].author.date);
assert_eq!(o.log("main", "", 1, 2).unwrap().len(), 2);
assert_eq!(subjects(o.log("main", "b", 0, 10).unwrap()), ["two", "one"]);
assert_eq!(
subjects(o.log("main", "src", 0, 10).unwrap()),
["three", "one"]
);
assert_eq!(subjects(o.log("main", "src", 1, 10).unwrap()), ["one"]);
assert!(o.log("main", "nowhere", 0, 10).unwrap().is_empty());
assert!(o.log("nope", "", 0, 10).is_err());
assert!(open(&d.join("missing")).is_err());
// What a push of main brought, past side and a ref it never saw.
assert_eq!(
subjects(o.brought("main", &["side", "gone"], 10).unwrap()),
["vendor", "Merge branch 'side'", "two"]
);
assert_eq!(o.brought("main", &[], 10).unwrap().len(), 5);
assert_eq!(o.brought("main", &[], 2).unwrap().len(), 2);
assert!(o.brought("main", &["main"], 10).unwrap().is_empty());
assert!(o.brought("nope", &[], 10).is_err());
run(vec!["checkout", "-q", "--detach"]);
assert_eq!(open(&d.join(".git")).unwrap().head(), None);
}
}