use crate::config::Config;
use anyhow::{Context, Result, anyhow, bail};
use std::path::{Path, PathBuf};
use std::process::Command;
pub trait GitClient: Send + Sync {
fn init(&self, path: &Path, branch: Option<&str>) -> impl Future<Output = Result<()>> + Send;
fn checkout(
&self,
repo_url: &str,
git_ref: &str,
) -> impl Future<Output = Result<PathBuf>> + Send;
}
pub struct SystemGitClient {
config: Config,
}
impl SystemGitClient {
pub fn new(config: Config) -> Self {
Self { config }
}
}
impl GitClient for SystemGitClient {
async fn init(&self, path: &Path, branch: Option<&str>) -> Result<()> {
let mut cmd = Command::new("git");
cmd.arg("init");
if let Some(b) = branch {
cmd.arg(format!("--initial-branch={}", b));
}
let output = cmd
.arg(path)
.output()
.context("Failed to execute git init")?;
if !output.status.success() {
bail!(
"git init failed at {:?}. Reason: {}",
path,
String::from_utf8_lossy(&output.stderr)
);
}
Ok(())
}
async fn checkout(&self, repo_url: &str, git_ref: &str) -> Result<PathBuf> {
let repo_dir = get_repo_dir(&self.config, repo_url);
if !repo_dir.exists() {
let output = Command::new("git")
.args(["clone", "--no-checkout", "--filter=blob:none", repo_url])
.arg(&repo_dir)
.output()
.context("Failed to execute git clone")?;
if !output.status.success() {
bail!(
"git clone failed for {}. Reason: {}",
repo_url,
String::from_utf8_lossy(&output.stderr)
);
}
}
let output = Command::new("git")
.current_dir(&repo_dir)
.args(["fetch", "origin", git_ref])
.output()
.context("Failed to execute git fetch")?;
if !output.status.success() {
return Err(anyhow!(
"git fetch failed for {} at {}. Reason: {}",
repo_url,
git_ref,
String::from_utf8_lossy(&output.stderr)
));
}
let output = Command::new("git")
.current_dir(&repo_dir)
.args(["checkout", "FETCH_HEAD"])
.output()
.context("Failed to execute git checkout")?;
if !output.status.success() {
bail!(
"git checkout failed for {}. Reason: {}",
git_ref,
String::from_utf8_lossy(&output.stderr)
);
}
Ok(repo_dir)
}
}
pub fn get_repo_dir(config: &Config, repo_url: &str) -> PathBuf {
let safe_name = repo_url
.replace("://", "_")
.replace("/", "_")
.replace(":", "_");
config.git_cache_dir.join(safe_name)
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs;
#[tokio::test]
async fn test_system_git_client_local_checkout() -> Result<()> {
let tmp_dir = tempfile::Builder::new().prefix("gdenv-test").tempdir()?;
let tmp_data_dir = tempfile::Builder::new()
.prefix("gdenv-test-data-dir")
.tempdir()?;
let source_repo = tmp_dir.path().join("source_repo");
fs::create_dir_all(&source_repo)?;
let config = Config::setup(Some(&tmp_data_dir.path()))?;
let git_client = SystemGitClient::new(config);
git_client.init(&source_repo, Some("main")).await?;
Command::new("git")
.current_dir(&source_repo)
.args(["config", "user.email", "test@example.com"])
.status()?;
Command::new("git")
.current_dir(&source_repo)
.args(["config", "user.name", "test"])
.status()?;
let file_path = source_repo.join("hello.txt");
fs::write(&file_path, "world")?;
Command::new("git")
.current_dir(&source_repo)
.args(["add", "."])
.status()?;
Command::new("git")
.current_dir(&source_repo)
.args(["commit", "-m", "initial commit"])
.status()?;
let repo_url = source_repo
.to_str()
.ok_or(anyhow::anyhow!("Invalid path"))?;
let checked_out_path = git_client.checkout(repo_url, "main").await?;
assert!(checked_out_path.exists());
assert!(checked_out_path.join("hello.txt").exists());
let content = fs::read_to_string(checked_out_path.join("hello.txt"))?;
assert_eq!(content, "world");
Ok(())
}
}