Skip to main content

lux_lib/git/
utils.rs

1use std::io;
2
3use git2::{AutotagOption, Cred, FetchOptions, RemoteCallbacks, Repository};
4use itertools::Itertools;
5use tempfile::tempdir;
6use thiserror::Error;
7
8use crate::git::url::RemoteGitUrl;
9
10#[derive(Debug, Error)]
11pub enum GitError {
12    #[error("error creating temporary directory to checkout git repositotory: {0}")]
13    CreateTempDir(io::Error),
14    #[error("error initializing temporary bare git repository to fetch metadata: {0}")]
15    BareRepoInit(git2::Error),
16    #[error("error initializing remote repository '{0}' to fetch metadata: {1}")]
17    RemoteInit(String, git2::Error),
18    #[error("error fetching from remote repository '{0}': {1}")]
19    RemoteFetch(String, git2::Error),
20    #[error("error listing remote refs for '{0}': {1}")]
21    RemoteList(String, git2::Error),
22    #[error("could not determine latest tag or commit sha for {0}")]
23    NoTagOrCommitSha(String),
24}
25
26pub(crate) enum SemVerTagOrSha {
27    SemVerTag(String),
28    CommitSha(String),
29}
30
31pub(crate) fn latest_semver_tag_or_commit_sha(
32    url: &RemoteGitUrl,
33) -> Result<SemVerTagOrSha, GitError> {
34    match latest_semver_tag(url)? {
35        Some(tag) => Ok(SemVerTagOrSha::SemVerTag(tag)),
36        None => {
37            let sha = latest_commit_sha(url)?.ok_or(GitError::NoTagOrCommitSha(url.to_string()))?;
38            Ok(SemVerTagOrSha::CommitSha(sha))
39        }
40    }
41}
42
43fn latest_semver_tag(url: &RemoteGitUrl) -> Result<Option<String>, GitError> {
44    let temp_dir = tempdir().map_err(GitError::CreateTempDir)?;
45
46    let url_str = url.to_string();
47    let repo = Repository::init_bare(&temp_dir).map_err(GitError::BareRepoInit)?;
48    let mut remote = repo
49        .remote_anonymous(&url_str)
50        .map_err(|err| GitError::RemoteInit(url_str.clone(), err))?;
51    let mut callbacks = RemoteCallbacks::new();
52    callbacks.credentials(|_url, username_from_url, _allowed_types| {
53        Cred::ssh_key_from_agent(username_from_url.unwrap_or("git"))
54    });
55    let mut fetch_opts = FetchOptions::new();
56    fetch_opts.download_tags(AutotagOption::All);
57    fetch_opts.remote_callbacks(callbacks);
58    remote
59        .fetch(&[] as &[&str], Some(&mut fetch_opts), None)
60        .map_err(|err| GitError::RemoteFetch(url_str.clone(), err))?;
61    let refs = remote
62        .list()
63        .map_err(|err| GitError::RemoteList(url_str.clone(), err))?;
64    Ok(refs
65        .iter()
66        .filter_map(|head| {
67            let tag_name = head.name().strip_prefix("refs/tags/")?;
68            let version_str = tag_name.strip_prefix('v').unwrap_or(tag_name);
69            if let Ok(version) = semver::Version::parse(version_str) {
70                Some((tag_name.to_string(), version))
71            } else {
72                None
73            }
74        })
75        .sorted_by(|(_, a), (_, b)| b.cmp(a))
76        .map(|(version_str, _)| version_str)
77        .collect_vec()
78        .first()
79        .cloned())
80}
81
82fn latest_commit_sha(url: &RemoteGitUrl) -> Result<Option<String>, GitError> {
83    let temp_dir = tempdir().map_err(GitError::CreateTempDir)?;
84    let url_str = url.to_string();
85    let repo = Repository::init_bare(&temp_dir).map_err(GitError::BareRepoInit)?;
86    let mut remote = repo
87        .remote_anonymous(&url_str)
88        .map_err(|err| GitError::RemoteInit(url_str.clone(), err))?;
89    let mut callbacks = RemoteCallbacks::new();
90    callbacks.credentials(|_url, username_from_url, _allowed_types| {
91        Cred::ssh_key_from_agent(username_from_url.unwrap_or("git"))
92    });
93    let mut fetch_opts = FetchOptions::new();
94    fetch_opts.remote_callbacks(callbacks);
95    remote
96        .fetch(&[] as &[&str], Some(&mut fetch_opts), None)
97        .map_err(|err| GitError::RemoteFetch(url_str.clone(), err))?;
98    let refs = remote
99        .list()
100        .map_err(|err| GitError::RemoteList(url_str.clone(), err))?;
101    Ok(refs.iter().find_map(|head| match head.name() {
102        "refs/heads/HEAD" => Some(head.oid().to_string()),
103        "refs/heads/main" => Some(head.oid().to_string()),
104        "refs/heads/master" => Some(head.oid().to_string()),
105        _ => None,
106    }))
107}
108
109#[cfg(test)]
110mod tests {
111
112    use super::*;
113
114    #[tokio::test]
115    async fn test_latest_semver_tag_http() {
116        if std::env::var("LUX_SKIP_IMPURE_TESTS").unwrap_or("0".into()) == "1" {
117            println!("Skipping impure test");
118            return;
119        }
120        let url = "https://github.com/lumen-oss/lux.git".parse().unwrap();
121        assert!(latest_semver_tag(&url).unwrap().is_some());
122    }
123
124    #[tokio::test]
125    #[cfg(feature = "ssh-tests")]
126    async fn test_latest_semver_tag_ssh_user() {
127        let url = "git@github.com:lumen-oss/lux.git".parse().unwrap();
128        assert!(latest_semver_tag(&url).unwrap().is_some());
129    }
130
131    #[tokio::test]
132    #[cfg(feature = "ssh-tests")]
133    async fn test_latest_semver_tag_ssh_schema() {
134        let url = "ssh://github.com/lumen-oss/lux.git".parse().unwrap();
135        assert!(latest_semver_tag(&url).unwrap().is_some());
136    }
137
138    #[tokio::test]
139    async fn test_latest_commit_sha_http() {
140        if std::env::var("LUX_SKIP_IMPURE_TESTS").unwrap_or("0".into()) == "1" {
141            println!("Skipping impure test");
142            return;
143        }
144        let url = "https://github.com/lumen-oss/lux.git".parse().unwrap();
145        assert!(latest_commit_sha(&url).unwrap().is_some());
146    }
147
148    #[tokio::test]
149    #[cfg(feature = "ssh-tests")]
150    async fn test_latest_commit_sha_ssh_user() {
151        let url = "git@github.com:lumen-oss/lux.git".parse().unwrap();
152        assert!(latest_commit_sha(&url).unwrap().is_some());
153    }
154
155    #[tokio::test]
156    #[cfg(feature = "ssh-tests")]
157    async fn test_latest_commit_sha_ssh_schema() {
158        let url = "ssh://github.com/lumen-oss/lux.git".parse().unwrap();
159        assert!(latest_commit_sha(&url).unwrap().is_some());
160    }
161}