Line data Source code
1 : use anyhow::anyhow;
2 : use anyhow::Result;
3 : use git2::Repository;
4 : use lazy_static::lazy_static;
5 : use regex::Regex;
6 : use semver::BuildMetadata;
7 : use semver::Prerelease;
8 : use semver::Version;
9 : use std::fmt;
10 : use std::path::PathBuf;
11 :
12 : lazy_static! {
13 : static ref REGEX_DESCRIBE_LONG: Regex =
14 : Regex::new("v([0-9]+).([0-9]+).([0-9]+)-([0-9]+)-g([a-f0-9]+)").unwrap();
15 : }
16 :
17 : /// A version derived from the long `git describe` output of a repository
18 : #[derive(Clone)]
19 : pub struct GitVersion {
20 : /// The raw `git describe` output, e.g. `v1.2.3-4-gabcdef0`
21 : pub description: String,
22 : /// The major version of the latest tag
23 : pub major: u32,
24 : /// The minor version of the latest tag
25 : pub minor: u32,
26 : /// The patch version of the latest tag
27 : pub patch: u32,
28 : /// The number of commits since the latest tag, 0 for a tagged commit
29 : pub iteration: u32,
30 : /// The abbreviated hash of the described commit
31 : pub short_hash: String,
32 : }
33 :
34 : impl GitVersion {
35 16 : pub fn as_semver(&self) -> Version {
36 16 : if self.iteration == 0 {
37 13 : Version {
38 13 : major: self.major as u64,
39 13 : minor: self.minor as u64,
40 13 : patch: self.patch as u64,
41 13 : pre: Prerelease::EMPTY,
42 13 : build: BuildMetadata::EMPTY,
43 13 : }
44 : } else {
45 3 : Version {
46 3 : major: self.major as u64,
47 3 : minor: self.minor as u64,
48 3 : patch: (self.patch + 1) as u64,
49 3 : pre: Prerelease::new(&format!("dev.{}", self.iteration)).unwrap(),
50 3 : // build: BuildMetadata::new(&self.short_hash).unwrap(),
51 3 : build: BuildMetadata::EMPTY,
52 3 : }
53 : }
54 16 : }
55 : }
56 :
57 : impl fmt::Display for GitVersion {
58 1 : fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
59 1 : self.as_semver().fmt(f)
60 1 : }
61 : }
62 :
63 : /// The git information of the repository a release is built from
64 : #[derive(Clone)]
65 : pub struct GitInfo {
66 : /// The version described by git
67 : pub version: GitVersion,
68 : }
69 :
70 12 : pub fn describe_git_version(source_dir: &PathBuf) -> Result<GitVersion> {
71 : // Open the current git repository
72 12 : let repo = match Repository::open(source_dir) {
73 9 : Ok(repo) => repo,
74 3 : Err(e) => return Err(anyhow!("failed to open git repository at: {}", e)),
75 : };
76 : // Resolve the git repo description
77 9 : let mut describe_opts = git2::DescribeOptions::new();
78 9 : describe_opts.describe_tags();
79 9 : let mut format_opts = git2::DescribeFormatOptions::new();
80 9 : format_opts.always_use_long_format(true);
81 :
82 9 : let description = repo.describe(&describe_opts)?;
83 9 : let description_text = description.format(Some(&format_opts))?;
84 :
85 9 : let description_parts = match REGEX_DESCRIBE_LONG.captures(&description_text) {
86 8 : Some(parts) => parts,
87 : None => {
88 1 : return Err(anyhow!(
89 1 : "git describe output is not matching version regex: {}",
90 1 : &description_text
91 1 : ))
92 : }
93 : };
94 :
95 : Ok(GitVersion {
96 8 : description: description_text.clone(),
97 8 : major: description_parts[1].parse().map_err(|e| {
98 0 : anyhow!(
99 : "failed to parse major version {}, {}",
100 0 : &description_parts[1],
101 : e
102 : )
103 0 : })?,
104 8 : minor: description_parts[2].parse().map_err(|e| {
105 0 : anyhow!(
106 : "failed to parse minor version {}, {}",
107 0 : &description_parts[2],
108 : e
109 : )
110 0 : })?,
111 8 : patch: description_parts[3].parse().map_err(|e| {
112 0 : anyhow!(
113 : "failed to parse patch version {}, {}",
114 0 : &description_parts[3],
115 : e
116 : )
117 0 : })?,
118 8 : iteration: description_parts[4].parse().map_err(|e| {
119 0 : anyhow!(
120 : "failed to parse iteration version {}, {}",
121 0 : &description_parts[4],
122 : e
123 : )
124 0 : })?,
125 8 : short_hash: description_parts[5].to_string(),
126 : })
127 12 : }
128 :
129 9 : pub fn collect_git_info(source_dir: &PathBuf) -> Result<GitInfo> {
130 9 : let version = describe_git_version(&source_dir)?;
131 7 : Ok(GitInfo { version })
132 9 : }
133 :
134 : #[cfg(test)]
135 : mod tests {
136 : use super::*;
137 : use crate::test_s3::{git_repository, TempDir};
138 :
139 : #[test]
140 1 : fn describes_a_tagged_commit() {
141 1 : let dir = TempDir::new();
142 1 : git_repository(dir.path(), "v1.2.3", 0);
143 1 : let info = collect_git_info(&dir.path().to_path_buf()).unwrap();
144 1 : assert_eq!((info.version.major, info.version.minor, info.version.patch, info.version.iteration), (1, 2, 3, 0));
145 1 : assert_eq!(info.version.short_hash.len(), 7);
146 1 : assert!(info.version.description.starts_with("v1.2.3-0-g"), "{}", info.version.description);
147 1 : assert_eq!(info.version.to_string(), "1.2.3");
148 1 : }
149 :
150 : #[test]
151 1 : fn counts_the_commits_after_the_tag() {
152 1 : let dir = TempDir::new();
153 1 : git_repository(dir.path(), "v1.2.3", 4);
154 1 : let version = describe_git_version(&dir.path().to_path_buf()).unwrap();
155 1 : assert_eq!(version.iteration, 4);
156 1 : assert_eq!(version.as_semver().to_string(), "1.2.4-dev.4");
157 1 : }
158 :
159 : #[test]
160 1 : fn fails_outside_of_a_repository() {
161 1 : let dir = TempDir::new();
162 1 : let error = describe_git_version(&dir.path().to_path_buf()).err().unwrap();
163 1 : assert!(error.to_string().contains("failed to open git repository"), "{error}");
164 1 : }
165 :
166 : #[test]
167 1 : fn fails_for_a_tag_that_is_no_version() {
168 1 : let dir = TempDir::new();
169 1 : git_repository(dir.path(), "nightly", 1);
170 1 : let error = describe_git_version(&dir.path().to_path_buf()).err().unwrap();
171 1 : assert!(error.to_string().contains("not matching version regex"), "{error}");
172 1 : }
173 : }
|