lux-lib 0.58.0

Library for the lux package manager for Lua
Documentation
use crate::{config::Config, fs, git::url::RemoteGitUrl};
use git2::{AutotagOption, Cred, FetchOptions, RemoteCallbacks, Repository};
use itertools::Itertools;
use miette::Diagnostic;
use thiserror::Error;

#[derive(Debug, Error, Diagnostic)]
pub enum GitError {
    #[error(transparent)]
    #[diagnostic(transparent)]
    Fs(#[from] fs::FsError),
    #[error("error initializing temporary bare git repository to fetch metadata")]
    BareRepoInit(#[source] git2::Error),
    #[error("error initializing remote repository '{0}' to fetch metadata")]
    RemoteInit(String, #[source] git2::Error),
    #[error("error fetching from remote repository '{0}'")]
    RemoteFetch(String, #[source] git2::Error),
    #[error("error listing remote refs for '{0}'")]
    RemoteList(String, #[source] git2::Error),
    #[error("could not determine latest tag or commit sha for '{0}'")]
    NoTagOrCommitSha(String),
}

pub(crate) enum SemVerTagOrSha {
    SemVerTag(String),
    CommitSha(String),
}

#[tracing::instrument(level = "trace")]
pub(crate) fn latest_semver_tag_or_commit_sha(
    url: &RemoteGitUrl,
    config: &Config,
) -> Result<SemVerTagOrSha, GitError> {
    match latest_semver_tag(url, config)? {
        Some(tag) => Ok(SemVerTagOrSha::SemVerTag(tag)),
        None => {
            let sha = latest_commit_sha(url, config)?
                .ok_or(GitError::NoTagOrCommitSha(url.to_string()))?;
            Ok(SemVerTagOrSha::CommitSha(sha))
        }
    }
}

#[tracing::instrument(level = "trace")]
fn latest_semver_tag(url: &RemoteGitUrl, config: &Config) -> Result<Option<String>, GitError> {
    let temp_dir = fs::tempfile::tempdir()?;

    let url_str = url.to_string();
    let repo = Repository::init_bare(&temp_dir).map_err(GitError::BareRepoInit)?;
    let mut remote = repo
        .remote_anonymous(&url_str)
        .map_err(|err| GitError::RemoteInit(url_str.clone(), err))?;
    let mut callbacks = RemoteCallbacks::new();
    callbacks.credentials(mk_credentials_callback(url, config));
    let mut fetch_opts = FetchOptions::new();
    fetch_opts.download_tags(AutotagOption::All);
    fetch_opts.remote_callbacks(callbacks);
    remote
        .fetch(&[] as &[&str], Some(&mut fetch_opts), None)
        .map_err(|err| GitError::RemoteFetch(url_str.clone(), err))?;
    let refs = remote
        .list()
        .map_err(|err| GitError::RemoteList(url_str.clone(), err))?;
    Ok(refs
        .iter()
        .filter_map(|head| {
            let tag_name = head.name().strip_prefix("refs/tags/")?;
            let version_str = tag_name.strip_prefix('v').unwrap_or(tag_name);
            if let Ok(version) = semver::Version::parse(version_str) {
                Some((tag_name.to_string(), version))
            } else {
                None
            }
        })
        .sorted_by(|(_, a), (_, b)| b.cmp(a))
        .map(|(version_str, _)| version_str)
        .collect_vec()
        .first()
        .cloned())
}

fn mk_credentials_callback(
    url: &RemoteGitUrl,
    config: &Config,
) -> impl FnMut(&str, Option<&str>, git2::CredentialType) -> Result<Cred, git2::Error> {
    let access_token = config.access_token(url.host()).map(|token| {
        (
            token.username().to_string(),
            unsafe { token.password() }.to_string(),
        )
    });
    move |_url, username_from_url, _allowed_types| {
        if let Some((username, password)) = &access_token {
            Cred::userpass_plaintext(username, password)
        } else {
            Cred::ssh_key_from_agent(username_from_url.unwrap_or("git"))
        }
    }
}

fn latest_commit_sha(url: &RemoteGitUrl, config: &Config) -> Result<Option<String>, GitError> {
    let temp_dir = fs::tempfile::tempdir()?;
    let url_str = url.to_string();
    let repo = Repository::init_bare(&temp_dir).map_err(GitError::BareRepoInit)?;
    let mut remote = repo
        .remote_anonymous(&url_str)
        .map_err(|err| GitError::RemoteInit(url_str.clone(), err))?;
    let mut callbacks = RemoteCallbacks::new();
    callbacks.credentials(mk_credentials_callback(url, config));
    let mut fetch_opts = FetchOptions::new();
    fetch_opts.remote_callbacks(callbacks);
    remote
        .fetch(&[] as &[&str], Some(&mut fetch_opts), None)
        .map_err(|err| GitError::RemoteFetch(url_str.clone(), err))?;
    let refs = remote
        .list()
        .map_err(|err| GitError::RemoteList(url_str.clone(), err))?;
    Ok(refs.iter().find_map(|head| match head.name() {
        "refs/heads/HEAD" => Some(head.oid().to_string()),
        "refs/heads/main" => Some(head.oid().to_string()),
        "refs/heads/master" => Some(head.oid().to_string()),
        _ => None,
    }))
}

#[cfg(test)]
mod tests {

    use super::*;
    use crate::config::ConfigBuilder;

    fn test_config() -> Config {
        ConfigBuilder::new().unwrap().build().unwrap()
    }

    #[tokio::test]
    async fn test_latest_semver_tag_http() {
        if std::env::var("LUX_SKIP_IMPURE_TESTS").unwrap_or("0".into()) == "1" {
            println!("Skipping impure test");
            return;
        }
        let url = "https://github.com/lumen-oss/lux.git".parse().unwrap();
        assert!(latest_semver_tag(&url, &test_config()).unwrap().is_some());
    }

    #[tokio::test]
    #[cfg(feature = "ssh-tests")]
    async fn test_latest_semver_tag_ssh_user() {
        let url = "git@github.com:lumen-oss/lux.git".parse().unwrap();
        assert!(latest_semver_tag(&url, &test_config()).unwrap().is_some());
    }

    #[tokio::test]
    #[cfg(feature = "ssh-tests")]
    async fn test_latest_semver_tag_ssh_schema() {
        let url = "ssh://github.com/lumen-oss/lux.git".parse().unwrap();
        assert!(latest_semver_tag(&url, &test_config()).unwrap().is_some());
    }

    #[tokio::test]
    async fn test_latest_commit_sha_http() {
        if std::env::var("LUX_SKIP_IMPURE_TESTS").unwrap_or("0".into()) == "1" {
            println!("Skipping impure test");
            return;
        }
        let url = "https://github.com/lumen-oss/lux.git".parse().unwrap();
        assert!(latest_commit_sha(&url, &test_config()).unwrap().is_some());
    }

    #[tokio::test]
    #[cfg(feature = "ssh-tests")]
    async fn test_latest_commit_sha_ssh_user() {
        let url = "git@github.com:lumen-oss/lux.git".parse().unwrap();
        assert!(latest_commit_sha(&url, &test_config()).unwrap().is_some());
    }

    #[tokio::test]
    #[cfg(feature = "ssh-tests")]
    async fn test_latest_commit_sha_ssh_schema() {
        let url = "ssh://github.com/lumen-oss/lux.git".parse().unwrap();
        assert!(latest_commit_sha(&url, &test_config()).unwrap().is_some());
    }
}