spin_plugins/
git.rs

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
9/// Enables cloning and fetching the latest of a git repository to a local
10/// directory.
11pub struct GitSource {
12    /// Address to remote git repository.
13    source_url: Url,
14    /// Branch to clone/fetch.
15    branch: String,
16    /// Destination to clone repository into.
17    git_root: PathBuf,
18}
19
20impl GitSource {
21    /// Creates a new git source
22    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    /// Clones a contents of a git repository to a local directory
31    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    /// Fetches the latest changes from the source repository
49    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
64// TODO: the following and templates/git.rs are duplicates
65
66pub(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                // TODO: consider cases like insufficient permission?
101                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}