lux_lib/git/
utils.rs

1use std::io;
2
3use git2::{AutotagOption, FetchOptions, Repository};
4use git_url_parse::GitUrl;
5use itertools::Itertools;
6use tempdir::TempDir;
7use thiserror::Error;
8
9#[derive(Debug, Error)]
10pub enum GitError {
11    #[error("error creating temporary directory to checkout git repositotory: {0}")]
12    CreateTempDir(io::Error),
13    #[error("error initializing temporary bare git repository to fetch metadata: {0}")]
14    BareRepoInit(git2::Error),
15    #[error("error initializing remote repository '{0}' to fetch metadata: {1}")]
16    RemoteInit(String, git2::Error),
17    #[error("error fetching from remote repository '{0}': {1}")]
18    RemoteFetch(String, git2::Error),
19    #[error("error listing remote refs for '{0}': {1}")]
20    RemoteList(String, git2::Error),
21    #[error("could not determine latest tag or commit sha for {0}")]
22    NoTagOrCommitSha(String),
23}
24
25pub(crate) fn latest_semver_tag_or_commit_sha(url: &GitUrl) -> Result<String, GitError> {
26    match latest_semver_tag(url)? {
27        Some(tag) => Ok(tag),
28        None => latest_commit_sha(url)?.ok_or(GitError::NoTagOrCommitSha(url.to_string())),
29    }
30}
31
32fn latest_semver_tag(url: &GitUrl) -> Result<Option<String>, GitError> {
33    let temp_dir = TempDir::new("lux-git-meta").map_err(GitError::CreateTempDir)?;
34
35    let url_str = url.to_string();
36    let repo = Repository::init_bare(&temp_dir).map_err(GitError::BareRepoInit)?;
37    let mut remote = repo
38        .remote_anonymous(&url_str)
39        .map_err(|err| GitError::RemoteInit(url_str.clone(), err))?;
40    let mut fetch_opts = FetchOptions::new();
41    fetch_opts.download_tags(AutotagOption::All);
42    remote
43        .fetch(&[] as &[&str], Some(&mut fetch_opts), None)
44        .map_err(|err| GitError::RemoteFetch(url_str.clone(), err))?;
45    let refs = remote
46        .list()
47        .map_err(|err| GitError::RemoteList(url_str.clone(), err))?;
48    Ok(refs
49        .iter()
50        .filter_map(|head| {
51            let tag_name = head.name().strip_prefix("refs/tags/")?;
52            let version_str = tag_name.strip_prefix('v').unwrap_or(tag_name);
53            if let Ok(version) = semver::Version::parse(version_str) {
54                Some((tag_name.to_string(), version))
55            } else {
56                None
57            }
58        })
59        .sorted_by(|(_, a), (_, b)| b.cmp(a))
60        .map(|(version_str, _)| version_str)
61        .collect_vec()
62        .first()
63        .cloned())
64}
65
66fn latest_commit_sha(url: &GitUrl) -> Result<Option<String>, GitError> {
67    let temp_dir = TempDir::new("lux-git-meta").map_err(GitError::CreateTempDir)?;
68    let url_str = url.to_string();
69    let repo = Repository::init_bare(&temp_dir).map_err(GitError::BareRepoInit)?;
70    let mut remote = repo
71        .remote_anonymous(&url_str)
72        .map_err(|err| GitError::RemoteInit(url_str.clone(), err))?;
73    let mut fetch_opts = FetchOptions::new();
74    remote
75        .fetch(&[] as &[&str], Some(&mut fetch_opts), None)
76        .map_err(|err| GitError::RemoteFetch(url_str.clone(), err))?;
77    let refs = remote
78        .list()
79        .map_err(|err| GitError::RemoteList(url_str.clone(), err))?;
80    Ok(refs.iter().find_map(|head| match head.name() {
81        "refs/heads/HEAD" => Some(head.oid().to_string()),
82        "refs/heads/main" => Some(head.oid().to_string()),
83        "refs/heads/master" => Some(head.oid().to_string()),
84        _ => None,
85    }))
86}
87
88#[cfg(test)]
89mod tests {
90
91    use super::*;
92
93    #[tokio::test]
94    async fn test_latest_semver_tag() {
95        if std::env::var("LUX_SKIP_IMPURE_TESTS").unwrap_or("0".into()) == "1" {
96            println!("Skipping impure test");
97            return;
98        }
99        let url = "https://github.com/nvim-neorocks/lux.git".parse().unwrap();
100        assert!(latest_semver_tag(&url).unwrap().is_some());
101    }
102
103    #[tokio::test]
104    async fn test_latest_commit_sha() {
105        if std::env::var("LUX_SKIP_IMPURE_TESTS").unwrap_or("0".into()) == "1" {
106            println!("Skipping impure test");
107            return;
108        }
109        let url = "https://github.com/nvim-neorocks/lux.git".parse().unwrap();
110        assert!(latest_commit_sha(&url).unwrap().is_some());
111    }
112}