1use std::path::{Path, PathBuf};
4use std::process::Command;
5
6use thiserror::Error;
7
8#[derive(Debug, Error)]
9pub enum GitError {
10 #[error("git command failed: {0}")]
11 Command(String),
12 #[error("io: {0}")]
13 Io(#[from] std::io::Error),
14}
15
16pub fn clone_or_fetch(url: &str, dest: &Path) -> Result<(), GitError> {
18 if dest.exists() && dest.join(".git").exists() {
19 run_git(dest, ["fetch", "--tags", "--prune", "--force"])?;
20 Ok(())
21 } else {
22 if let Some(parent) = dest.parent() {
23 std::fs::create_dir_all(parent)?;
24 }
25 run_git(
26 &PathBuf::from("."),
27 [
28 "clone",
29 "--quiet",
30 "--no-checkout",
31 url,
32 &dest.to_string_lossy(),
33 ],
34 )?;
35 Ok(())
36 }
37}
38
39pub fn checkout(repo: &Path, gitref: &str) -> Result<(), GitError> {
41 run_git(repo, ["checkout", "--quiet", "--detach", gitref])
42}
43
44pub fn head_sha(repo: &Path) -> Result<String, GitError> {
46 let out = Command::new("git")
47 .arg("-C")
48 .arg(repo)
49 .args(["rev-parse", "HEAD"])
50 .output()?;
51 if !out.status.success() {
52 return Err(GitError::Command(
53 String::from_utf8_lossy(&out.stderr).into_owned(),
54 ));
55 }
56 Ok(String::from_utf8_lossy(&out.stdout).trim().to_string())
57}
58
59fn run_git<'a, A>(cwd: &Path, args: A) -> Result<(), GitError>
60where
61 A: IntoIterator<Item = &'a str>,
62{
63 let out = Command::new("git").current_dir(cwd).args(args).output()?;
64 if out.status.success() {
65 Ok(())
66 } else {
67 Err(GitError::Command(
68 String::from_utf8_lossy(&out.stderr).into_owned(),
69 ))
70 }
71}