Skip to main content

lux_lib/git/
utils.rs

1use crate::{config::Config, fs, git::url::RemoteGitUrl};
2use git2::{AutotagOption, Cred, FetchOptions, RemoteCallbacks, Repository};
3use itertools::Itertools;
4
5pub(crate) enum SemVerTagOrSha {
6    SemVerTag(String),
7    CommitSha(String),
8}
9
10#[tracing::instrument(level = "trace")]
11pub(crate) fn latest_semver_tag_or_commit_sha(
12    url: &RemoteGitUrl,
13    config: &Config,
14) -> Result<SemVerTagOrSha, super::Error> {
15    match latest_semver_tag(url, config)? {
16        Some(tag) => Ok(SemVerTagOrSha::SemVerTag(tag)),
17        None => {
18            let sha = latest_commit_sha(url, config)?
19                .ok_or(super::Error::NoTagOrCommitSha(url.to_string()))?;
20            Ok(SemVerTagOrSha::CommitSha(sha))
21        }
22    }
23}
24
25#[tracing::instrument(level = "trace")]
26fn latest_semver_tag(url: &RemoteGitUrl, config: &Config) -> Result<Option<String>, super::Error> {
27    let temp_dir = fs::tempfile::tempdir()?;
28
29    let url_str = url.to_string();
30    let repo = Repository::init_bare(&temp_dir).map_err(super::Error::BareRepoInit)?;
31    let mut remote = repo
32        .remote_anonymous(&url_str)
33        .map_err(|err| super::Error::RemoteInit(url_str.clone(), err))?;
34    let mut callbacks = RemoteCallbacks::new();
35    callbacks.credentials(mk_credentials_callback(url, config));
36    let mut fetch_opts = FetchOptions::new();
37    fetch_opts.download_tags(AutotagOption::All);
38    fetch_opts.remote_callbacks(callbacks);
39    remote
40        .fetch(&[] as &[&str], Some(&mut fetch_opts), None)
41        .map_err(|err| super::Error::RemoteFetch(url_str.clone(), err))?;
42    let refs = remote
43        .list()
44        .map_err(|err| super::Error::RemoteList(url_str.clone(), err))?;
45    Ok(refs
46        .iter()
47        .filter_map(|head| {
48            let tag_name = head.name().strip_prefix("refs/tags/")?;
49            let version_str = tag_name.strip_prefix('v').unwrap_or(tag_name);
50            if let Ok(version) = semver::Version::parse(version_str) {
51                Some((tag_name.to_string(), version))
52            } else {
53                None
54            }
55        })
56        .sorted_by(|(_, a), (_, b)| b.cmp(a))
57        .map(|(version_str, _)| version_str)
58        .collect_vec()
59        .first()
60        .cloned())
61}
62
63fn mk_credentials_callback(
64    url: &RemoteGitUrl,
65    config: &Config,
66) -> impl FnMut(&str, Option<&str>, git2::CredentialType) -> Result<Cred, git2::Error> {
67    let access_token = config.access_token(url.host()).map(|token| {
68        (
69            token.username().to_string(),
70            unsafe { token.password() }.to_string(),
71        )
72    });
73    move |_url, username_from_url, _allowed_types| {
74        if let Some((username, password)) = &access_token {
75            Cred::userpass_plaintext(username, password)
76        } else {
77            Cred::ssh_key_from_agent(username_from_url.unwrap_or("git"))
78        }
79    }
80}
81
82fn latest_commit_sha(url: &RemoteGitUrl, config: &Config) -> Result<Option<String>, super::Error> {
83    let temp_dir = fs::tempfile::tempdir()?;
84    let url_str = url.to_string();
85    let repo = Repository::init_bare(&temp_dir).map_err(super::Error::BareRepoInit)?;
86    let mut remote = repo
87        .remote_anonymous(&url_str)
88        .map_err(|err| super::Error::RemoteInit(url_str.clone(), err))?;
89    let mut callbacks = RemoteCallbacks::new();
90    callbacks.credentials(mk_credentials_callback(url, config));
91    let mut fetch_opts = FetchOptions::new();
92    fetch_opts.remote_callbacks(callbacks);
93    remote
94        .fetch(&[] as &[&str], Some(&mut fetch_opts), None)
95        .map_err(|err| super::Error::RemoteFetch(url_str.clone(), err))?;
96    let refs = remote
97        .list()
98        .map_err(|err| super::Error::RemoteList(url_str.clone(), err))?;
99    Ok(refs.iter().find_map(|head| match head.name() {
100        "refs/heads/HEAD" => Some(head.oid().to_string()),
101        "refs/heads/main" => Some(head.oid().to_string()),
102        "refs/heads/master" => Some(head.oid().to_string()),
103        _ => None,
104    }))
105}
106
107#[cfg(all(test, any(feature = "impure_tests", feature = "ssh-tests")))]
108mod tests {
109
110    use super::*;
111    use crate::config::ConfigBuilder;
112    #[cfg(feature = "impure_tests")]
113    use flaky_test::flaky_test;
114
115    fn test_config() -> Config {
116        ConfigBuilder::new().unwrap().build().unwrap()
117    }
118
119    #[cfg(feature = "impure_tests")]
120    #[flaky_test(tokio, times = 5)]
121    async fn test_latest_semver_tag_http() {
122        let url = "https://github.com/lumen-oss/lux.git".parse().unwrap();
123        assert!(latest_semver_tag(&url, &test_config()).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, &test_config()).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, &test_config()).unwrap().is_some());
138    }
139
140    #[cfg(feature = "impure_tests")]
141    #[flaky_test(tokio, times = 5)]
142    async fn test_latest_commit_sha_http() {
143        let url = "https://github.com/lumen-oss/lux.git".parse().unwrap();
144        assert!(latest_commit_sha(&url, &test_config()).unwrap().is_some());
145    }
146
147    #[tokio::test]
148    #[cfg(feature = "ssh-tests")]
149    async fn test_latest_commit_sha_ssh_user() {
150        let url = "git@github.com:lumen-oss/lux.git".parse().unwrap();
151        assert!(latest_commit_sha(&url, &test_config()).unwrap().is_some());
152    }
153
154    #[tokio::test]
155    #[cfg(feature = "ssh-tests")]
156    async fn test_latest_commit_sha_ssh_schema() {
157        let url = "ssh://github.com/lumen-oss/lux.git".parse().unwrap();
158        assert!(latest_commit_sha(&url, &test_config()).unwrap().is_some());
159    }
160}