use std::convert::TryFrom;
use crate::{GitVersion, SemanticVersion, VersionError, VersionString};
use regex::Regex;
pub fn run_git_describe() -> Result<String, VersionError> {
let git_string = std::process::Command::new("git")
.args(&[
"describe",
"--always",
"--dirty=-dirty",
"--long",
"--match=*v[0-9]*.[0-9]*.[0-9]*",
"--abbrev=8",
])
.output()
.map_err(|_| VersionError::Command)?
.stdout;
Ok(std::str::from_utf8(&git_string)
.map_err(|_| VersionError::UTF8)?
.trim()
.to_string())
}
pub fn parse_git_describe(git_string: &str) -> Result<GitVersion, VersionError> {
let parser = Regex::new(
r"(?x) # insignificant whitespace mode (for comments)
^ # line start
(
(?:(?P<prefix>[\w\d]+)-)? # Start the version with an optional prefix that ends with a hyphen
v # the whole semver block starts with v
(?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+) # semver numbers
(?P<rc>-rc)? # '-rc' is optional
-(?P<commits>\d+) # '-<n>' mandatory commit-counter
-g # '-g' precedes the hash if a tag was found
)? # semver block is optional (missing if no
# tag / release was made yet)
(?P<hash>[\w\d]{8}) # 8 alphanumeric digits for the hash
(?P<dirty>-dirty)? # optional '-dirty' flag
$ # line end (therefore, call trim() before)
",
)
.map_err(|_| VersionError::Regex)?;
let captures = parser
.captures(git_string)
.ok_or_else(|| VersionError::NoGitHash(VersionString::try_from(git_string).unwrap()))?;
let semver = match (
captures.name("major"),
captures.name("minor"),
captures.name("patch"),
captures.name("commits"),
) {
(Some(major), Some(minor), Some(patch), Some(commits)) => {
let rc = captures.name("rc").is_some();
Some(SemanticVersion {
major: major
.as_str()
.parse::<u32>()
.map_err(|_| VersionError::InvalidSemver)?,
minor: minor
.as_str()
.parse::<u32>()
.map_err(|_| VersionError::InvalidSemver)?,
patch: patch
.as_str()
.parse::<u32>()
.map_err(|_| VersionError::InvalidSemver)?,
rc,
commits: commits
.as_str()
.parse::<u32>()
.map_err(|_| VersionError::InvalidSemver)?,
})
}
(None, None, None, None) => None,
_ => {
return Err(VersionError::IncompleteSemver);
}
};
let hash_str = captures
.name("hash")
.expect("The git hash should be matched in any case.")
.as_str();
let hash = u32::from_str_radix(hash_str, 16)
.expect("The git hash is not hexadecimal")
.to_be_bytes();
let dirty = captures.name("dirty").is_some();
let version_string = heapless::String::try_from(git_string).unwrap();
Ok(GitVersion {
semver,
hash,
dirty,
git_string: version_string,
})
}
#[cfg(test)]
mod tests {
use super::*;
fn make_ver(
semver: Option<SemanticVersion>,
hash: [u8; 4],
dirty: bool,
git_string: &str,
) -> GitVersion {
let version_string = heapless::String::try_from(git_string).unwrap();
GitVersion {
semver,
hash,
dirty,
git_string: version_string,
}
}
#[test]
fn parser() -> Result<(), crate::types::VersionError> {
assert_eq!(
super::parse_git_describe("v0.1.0-0-g01234567")?,
make_ver(
Some(SemanticVersion {
major: 0,
minor: 1,
patch: 0,
rc: false,
commits: 0,
}),
[0x01, 0x23, 0x45, 0x67],
false,
"v0.1.0-0-g01234567"
)
);
assert_eq!(
super::parse_git_describe("v0.1.0-0-g01234567-dirty")?,
make_ver(
Some(SemanticVersion {
major: 0,
minor: 1,
patch: 0,
rc: false,
commits: 0,
}),
[0x01, 0x23, 0x45, 0x67],
true,
"v0.1.0-0-g01234567-dirty"
)
);
assert_eq!(
super::parse_git_describe("v0.1.0-rc-0-g01234567")?,
make_ver(
Some(SemanticVersion {
major: 0,
minor: 1,
patch: 0,
rc: true,
commits: 0,
}),
[0x01, 0x23, 0x45, 0x67],
false,
"v0.1.0-rc-0-g01234567"
)
);
Ok(())
}
#[test]
fn test_parser_with_prefix() {
assert_eq!(
super::parse_git_describe("some711-v0.1.0-rc-0-g01234567").unwrap(),
make_ver(
Some(SemanticVersion {
major: 0,
minor: 1,
patch: 0,
rc: true,
commits: 0,
}),
[0x01, 0x23, 0x45, 0x67],
false,
"some711-v0.1.0-rc-0-g01234567"
)
);
}
#[test]
fn test_git_describe_command() {
let version = super::run_git_describe().unwrap();
let git_version = super::parse_git_describe(&version).unwrap();
assert!(git_version.semver.is_some());
assert_eq!(git_version.hash.len(), 4);
assert!(git_version.git_string.len() >= 13);
}
}