use crate::retrieval::Kind;
use std::path::{Path, PathBuf};
#[derive(Debug, Clone)]
pub struct Client {
pub base_dir: PathBuf,
pub commit_author_name: String,
pub commit_author_email: String,
}
impl Client {
pub fn new(base_dir: PathBuf, commit_author_name: String, commit_author_email: String) -> Self {
Self { base_dir, commit_author_name, commit_author_email }
}
pub fn clear(&self) -> std::io::Result<()> {
if self.base_dir.exists() {
std::fs::remove_dir_all(&self.base_dir)?;
}
Ok(())
}
pub fn repo_path(&self, kind: Kind, owner: &str, repository: &str) -> PathBuf {
self.base_dir.join(kind.as_str()).join(owner).join(repository)
}
pub fn repository_exists(&self, kind: Kind, owner: &str, repository: &str) -> bool {
let repo_path = self.repo_path(kind, owner, repository);
git2::Repository::open(&repo_path).is_ok()
}
pub fn resolve_head(&self, kind: Kind, owner: &str, repository: &str) -> Result<String, super::Error> {
let repo_path = self.repo_path(kind, owner, repository);
let repo = git2::Repository::open(&repo_path)?;
let head = repo.head()?;
let commit = head.peel_to_commit()?;
Ok(commit.id().to_string())
}
pub async fn read_file(
&self,
kind: Kind,
owner: &str,
repository: &str,
commit: Option<&str>,
file_name: &str,
) -> Result<Option<(String, String)>, super::Error> {
let repo_path = self.repo_path(kind, owner, repository);
match commit {
Some(sha) => {
match read_file_at_commit(&repo_path, file_name, sha) {
Ok(content) => Ok(Some((content, sha.to_string()))),
Err(e) if is_not_found(&e) => Ok(None),
Err(e) => Err(e),
}
}
None => {
let file_path = repo_path.join(file_name);
match tokio::fs::read_to_string(&file_path).await {
Ok(content) => {
let resolved = self
.resolve_head(kind, owner, repository)
.unwrap_or_else(|_| "HEAD".to_string());
Ok(Some((content, resolved)))
}
Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(None),
Err(e) => Err(e.into()),
}
}
}
}
pub async fn read_json<T: serde::de::DeserializeOwned>(
&self,
kind: Kind,
owner: &str,
repository: &str,
commit: Option<&str>,
file_name: &str,
) -> Result<Option<(T, String)>, super::Error> {
let Some((content, resolved_commit)) =
self.read_file(kind, owner, repository, commit, file_name).await?
else {
return Ok(None);
};
let mut de = serde_json::Deserializer::from_str(&content);
let value = serde_path_to_error::deserialize(&mut de)?;
Ok(Some((value, resolved_commit)))
}
pub async fn publish<CTXEXT: crate::ctx::ContextExt>(
&self,
ctx: &crate::ctx::Context<CTXEXT, impl crate::ctx::persistent_cache::PersistentCacheClient>,
kind: Kind,
repository: &str,
files: &[(&str, &str)],
commit_message: &str,
) -> Result<(String, String), super::Error> {
let (ctx_name, ctx_email) = tokio::join!(
ctx.commit_author_name(),
ctx.commit_author_email(),
);
let commit_author_name = ctx_name
.map(|a| a.to_string())
.unwrap_or_else(|| self.commit_author_name.clone());
let commit_author_email = ctx_email
.map(|a| a.to_string())
.unwrap_or_else(|| self.commit_author_email.clone());
let owner = commit_author_name.clone();
let repo_path = self.repo_path(kind, &owner, repository);
std::fs::create_dir_all(&repo_path)?;
let repo = match git2::Repository::open(&repo_path) {
Ok(repo) => {
let mut checkout = git2::build::CheckoutBuilder::new();
checkout.force();
checkout.remove_untracked(true);
if let Ok(head) = repo.head() {
if let Ok(commit) = head.peel_to_commit() {
repo.reset(
commit.as_object(),
git2::ResetType::Hard,
Some(&mut checkout),
)?;
}
}
repo
}
Err(_) => git2::Repository::init(&repo_path)?,
};
for (name, content) in files {
let file_path = repo_path.join(name);
std::fs::write(&file_path, content)?;
}
let mut index = repo.index()?;
for (name, _) in files {
index.add_path(Path::new(name))?;
}
index.write()?;
let tree_oid = index.write_tree()?;
let parent = repo.head().ok().and_then(|h| h.peel_to_commit().ok());
if let Some(ref parent) = parent {
if parent.tree_id() == tree_oid {
return Ok((owner, parent.id().to_string()));
}
}
let tree = repo.find_tree(tree_oid)?;
let sig = git2::Signature::now(&commit_author_name, &commit_author_email)?;
let parents: Vec<&git2::Commit> = parent.iter().collect();
let commit_oid = repo.commit(
Some("HEAD"),
&sig,
&sig,
commit_message,
&tree,
&parents,
)?;
Ok((owner, commit_oid.to_string()))
}
pub async fn publish_and_push<CTXEXT: crate::ctx::ContextExt>(
&self,
ctx: &crate::ctx::Context<CTXEXT, impl crate::ctx::persistent_cache::PersistentCacheClient>,
kind: Kind,
repository: &str,
files: &[(&str, &str)],
commit_message: &str,
remote_url: &str,
token: &str,
) -> Result<(String, String), super::Error> {
let (ctx_name, ctx_email) = tokio::join!(
ctx.commit_author_name(),
ctx.commit_author_email(),
);
let commit_author_name = ctx_name
.map(|a| a.to_string())
.unwrap_or_else(|| self.commit_author_name.clone());
let commit_author_email = ctx_email
.map(|a| a.to_string())
.unwrap_or_else(|| self.commit_author_email.clone());
let owner = commit_author_name.clone();
let repo_path = self.repo_path(kind, &owner, repository);
std::fs::create_dir_all(&repo_path)?;
let repo = match git2::Repository::open(&repo_path) {
Ok(r) => r,
Err(_) => git2::Repository::init(&repo_path)?,
};
match repo.find_remote("origin") {
Ok(_) => {
repo.remote_set_url("origin", remote_url)?;
}
Err(_) => {
repo.remote("origin", remote_url)?;
}
}
{
for branch in &["main", "master"] {
let mut callbacks = git2::RemoteCallbacks::new();
callbacks.credentials(|_url, _username_from_url, _allowed_types| {
git2::Cred::userpass_plaintext("x-access-token", token)
});
let mut fetch_options = git2::FetchOptions::new();
fetch_options.remote_callbacks(callbacks);
let mut remote = repo.find_remote("origin")?;
let _ = remote.fetch(&[branch], Some(&mut fetch_options), None);
}
for branch in &["refs/remotes/origin/main", "refs/remotes/origin/master"] {
if let Ok(reference) = repo.find_reference(branch) {
if let Ok(commit) = reference.peel_to_commit() {
let mut checkout = git2::build::CheckoutBuilder::new();
checkout.force();
checkout.remove_untracked(true);
let _ = repo.reset(
commit.as_object(),
git2::ResetType::Hard,
Some(&mut checkout),
);
let local_branch = branch.replace("refs/remotes/origin/", "");
let _ = repo.set_head(
&format!("refs/heads/{}", local_branch),
);
break;
}
}
}
}
for (name, content) in files {
let file_path = repo_path.join(name);
std::fs::write(&file_path, content)?;
}
let mut index = repo.index()?;
for (name, _) in files {
index.add_path(Path::new(name))?;
}
index.write()?;
let tree_oid = index.write_tree()?;
let parent = repo.head().ok().and_then(|h| h.peel_to_commit().ok());
if let Some(ref parent) = parent {
if parent.tree_id() == tree_oid {
return Ok((owner, parent.id().to_string()));
}
}
let tree = repo.find_tree(tree_oid)?;
let sig = git2::Signature::now(&commit_author_name, &commit_author_email)?;
let parents: Vec<&git2::Commit> = parent.iter().collect();
let commit_oid = repo.commit(
Some("HEAD"), &sig, &sig, commit_message, &tree, &parents,
)?;
let mut remote = repo.find_remote("origin")?;
let head_ref = repo.head()?;
let branch_name = head_ref.shorthand().unwrap_or("main");
let refspec = format!("refs/heads/{}:refs/heads/{}", branch_name, branch_name);
let mut callbacks = git2::RemoteCallbacks::new();
callbacks.credentials(|_url, _username_from_url, _allowed_types| {
git2::Cred::userpass_plaintext("x-access-token", token)
});
let mut push_options = git2::PushOptions::new();
push_options.remote_callbacks(callbacks);
remote.push(&[&refspec], Some(&mut push_options))?;
Ok((owner, commit_oid.to_string()))
}
}
fn is_not_found(e: &super::Error) -> bool {
match e {
super::Error::Git(e) => {
e.code() == git2::ErrorCode::NotFound
|| e.class() == git2::ErrorClass::Object
|| e.class() == git2::ErrorClass::Reference
}
_ => false,
}
}
fn read_file_at_commit(
repo_path: &Path,
file_name: &str,
commit_sha: &str,
) -> Result<String, super::Error> {
let repo = git2::Repository::open(repo_path)?;
let oid = git2::Oid::from_str(commit_sha)?;
let commit = repo.find_commit(oid)?;
let tree = commit.tree()?;
let entry = tree
.get_name(file_name)
.ok_or_else(|| git2::Error::from_str(&format!("{} not found at commit {}", file_name, commit_sha)))?;
let blob = repo.find_blob(entry.id())?;
let content = std::str::from_utf8(blob.content())?;
Ok(content.to_string())
}