rtx-cli 1.3.0

Polyglot runtime manager (asdf rust clone)
use std::fs::create_dir_all;
use std::path::PathBuf;

use color_eyre::eyre::Result;

use crate::cmd;
use crate::file::touch_dir;

pub struct Git {
    pub dir: PathBuf,
}

impl Git {
    pub fn new(dir: PathBuf) -> Self {
        Self { dir }
    }

    pub fn is_repo(&self) -> bool {
        self.dir.join(".git").is_dir()
    }

    pub fn remote_default_branch(&self) -> Result<String> {
        let branch = cmd!(
            "git",
            "-C",
            &self.dir,
            "symbolic-ref",
            "refs/remotes/origin/HEAD"
        )
        .read()?;

        let branch = branch.rsplit_once('/').unwrap().1;
        Ok(branch.to_string())
    }

    pub fn update(&self, gitref: Option<String>) -> Result<(String, String)> {
        let gitref = gitref.map_or_else(|| self.remote_default_branch(), Ok)?;
        debug!("updating {} to {}", self.dir.display(), gitref);
        cmd!(
            "git",
            "-C",
            &self.dir,
            "fetch",
            "--prune",
            "--update-head-ok",
            "origin",
            [gitref.as_str(), gitref.as_str()].join(":"),
        )
        .run()?;
        let prev_rev = self.current_sha()?;
        cmd!(
            "git",
            "-C",
            &self.dir,
            "-c",
            "advice.detachedHead=false",
            "-c",
            "advice.objectNameWarning=false",
            "checkout",
            "--force",
            gitref
        )
        .run()?;
        let post_rev = self.current_sha()?;
        touch_dir(&self.dir)?;

        Ok((prev_rev, post_rev))
    }

    pub fn clone(&self, url: &str) -> Result<()> {
        debug!("cloning {} to {}", url, self.dir.display());
        if let Some(parent) = self.dir.parent() {
            create_dir_all(parent)?;
        }
        match get_git_version() {
            Ok(version) => trace!("git version: {}", version),
            Err(err) => warn!(
                "failed to get git version: {}\n Git is required to use rtx.",
                err
            ),
        }
        cmd!("git", "clone", "-q", "--depth", "1", url, &self.dir).run()?;
        Ok(())
    }

    pub fn current_sha(&self) -> Result<String> {
        let sha = cmd!("git", "-C", &self.dir, "rev-parse", "HEAD").read()?;
        debug!("current sha for {}: {}", self.dir.display(), &sha);
        Ok(sha)
    }

    pub fn get_remote_url(&self) -> Option<String> {
        let res = cmd!(
            "git",
            "-C",
            &self.dir,
            "config",
            "--get",
            "remote.origin.url"
        )
        .read();
        match res {
            Ok(url) => {
                debug!("remote url for {}: {}", self.dir.display(), &url);
                Some(url)
            }
            Err(err) => {
                warn!(
                    "failed to get remote url for {}: {}",
                    self.dir.display(),
                    err
                );
                None
            }
        }
    }
}

fn get_git_version() -> Result<String> {
    let version = cmd!("git", "--version").read()?;
    Ok(version.trim().into())
}

#[cfg(test)]
mod tests {
    use tempfile::tempdir;

    use super::*;

    #[test]
    fn test_clone_and_update() {
        let dir = tempdir().unwrap().into_path();
        let git = Git::new(dir);
        git.clone("https://github.com/asdf-vm/asdf-plugins")
            .unwrap();
        let prev_rev = "f4b510d1d0c01ab2da95b80a1c1521f651cdd708".to_string();
        let latest = git.current_sha().unwrap();
        let update_result = git.update(Some(prev_rev.clone())).unwrap();
        assert_eq!(update_result, (latest.to_string(), prev_rev.to_string()));
        let update_result = git.update(None).unwrap();
        assert_eq!(update_result, (prev_rev, latest));
    }
}