mirror of
https://seed.radicle.dev/rad:z4V1sjrXqjvFdnCUbxPFqd5p4DtH5.git
synced 2026-11-08 13:10:44 +08:00
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:
@@ -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(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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> {
|
||||
|
||||
Reference in New Issue
Block a user