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}