use crate::util::{check_status, run_with_cancel};
use async_trait::async_trait;
use repolith_core::action::Action;
use repolith_core::types::{ActionId, BuildError, BuildOutput, Ctx, Sha256};
use sha2::{Digest, Sha256 as ShaHasher};
use std::path::PathBuf;
use tokio::process::Command;
pub struct GitClone {
pub id: ActionId,
pub repo_url: String,
pub path: PathBuf,
pub deps: Vec<ActionId>,
}
#[async_trait]
impl Action for GitClone {
fn id(&self) -> ActionId {
self.id.clone()
}
fn deps(&self) -> Vec<ActionId> {
self.deps.clone()
}
async fn input_hash(&self, ctx: &Ctx) -> Result<Sha256, BuildError> {
let mut cmd = Command::new("git");
cmd.args(ls_remote_args(&self.repo_url));
let out = run_with_cancel(cmd, &ctx.cancel).await?;
if !out.status.success() {
return Err(BuildError::UpstreamUnreachable(format!(
"git ls-remote {}: {}",
self.repo_url,
String::from_utf8_lossy(&out.stderr).trim()
)));
}
let sha1 = String::from_utf8_lossy(&out.stdout)
.split_whitespace()
.next()
.unwrap_or_default()
.to_string();
if sha1.is_empty() {
return Err(BuildError::UpstreamUnreachable(format!(
"git ls-remote {} returned no HEAD",
self.repo_url
)));
}
Ok(hash_url_and_sha(&self.repo_url, &sha1))
}
async fn execute(&self, ctx: &Ctx) -> Result<BuildOutput, BuildError> {
let path_str = self
.path
.to_str()
.ok_or_else(|| BuildError::Io(format!("non-utf8 path: {:?}", self.path)))?;
if self.path.join(".git").is_dir() {
let mut fetch = Command::new("git");
fetch.args(["-C", path_str, "fetch", "origin", "--quiet"]);
check_status(&run_with_cancel(fetch, &ctx.cancel).await?)?;
let mut reset = Command::new("git");
reset.args(["-C", path_str, "reset", "--hard", "FETCH_HEAD", "--quiet"]);
check_status(&run_with_cancel(reset, &ctx.cancel).await?)?;
} else {
if let Some(parent) = self.path.parent()
&& !parent.as_os_str().is_empty()
{
std::fs::create_dir_all(parent)
.map_err(|e| BuildError::Io(format!("create_dir_all: {e}")))?;
}
let mut clone = Command::new("git");
clone.args(clone_args(&self.repo_url, path_str));
check_status(&run_with_cancel(clone, &ctx.cancel).await?)?;
}
let mut head = Command::new("git");
head.args(["-C", path_str, "rev-parse", "HEAD"]);
let head_out = run_with_cancel(head, &ctx.cancel).await?;
if !head_out.status.success() {
return Err(BuildError::CommandFailed {
exit_code: head_out.status.code().unwrap_or(-1),
stderr: String::from_utf8_lossy(&head_out.stderr).trim().to_string(),
});
}
let sha1 = String::from_utf8_lossy(&head_out.stdout).trim().to_string();
let mut h = ShaHasher::new();
h.update(sha1.as_bytes());
Ok(BuildOutput {
output_hash: Sha256(h.finalize().into()),
stdout: format!("HEAD {sha1}"),
})
}
}
fn hash_url_and_sha(url: &str, sha1: &str) -> Sha256 {
let mut h = ShaHasher::new();
h.update(url.as_bytes());
h.update(b":");
h.update(sha1.as_bytes());
Sha256(h.finalize().into())
}
fn ls_remote_args(url: &str) -> [&str; 4] {
["ls-remote", "--", url, "HEAD"]
}
fn clone_args<'a>(url: &'a str, path: &'a str) -> [&'a str; 5] {
["clone", "--quiet", "--", url, path]
}
#[cfg(test)]
mod argv_tests {
use super::{clone_args, ls_remote_args};
fn assert_separator_before(args: &[&str], url: &str) {
let dash = args
.iter()
.position(|a| *a == "--")
.unwrap_or_else(|| panic!("`--` argv separator missing from {args:?}"));
let url_pos = args
.iter()
.position(|a| *a == url)
.unwrap_or_else(|| panic!("url `{url}` missing from {args:?}"));
assert!(
dash < url_pos,
"`--` (idx {dash}) must precede url (idx {url_pos}) in {args:?}"
);
}
#[test]
fn ls_remote_argv_has_separator() {
let url = "https://example.com/r.git";
assert_separator_before(&ls_remote_args(url), url);
}
#[test]
fn clone_argv_has_separator() {
let url = "https://example.com/r.git";
let path = "/tmp/r";
assert_separator_before(&clone_args(url, path), url);
}
}