Menu
AkurAI-Build
publicLatest change 30e6b8c347048d64c2c048dfd05a8ded219d37f6 - Host authenticated Git repositories over Smart HTTP with repo host/sync CLI by Ólafur Búi Ólafsson
use std::path::{Path, PathBuf};
use anyhow::{Context, Result, ensure};
use serde::Serialize;
use crate::config::validate_ref;
/// Maximum age (seconds) of a cached mirror before a fetch is forced.
const MIRROR_TTL: u64 = 30;
/// Bound the number of tree entries we resolve per-file commits for.
const MAX_ENTRIES: usize = 500;
#[derive(Clone)]
pub struct RepoTree {
root: PathBuf,
}
#[derive(Serialize)]
pub struct TreeEntry {
pub name: String,
pub kind: String,
pub size: u64,
pub commit_message: Option<String>,
pub commit_at: Option<i64>,
}
#[derive(Serialize)]
pub struct LatestCommit {
pub sha: String,
pub message: String,
pub author: String,
pub committed_at: i64,
}
#[derive(Serialize)]
pub struct RepoCounts {
pub commits: u64,
pub branches: u64,
pub tags: u64,
}
#[derive(Serialize)]
pub struct TreeResponse {
pub reference: String,
pub path: String,
pub entries: Vec<TreeEntry>,
pub latest_commit: Option<LatestCommit>,
pub counts: RepoCounts,
pub readme: Option<String>,
pub readme_name: Option<String>,
}
impl RepoTree {
pub fn new(data_root: &Path) -> Result<Self> {
let root = data_root.join("mirrors");
std::fs::create_dir_all(&root)?;
Ok(Self { root })
}
fn mirror_path(&self, repository: &str) -> PathBuf {
self.root.join(format!("{repository}.git"))
}
/// Ensure a bare mirror exists and is reasonably fresh.
fn sync_mirror(&self, repository: &str, url: &str) -> Result<PathBuf> {
let path = self.mirror_path(repository);
if path.join("HEAD").is_file() {
let fresh = std::fs::metadata(path.join("FETCH_HEAD"))
.ok()
.and_then(|m| m.modified().ok())
.and_then(|t| t.elapsed().ok())
.is_some_and(|age| age.as_secs() < MIRROR_TTL);
if !fresh {
let status = git(&path, ["fetch", "--prune", "origin"])?;
ensure!(status, "git fetch failed for {repository}");
}
return Ok(path);
}
std::fs::create_dir_all(&path)?;
let parent = path
.parent()
.context("mirror path has no parent")?
.to_path_buf();
let name = path
.file_name()
.context("mirror path has no file name")?
.to_string_lossy()
.to_string();
let status = git_output(
&parent,
["clone", "--bare", "--filter=blob:none", url, &name],
)
.is_ok();
ensure!(status, "git clone --bare failed for {repository}");
Ok(path)
}
/// Resolve the default branch of the mirror.
fn default_ref(&self, path: &Path) -> Result<String> {
if let Ok(head) = git_output(path, ["symbolic-ref", "--short", "HEAD"])
&& !head.is_empty()
{
return Ok(head);
}
Ok("HEAD".to_string())
}
pub fn tree(
&self,
repository: &str,
url: &str,
reference: Option<&str>,
sub_path: &str,
) -> Result<TreeResponse> {
if let Some(reference) = reference {
validate_ref(reference)?;
}
ensure!(!sub_path.contains(".."), "invalid tree path");
let path = self.sync_mirror(repository, url)?;
let reference = match reference {
Some(value) => value.to_string(),
None => self.default_ref(&path)?,
};
let spec = if sub_path.is_empty() {
reference.clone()
} else {
format!("{reference}:{sub_path}")
};
let raw = git_output(&path, ["ls-tree", "--long", "-z", &spec])
.with_context(|| format!("ls-tree failed for {repository}@{reference}"))?;
let mut entries = parse_tree(&raw);
entries.sort_by(|a, b| {
(a.kind != "tree")
.cmp(&(b.kind != "tree"))
.then_with(|| a.name.to_lowercase().cmp(&b.name.to_lowercase()))
});
// Per-file latest commit (bounded).
for entry in entries.iter_mut().take(MAX_ENTRIES) {
let entry_path = if sub_path.is_empty() {
entry.name.clone()
} else {
format!("{sub_path}/{}", entry.name)
};
if let Ok(line) = git_output(
&path,
[
"log",
"-1",
"--format=%s%x00%ct",
&reference,
"--",
&entry_path,
],
) && let Some((message, ts)) = line.split_once('\0')
{
entry.commit_message = Some(message.trim().to_string());
entry.commit_at = ts.trim().parse::<i64>().ok();
}
}
let latest_commit = git_output(
&path,
["log", "-1", "--format=%H%x00%s%x00%an%x00%ct", &reference],
)
.ok()
.and_then(|line| parse_commit(&line));
let counts = RepoCounts {
commits: git_output(&path, ["rev-list", "--count", &reference])
.ok()
.and_then(|v| v.trim().parse().ok())
.unwrap_or(0),
branches: count_lines(&git_output(&path, ["branch", "--list"]).unwrap_or_default()),
tags: count_lines(&git_output(&path, ["tag", "--list"]).unwrap_or_default()),
};
let (readme, readme_name) = read_readme(&path, &reference, sub_path);
Ok(TreeResponse {
reference,
path: sub_path.to_string(),
entries,
latest_commit,
counts,
readme,
readme_name,
})
}
}
fn parse_tree(raw: &str) -> Vec<TreeEntry> {
// Each record: "<mode> <type> <sha> <size>\t<name>\0"
raw.split('\0')
.filter(|record| !record.is_empty())
.filter_map(|record| {
let (meta, name) = record.split_once('\t')?;
let mut fields = meta.split_whitespace();
let _mode = fields.next()?;
let kind = fields.next()?;
let _sha = fields.next()?;
let size = fields.next().unwrap_or("0");
Some(TreeEntry {
name: name.to_string(),
kind: kind.to_string(),
size: size.parse().unwrap_or(0),
commit_message: None,
commit_at: None,
})
})
.collect()
}
fn parse_commit(line: &str) -> Option<LatestCommit> {
let mut fields = line.split('\0');
let sha = fields.next()?.trim().to_string();
let message = fields.next()?.trim().to_string();
let author = fields.next()?.trim().to_string();
let committed_at = fields.next()?.trim().parse().ok()?;
if sha.is_empty() {
return None;
}
Some(LatestCommit {
sha,
message,
author,
committed_at,
})
}
fn read_readme(path: &Path, reference: &str, sub_path: &str) -> (Option<String>, Option<String>) {
for candidate in ["README.md", "README.markdown", "README", "README.txt"] {
let spec = if sub_path.is_empty() {
format!("{reference}:{candidate}")
} else {
format!("{reference}:{sub_path}/{candidate}")
};
if let Ok(content) = git_output(path, ["show", &spec])
&& !content.is_empty()
{
return (Some(content), Some(candidate.to_string()));
}
}
(None, None)
}
fn count_lines(value: &str) -> u64 {
value.lines().filter(|l| !l.trim().is_empty()).count() as u64
}
fn git<const N: usize>(cwd: &Path, args: [&str; N]) -> Result<bool> {
let status = crate::git_process::command(cwd).args(args).status()?;
Ok(status.success())
}
fn git_output<const N: usize>(cwd: &Path, args: [&str; N]) -> Result<String> {
crate::git_process::output(cwd, args)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parses_ls_tree_records() {
let raw = "100644 blob abc123 42\tREADME.md\x0040000 tree def456 -\tsrc\x00";
let entries = parse_tree(raw);
assert_eq!(entries.len(), 2);
assert_eq!(entries[0].name, "README.md");
assert_eq!(entries[0].kind, "blob");
assert_eq!(entries[0].size, 42);
assert_eq!(entries[1].name, "src");
assert_eq!(entries[1].kind, "tree");
}
#[test]
fn parses_commit_line() {
let commit =
parse_commit("abc\0Initial commit\0Alice\x001700000000").expect("valid commit fixture");
assert_eq!(commit.sha, "abc");
assert_eq!(commit.message, "Initial commit");
assert_eq!(commit.author, "Alice");
assert_eq!(commit.committed_at, 1700000000);
}
#[test]
fn rejects_empty_commit_sha() {
assert!(parse_commit("\0msg\0author\x00123").is_none());
}
}