Skip to main content

lux_lib/git/
utils.rs

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