Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 40 additions & 3 deletions crates/walgit-git/src/pkt.rs
Original file line number Diff line number Diff line change
Expand Up @@ -358,19 +358,43 @@ pub async fn read_ls_refs_args<R: AsyncRead + Unpin>(
/// Arguments for the v2 `object-info` command.
#[derive(Debug, Default, Clone)]
pub struct ObjectInfoRequest {
pub size: bool,
pub oids: Vec<String>,
}

pub fn parse_object_info(cmd: &V2Command) -> ObjectInfoRequest {
let mut req = ObjectInfoRequest::default();
for a in &cmd.args {
if let Some(o) = a.strip_prefix("oid ") {
req.oids.push(o.to_string());
}
parse_object_info_line(&mut req, a);
}
req
}

fn parse_object_info_line(req: &mut ObjectInfoRequest, line: &str) {
if line == "size" {
req.size = true;
} else if let Some(oid) = line.strip_prefix("oid ") {
req.oids.push(oid.to_string());
}
}

/// Read the command-specific section after the v2 command delimiter.
pub async fn read_object_info_args<R: AsyncRead + Unpin>(
mut reader: R,
mut req: ObjectInfoRequest,
) -> Result<ObjectInfoRequest, GitError> {
loop {
match read_pkt_line(&mut reader).await? {
Some(PktLine::Data(data)) => {
parse_object_info_line(&mut req, String::from_utf8_lossy(&data).trim_end());
}
Some(PktLine::Delim) => {}
Some(PktLine::Flush | PktLine::ResponseEnd) | None => break,
}
}
Ok(req)
}

fn io_to_git(e: std::io::Error) -> GitError {
GitError::Io(e)
}
Expand Down Expand Up @@ -447,4 +471,17 @@ mod tests {
assert_eq!(req.prefixes, vec!["refs/heads/ref-199"]);
assert!(req.peel);
}

#[tokio::test]
async fn read_object_info_args_reads_size_and_oids_after_delimiter() {
let mut body = Vec::new();
encode_data(&mut body, b"size\n");
encode_data(&mut body, b"oid 1111111111111111111111111111111111111111\n");
encode_flush(&mut body);
let req = read_object_info_args(std::io::Cursor::new(body), ObjectInfoRequest::default())
.await
.unwrap();
assert!(req.size);
assert_eq!(req.oids, vec!["1111111111111111111111111111111111111111"]);
}
}
61 changes: 61 additions & 0 deletions crates/walgit-server/src/cache.rs
Original file line number Diff line number Diff line change
Expand Up @@ -245,6 +245,65 @@ impl RefIndexCache {
}
}

// ---------------------------------------------------------------------------
// Object information (protocol v2)
// ---------------------------------------------------------------------------

#[derive(Clone, PartialEq, Eq, Hash)]
struct ObjectInfoKey {
repo: String,
version: String,
oid: gix_hash::ObjectId,
}

/// Bounded cache of object sizes. The manifest version keeps negative results
/// from surviving a push that makes an object visible.
#[derive(Clone)]
pub struct ObjectInfoCache {
inner: Cache<ObjectInfoKey, Option<u64>>,
}

impl ObjectInfoCache {
pub fn new(max_entries: usize) -> Self {
Self {
inner: Cache::builder().max_capacity(max_entries as u64).build(),
}
}

fn key(repo: &str, version: Option<&Version>, oid: gix_hash::ObjectId) -> ObjectInfoKey {
ObjectInfoKey {
repo: repo.to_string(),
version: version.map(|v| v.as_str().to_string()).unwrap_or_default(),
oid,
}
}

pub fn get(
&self,
repo: &str,
version: Option<&Version>,
oid: gix_hash::ObjectId,
) -> Option<Option<u64>> {
let value = self.inner.get(&Self::key(repo, version, oid));
if value.is_some() {
metrics::counter!("walgit_cache_object_info_hit").increment(1);
} else {
metrics::counter!("walgit_cache_object_info_miss").increment(1);
}
value
}

pub fn insert(
&self,
repo: &str,
version: Option<&Version>,
oid: gix_hash::ObjectId,
size: Option<u64>,
) {
self.inner.insert(Self::key(repo, version, oid), size);
}
}

