use std::path::Path;
use std::process::Command;
#[derive(Debug, Clone)]
#[non_exhaustive]
pub struct CloneOptions {
pub url: String,
pub depth: Option<u32>,
pub r#ref: Option<String>,
pub index: bool,
}
impl CloneOptions {
#[must_use]
pub fn new(url: impl Into<String>) -> Self {
Self {
url: url.into(),
depth: None,
r#ref: None,
index: false,
}
}
}
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum CloneError {
#[error("git error: {0}")]
Git(String),
#[error("I/O error: {0}")]
Io(String),
}
fn run_git(args: &[&str]) -> Result<(), CloneError> {
let output = Command::new("git")
.args(args)
.output()
.map_err(|e| CloneError::Io(e.to_string()))?;
if output.status.success() {
Ok(())
} else {
let stderr = String::from_utf8_lossy(&output.stderr);
Err(CloneError::Git(stderr.trim().to_owned()))
}
}
pub fn clone_or_update(options: &CloneOptions, dest: &Path) -> Result<(), CloneError> {
if dest.join(".git").is_dir() {
let dest_str = dest
.to_str()
.ok_or_else(|| CloneError::Io("destination path is not valid UTF-8".to_owned()))?;
run_git(&["-C", dest_str, "fetch", "origin"])?;
if let Some(git_ref) = &options.r#ref {
run_git(&["-C", dest_str, "checkout", git_ref])?;
}
} else {
let dest_str = dest
.to_str()
.ok_or_else(|| CloneError::Io("destination path is not valid UTF-8".to_owned()))?;
let mut args: Vec<&str> = vec!["clone"];
let depth_str;
if let Some(depth) = options.depth {
depth_str = depth.to_string();
args.extend_from_slice(&["--depth", &depth_str]);
}
if let Some(git_ref) = &options.r#ref {
args.extend_from_slice(&["--branch", git_ref]);
}
args.push(&options.url);
args.push(dest_str);
run_git(&args)?;
}
Ok(())
}
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
mod tests {
use super::*;
use tempfile::tempdir;
#[test]
fn clone_invalid_url_returns_git_error() {
let dir = tempdir().expect("tempdir");
let dest = dir.path().join("target");
let opts = CloneOptions::new("https://invalid.example.test/no-such-repo.git");
let err = clone_or_update(&opts, &dest);
assert!(err.is_err());
let msg = err.unwrap_err().to_string();
assert!(
!msg.contains("not yet implemented"),
"should no longer return stub error, got: {msg}"
);
}
}