use camino::Utf8Path;
use tokio::process::Command;
use crate::error::{Error, Result};
pub async fn clone_at(url: &str, dest: &Utf8Path) -> Result<()> {
let output = Command::new("git")
.arg("clone")
.arg("--")
.arg(url)
.arg(dest.as_str())
.output()
.await
.map_err(|e| Error::Git(format!("spawn `git clone {url}`: {e}")))?;
if !output.status.success() {
return Err(Error::Git(format!(
"git clone {url}: {}",
String::from_utf8_lossy(&output.stderr).trim()
)));
}
Ok(())
}
pub async fn fetch(dir: &Utf8Path) -> Result<()> {
let output = Command::new("git")
.current_dir(dir.as_std_path())
.arg("fetch")
.arg("--prune")
.output()
.await
.map_err(|e| Error::Git(format!("spawn `git fetch`: {e}")))?;
if !output.status.success() {
return Err(Error::Git(format!(
"git fetch in {dir}: {}",
String::from_utf8_lossy(&output.stderr).trim()
)));
}
Ok(())
}
pub async fn checkout(dir: &Utf8Path, rev: &str) -> Result<()> {
if rev.starts_with('-') {
return Err(Error::Git(format!(
"rev `{rev}` starts with '-' (looks like a CLI option); refusing to pass to git checkout"
)));
}
let literal_err = match try_checkout(dir, rev).await {
Ok(()) => return Ok(()),
Err(e) => e,
};
if rev == "HEAD" || rev.starts_with("origin/") || rev.starts_with("refs/") {
return Err(literal_err);
}
let upstream = format!("origin/{rev}");
match try_checkout(dir, &upstream).await {
Ok(()) => Ok(()),
Err(_) => Err(literal_err),
}
}
async fn try_checkout(dir: &Utf8Path, rev: &str) -> Result<()> {
let output = Command::new("git")
.current_dir(dir.as_std_path())
.arg("-c")
.arg("advice.detachedHead=false")
.arg("checkout")
.arg(rev)
.output()
.await
.map_err(|e| Error::Git(format!("spawn `git checkout {rev}`: {e}")))?;
if !output.status.success() {
return Err(Error::Git(format!(
"git checkout {rev} in {dir}: {}",
String::from_utf8_lossy(&output.stderr).trim()
)));
}
Ok(())
}
pub async fn rev_parse(dir: &Utf8Path, rev: &str) -> Result<String> {
let output = Command::new("git")
.current_dir(dir.as_std_path())
.arg("rev-parse")
.arg(rev)
.output()
.await
.map_err(|e| Error::Git(format!("spawn `git rev-parse {rev}`: {e}")))?;
if !output.status.success() {
return Err(Error::Git(format!(
"git rev-parse {rev} in {dir}: {}",
String::from_utf8_lossy(&output.stderr).trim()
)));
}
Ok(String::from_utf8_lossy(&output.stdout).trim().to_string())
}
pub async fn current_head(dir: &Utf8Path) -> Result<String> {
rev_parse(dir, "HEAD").await
}
pub async fn repo_name_from_remote(dir: &Utf8Path) -> Option<String> {
let output = Command::new("git")
.current_dir(dir.as_std_path())
.args(["config", "--get", "remote.origin.url"])
.output()
.await
.ok()?;
if !output.status.success() {
return None;
}
let url = String::from_utf8(output.stdout).ok()?;
parse_repo_basename(url.trim())
}
fn parse_repo_basename(url: &str) -> Option<String> {
let url = url.trim();
if url.is_empty() {
return None;
}
let last = url.rsplit(['/', ':']).next()?;
let trimmed = last.trim_end_matches(".git").trim();
if trimmed.is_empty() {
None
} else {
Some(trimmed.to_string())
}
}
pub async fn is_available() -> bool {
Command::new("git")
.arg("--version")
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::null())
.status()
.await
.map(|s| s.success())
.unwrap_or(false)
}
#[cfg(test)]
mod tests {
use super::parse_repo_basename;
#[test]
fn parse_repo_basename_handles_https_with_dot_git() {
assert_eq!(
parse_repo_basename("https://github.com/yukimemi/kata.git").as_deref(),
Some("kata"),
);
}
#[test]
fn parse_repo_basename_handles_https_without_dot_git() {
assert_eq!(
parse_repo_basename("https://github.com/yukimemi/kata").as_deref(),
Some("kata"),
);
}
#[test]
fn parse_repo_basename_handles_ssh_with_dot_git() {
assert_eq!(
parse_repo_basename("git@github.com:yukimemi/kata.git").as_deref(),
Some("kata"),
);
}
#[test]
fn parse_repo_basename_handles_ssh_without_dot_git() {
assert_eq!(
parse_repo_basename("git@github.com:yukimemi/kata").as_deref(),
Some("kata"),
);
}
#[test]
fn parse_repo_basename_returns_none_on_garbage_input() {
assert!(parse_repo_basename("").is_none());
assert!(parse_repo_basename("/").is_none());
assert!(parse_repo_basename(":").is_none());
assert!(parse_repo_basename(".git").is_none());
}
}