// ---------------------------------------------------------------------------
// ServerCaches — aggregate held by AppState
// ---------------------------------------------------------------------------
Expand All @@ -254,6 +313,7 @@ impl RefIndexCache {
pub struct ServerCaches {
pub ref_advert: RefAdvertCache,
pub ref_index: RefIndexCache,
pub object_info: ObjectInfoCache,
/// Rendered sha-addressed web API JSON (immutable): key = repo\0kind\0sha\0path.
pub api_immutable: Cache<String, bytes::Bytes>,
}
Expand All @@ -263,6 +323,7 @@ impl ServerCaches {
Self {
ref_advert: RefAdvertCache::new(cfg.cache.ref_advert_entries),
ref_index: RefIndexCache::new(cfg.cache.ref_advert_entries.max(64)),
object_info: ObjectInfoCache::new(cfg.cache.object_info_entries),
api_immutable: Cache::builder()
.max_capacity(64 * 1024 * 1024)
.weigher(|k: &String, v: &bytes::Bytes| {
Expand Down
92 changes: 84 additions & 8 deletions crates/walgit-server/src/smart.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ use crate::repo::RepoRoute;
use crate::stream::{VecWriter, body_to_async_read, maybe_gunzip, write_body_pipe};
use tracing::Instrument;

const MAX_OBJECT_INFO_OIDS: usize = 1024;

/// Cache-control headers for smart endpoints (info/refs and pkt responses).
fn no_cache_headers() -> [(axum::http::HeaderName, &'static str); 3] {
[
Expand Down Expand Up @@ -151,6 +153,7 @@ async fn v2_capability_advert(
// packfile section: auth, WAL sync, materialization progress. Both engines frame their sections that way.
fetch.push_str(" sideband-all packfile-uris packfile-indexes");
pktline::encode_text(buf, &format!("{fetch}\n"));
pktline::encode_text(buf, "object-info=size\n");
pktline::encode_text(buf, "server-option\n");
let fmt = match handle.local().object_format() {
walgit_git::ObjectFormat::Sha1 => "sha1",
Expand Down Expand Up @@ -333,16 +336,89 @@ async fn upload_pack_v2(
))
}
"object-info" => {
let _guard = handle.sync().await.map_err(wal_err)?;
if let Some(r) = not_served_here(st, &route.id, "git-upload-pack").await {
return Ok(r);
}
let req = walgit_git::pkt::parse_object_info(&cmd);
let req = walgit_git::pkt::read_object_info_args(reader, req)
.await
.map_err(git_err)?;
if !req.size {
return Err(ApiError::BadRequest(
"object-info currently requires the size attribute".into(),
));
}
if req.oids.len() > MAX_OBJECT_INFO_OIDS {
return Ok(git_err_response(
"git-upload-pack",
&format!(
"walgit: object-info request has {} object ids; maximum is {MAX_OBJECT_INFO_OIDS}",
req.oids.len()
),
));
}
let (_guard, access) = handle.sync_objects().await.map_err(wal_err)?;
let repo_key = route.id.to_string();
let version = handle.manifest_version();
let oids: Vec<Option<gix_hash::ObjectId>> = req
.oids
.iter()
.map(|hex| gix_hash::ObjectId::from_hex(hex.as_bytes()).ok())
.collect();
let sizes = match access {
walgit_wal::ObjectAccess::Local => {
let repo = handle.local().gix();
oids.iter()
.map(|oid| {
let Some(oid) = oid else { return None };
if let Some(size) =
st.caches.object_info.get(&repo_key, version.as_ref(), *oid)
{
return size;
}
let size = gix_object::FindHeader::try_header(&repo.objects, oid)
.ok()
.flatten()
.map(|header| header.size);
st.caches
.object_info
.insert(&repo_key, version.as_ref(), *oid, size);
size
})
.collect()
}
walgit_wal::ObjectAccess::Remote(packs) => {
let results = futures::future::join_all(oids.iter().map(|oid| {
let packs = packs.clone();
let cache = st.caches.object_info.clone();
let repo_key = repo_key.clone();
let version = version.clone();
async move {
let Some(oid) = oid else { return Ok(None) };
if let Some(size) = cache.get(&repo_key, version.as_ref(), *oid) {
return Ok(size);
}
let size = packs.header(oid).await?.map(|(_, size)| size);
cache.insert(&repo_key, version.as_ref(), *oid, size);
Ok::<Option<u64>, walgit_wal::WalError>(size)
}
}))
.await;
results
.into_iter()
.collect::<Result<Vec<_>, _>>()
.map_err(wal_err)?
}
};
let mut sizes_buf = Vec::with_capacity(256);
let repo = handle.local().gix();
for hex in &req.oids {
let size = gix_hash::ObjectId::from_hex(hex.as_bytes())
.ok()
.and_then(|oid| repo.find_object(oid).ok())
.map_or(-1, |o| o.data.len() as i64);
pktline::encode_text(&mut sizes_buf, &format!("size {size}\n"));
pktline::encode_text(&mut sizes_buf, "size\n");
for (hex, size) in req.oids.iter().zip(sizes) {
match size {
Some(size) => {
pktline::encode_text(&mut sizes_buf, &format!("{hex} {size}\n"));
}
None => pktline::encode_text(&mut sizes_buf, &format!("{hex} \n")),
}
}
pktline::encode_flush(&mut sizes_buf);
Ok(text_response(
Expand Down
Loading