1use anyhow::Result;
2use std::io::ErrorKind;
3use std::path::{Path, PathBuf};
4use tokio::process::Command;
5use url::Url;
6
7const DEFAULT_BRANCH: &str = "main";
8
9pub struct GitSource {
12 source_url: Url,
14 branch: String,
16 git_root: PathBuf,
18}
19
20impl GitSource {
21 pub fn new(source_url: &Url, branch: Option<String>, git_root: impl AsRef<Path>) -> GitSource {
23 Self {
24 source_url: source_url.clone(),
25 branch: branch.unwrap_or_else(|| DEFAULT_BRANCH.to_owned()),
26 git_root: git_root.as_ref().to_owned(),
27 }
28 }
29
30 pub async fn clone_repo(&self) -> Result<()> {
32 let mut git = Command::new("git");
33 git.args([
34 "clone",
35 self.source_url.as_ref(),
36 "--branch",
37 &self.branch,
38 "--single-branch",
39 ])
40 .arg(&self.git_root);
41 let clone_result = git.output().await.understand_git_result();
42 if let Err(e) = clone_result {
43 anyhow::bail!("Error cloning Git repo {}: {}", self.source_url, e)
44 }
45 Ok(())
46 }
47
48 pub async fn pull(&self) -> Result<()> {
50 let mut git = Command::new("git");
51 git.arg("-C").arg(&self.git_root).arg("pull");
52 let pull_result = git.output().await.understand_git_result();
53 if let Err(e) = pull_result {
54 anyhow::bail!(
55 "Error updating Git repo at {}: {}",
56 self.git_root.display(),
57 e
58 )
59 }
60 Ok(())
61 }
62}
63
64pub(crate) enum GitError {
67 ProgramFailed(Vec<u8>),
68 ProgramNotFound,
69 Other(anyhow::Error),
70}
71
72impl std::fmt::Display for GitError {
73 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
74 match self {
75 Self::ProgramNotFound => f.write_str("`git` command not found - is git installed?"),
76 Self::Other(e) => e.fmt(f),
77 Self::ProgramFailed(stderr) => match std::str::from_utf8(stderr) {
78 Ok(s) => f.write_str(s),
79 Err(_) => f.write_str("(cannot get error)"),
80 },
81 }
82 }
83}
84
85pub(crate) trait UnderstandGitResult {
86 fn understand_git_result(self) -> Result<Vec<u8>, GitError>;
87}
88
89impl UnderstandGitResult for Result<std::process::Output, std::io::Error> {
90 fn understand_git_result(self) -> Result<Vec<u8>, GitError> {
91 match self {
92 Ok(output) => {
93 if output.status.success() {
94 Ok(output.stdout)
95 } else {
96 Err(GitError::ProgramFailed(output.stderr))
97 }
98 }
99 Err(e) => match e.kind() {
100 ErrorKind::NotFound => Err(GitError::ProgramNotFound),
102 _ => {
103 let err = anyhow::Error::from(e).context("Failed to run `git` command");
104 Err(GitError::Other(err))
105 }
106 },
107 }
108 }
109}