Speed up opening files in large repositories

repo_blob and repo_readme resolved the blob through radicle-surf, which
also walks history to find the last commit that touched the path.
libgit2 ignores the commit-graph and diffs every commit, so opening a
file last changed long ago took a long time and varied by how far back
the file changed, not by its size.

Resolve the content with a direct tree lookup and the last commit via
`git rev-list -1`, falling back to the libgit2 walk when git is
unavailable.
This commit is contained in:
Rūdolfs Ošiņš
2026-07-03 11:30:51 +02:00
parent 2bfd580bbe
commit 0bfe2d7220
2 changed files with 137 additions and 39 deletions
+30 -12
View File
@@ -20,26 +20,44 @@ pub struct Blob {
content: String,
}
impl<T: AsRef<[u8]>> From<surf::blob::Blob<T>> for Blob {
fn from(blob: surf::blob::Blob<T>) -> Self {
let content = match blob.size() {
s if s > MAX_BLOB_SIZE && blob.is_binary() => {
impl Blob {
/// Build a blob from its raw bytes and the commit that last touched it.
///
/// Resolving the last commit is the caller's responsibility so it can pick
/// a fast strategy (e.g. the commit-graph) instead of an expensive history
/// walk on large repositories.
pub fn new(id: Oid, is_binary: bool, commit: Commit, content: &[u8]) -> Self {
let mime_type = infer::get(content)
.map(|i| i.mime_type().to_string())
.unwrap_or_else(|| "application/octet-stream".to_string());
let content = match content.len() {
s if s > MAX_BLOB_SIZE && is_binary => {
String::from("Payload too large, content has been truncated")
}
_ => match std::str::from_utf8(blob.content()) {
_ => match std::str::from_utf8(content) {
Ok(s) => s.to_owned(),
Err(_) => BASE64_STANDARD.encode(blob.content()),
Err(_) => BASE64_STANDARD.encode(content),
},
};
let mime_type = infer::get(blob.content()).map(|i| i.mime_type().to_string());
Blob {
id: crate::oid::from_surf(blob.object_id()),
binary: blob.is_binary(),
commit: blob.commit().clone().into(),
id,
binary: is_binary,
commit,
content,
mime_type: mime_type.unwrap_or_else(|| "application/octet-stream".to_string()),
mime_type,
}
}
}
impl<T: AsRef<[u8]>> From<surf::blob::Blob<T>> for Blob {
fn from(blob: surf::blob::Blob<T>) -> Self {
Blob::new(
crate::oid::from_surf(blob.object_id()),
blob.is_binary(),
blob.commit().clone().into(),
blob.content(),
)
}
}
+107 -27
View File
@@ -91,6 +91,51 @@ fn resolve_revision(
}
}
/// Resolve the most recent commit that modified `path`, reachable from `head`.
///
/// Fast path: `git rev-list` walks the history using the commit-graph (when
/// present), skipping the per-commit tree diff that libgit2 performs for a
/// pathspec walk. On large histories (e.g. the Linux kernel) the libgit2 walk
/// is seconds-to-minutes for a file last touched long ago, while this is
/// near-instant. Not a verification step: trust comes from the signed tip and
/// git's content-addressed DAG; the commit-graph is a local derived index over
/// those same objects. Falls back to the libgit2 walk if git is unavailable.
fn last_path_commit(
surf_repo: &surf::Repository,
repo_path: &std::path::Path,
head: git::Oid,
path: &std::path::Path,
) -> Result<repo::Commit, Error> {
let fast = std::process::Command::new("git")
.current_dir(repo_path)
.arg("rev-list")
.arg("-1")
.arg(head.to_string())
.arg("--")
// `:(literal)` disables pathspec glob matching so file names
// containing `[`, `*` or `?` (e.g. `src/pages/[id].ts`) are looked
// up verbatim instead of being treated as wildcard patterns.
.arg(format!(":(literal){}", path.display()))
.output()
.ok()
.filter(|output| output.status.success())
.and_then(|output| {
String::from_utf8_lossy(&output.stdout)
.trim()
.parse::<git::Oid>()
.ok()
});
let commit = match fast {
Some(oid) => surf_repo.commit(crate::oid::into_surf(oid))?,
None => surf_repo
.last_commit(&path, crate::oid::into_surf(head))?
.ok_or_else(|| git2::Error::from_str("no commit found for path"))?,
};
Ok(commit.into())
}
/// Collect canonical branches and tags as declared by the repository's
/// identity document. The canonical-refs rules (the `xyz.radicle.crefs`
/// payload, or a synthesized default covering the project's default branch)
@@ -328,10 +373,8 @@ pub trait Repo: Profile {
) -> Result<Option<repo::Readme>, Error> {
let profile = self.profile();
let storage_repo = profile.storage.repository(rid)?;
let repo = radicle_surf::Repository::open(storage::git::paths::repository(
&profile.storage,
&rid,
))?;
let repo_path = storage::git::paths::repository(&profile.storage, &rid);
let surf_repo = radicle_surf::Repository::open(&repo_path)?;
let paths = [
"README",
@@ -347,31 +390,48 @@ pub trait Repo: Profile {
Some(sha) => sha,
None => resolve_revision(&storage_repo, peer, revision)?,
};
let tree = storage_repo.backend.find_commit(oid.into())?.tree()?;
for path in paths
.iter()
.map(ToString::to_string)
.chain(paths.iter().map(|p| p.to_lowercase()))
{
if let Ok(blob) = repo.blob(crate::oid::into_surf(oid), &path) {
if blob.size() > MAX_BLOB_SIZE {
return Err(Error::FileTooLarge(blob.size()));
}
let Ok(entry) = tree.get_path(std::path::Path::new(&path)) else {
continue;
};
let Ok(blob) = entry
.to_object(&storage_repo.backend)
.and_then(|object| object.peel_to_blob())
else {
continue;
};
let content = match std::str::from_utf8(blob.content()) {
Ok(s) => s.to_owned(),
Err(_) => base64::engine::general_purpose::STANDARD.encode(blob.content()),
};
return Ok(Some(repo::Readme {
id: blob.object_id(),
commit: blob.commit().clone().into(),
mime_type: "text/plain".to_owned(),
path,
content,
binary: blob.is_binary(),
}));
if blob.size() > MAX_BLOB_SIZE {
return Err(Error::FileTooLarge(blob.size()));
}
let content = match std::str::from_utf8(blob.content()) {
Ok(s) => s.to_owned(),
Err(_) => base64::engine::general_purpose::STANDARD.encode(blob.content()),
};
// A failed last-commit lookup skips this candidate instead of
// failing the whole call, so the repo home still renders (at
// worst without a README), matching the pre-rewrite behaviour.
let Ok(last_commit) =
last_path_commit(&surf_repo, &repo_path, oid, std::path::Path::new(&path))
else {
continue;
};
return Ok(Some(repo::Readme {
id: radicle_surf::Oid::from(blob.id()),
commit: last_commit,
mime_type: "text/plain".to_owned(),
path,
content,
binary: blob.is_binary(),
}));
}
Ok(None)
}
@@ -405,13 +465,33 @@ pub trait Repo: Profile {
sha: Option<git::Oid>,
) -> Result<source::blob::Blob, Error> {
let profile = self.profile();
let repo = radicle_surf::Repository::open(radicle::storage::git::paths::repository(
&profile.storage,
&rid,
))?;
let oid = sha.map_or_else(|| repo.head(), |sha| Ok(crate::oid::into_surf(sha)))?;
let storage_repo = profile.storage.repository(rid)?;
let repo_path = storage::git::paths::repository(&profile.storage, &rid);
let surf_repo = radicle_surf::Repository::open(&repo_path)?;
repo.blob(oid, &path).map(Into::into).map_err(Error::from)
let oid = match sha {
Some(sha) => sha,
None => crate::oid::from_surf(surf_repo.head()?),
};
// Resolve the blob via a direct tree lookup. `surf::Repository::blob`
// additionally walks history to find the last commit that touched the
// path, which we do separately (and cheaply) below.
let commit = storage_repo.backend.find_commit(oid.into())?;
let entry = commit.tree()?.get_path(&path)?;
let blob = entry
.to_object(&storage_repo.backend)?
.into_blob()
.map_err(|_| git2::Error::from_str("path does not point to a blob"))?;
let last_commit = last_path_commit(&surf_repo, &repo_path, oid, &path)?;
Ok(source::blob::Blob::new(
blob.id().into(),
blob.is_binary(),
last_commit,
blob.content(),
))
}
fn list_repo_refs(&self, rid: identity::RepoId) -> Result<repo::RepoRefs, Error> {