use crate::utils::paths::clean_path;
use anyhow::Context as _;
use git2::{Commit, Repository, Sort};
use std::{
fmt,
path::{Path, PathBuf},
};
pub const GIT_REQUEST_NOT_FOUND: &str = "Git object doesn't exist";
pub struct Repo {
pub archive_path: String,
pub path: PathBuf,
pub org: String,
pub name: String,
pub repo: Repository,
}
#[derive(Debug)]
pub struct Blob {
pub content: Vec<u8>,
pub path: String,
}
impl fmt::Debug for Repo {
fn fmt(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
write!(
formatter,
"Repo for {}/{} in the archive at {}",
self.org, self.name, self.archive_path
)
}
}
#[expect(
clippy::missing_trait_methods,
clippy::unwrap_used,
reason = "Expect to have git repo on disk"
)]
impl Clone for Repo {
fn clone(&self) -> Self {
Self {
archive_path: self.archive_path.clone(),
org: self.org.clone(),
name: self.name.clone(),
path: self.path.clone(),
repo: Repository::open(self.path.clone()).unwrap(),
}
}
}
impl Repo {
fn find(&self, query: &str) -> anyhow::Result<Vec<u8>> {
tracing::trace!(query, "Git reverse parse search");
let obj = self.repo.revparse_single(query)?;
let blob = obj.as_blob().context("Couldn't cast Git object to blob")?;
Ok(blob.content().to_owned())
}
pub fn find_blob(
archive_path: &Path,
namespace: &str,
name: &str,
remainder: &str,
commitish: &str,
) -> anyhow::Result<Blob> {
let repo = Self::new(archive_path, namespace, name)?;
let blob_path = clean_path(remainder);
let blob = repo.get_bytes_at_path(commitish, &blob_path)?;
Ok(blob)
}
pub fn new(archive_path: &Path, org: &str, name: &str) -> anyhow::Result<Self> {
let archive_path_str = archive_path.to_string_lossy();
tracing::trace!(org, name, "Creating new Repo at {archive_path_str}");
let repo_path = format!("{archive_path_str}/{org}/{name}");
Ok(Self {
archive_path: archive_path_str.into(),
org: org.into(),
name: name.into(),
path: PathBuf::from(repo_path.clone()),
repo: Repository::open(repo_path)?,
})
}
pub fn from_path(path: &Path) -> anyhow::Result<Self> {
let components: Vec<&str> = path
.components()
.filter_map(|component| component.as_os_str().to_str())
.collect();
if components.len() < 2 {
anyhow::bail!("Path must contain at least org and name");
}
let name = (*components
.last()
.ok_or_else(|| anyhow::anyhow!("Missing repo name"))?)
.to_owned();
let org = (*components
.get(components.len() - 2)
.ok_or_else(|| anyhow::anyhow!("Missing repo org"))?)
.to_owned();
let archive_path_slice = components.get(..components.len() - 2).ok_or_else(|| {
anyhow::anyhow!("Path does not contain enough components for archive_path")
})?;
let archive_path = archive_path_slice.iter().collect::<PathBuf>();
Self::new(&archive_path, &org, &name)
}
pub fn get_bytes_at_path(&self, commitish: &str, path: &str) -> anyhow::Result<Blob> {
let base_revision = format!("{commitish}:{path}");
for postfix in ["", "/index.html", ".html", "index.html"] {
let query = format!("{base_revision}{postfix}");
let blob = self.find(&query);
if let Ok(content) = blob {
let filepath = format!("{path}{postfix}");
tracing::trace!(query, "Found Git object");
return Ok(Blob {
content,
path: filepath,
});
}
}
tracing::debug!(base_revision, "Couldn't find requested Git object");
anyhow::bail!(GIT_REQUEST_NOT_FOUND)
}
pub fn iter_commits(&self) -> anyhow::Result<impl Iterator<Item = Commit>> {
let mut revwalk = self.repo.revwalk()?;
revwalk.set_sorting(Sort::TOPOLOGICAL | Sort::REVERSE)?;
revwalk.push_head()?;
Ok(revwalk
.filter_map(|found_oid| {
let oid = found_oid.ok()?;
self.repo.find_commit(oid).ok()
})
.collect::<Vec<Commit>>()
.into_iter())
}
}