use crate::{
constants::{
GIT_BRANCH_NAME, GIT_COMMIT_AUTHOR_EMAIL, GIT_COMMIT_AUTHOR_NAME, GIT_COMMIT_COUNT,
GIT_COMMIT_DATE_NAME, GIT_COMMIT_MESSAGE, GIT_COMMIT_TIMESTAMP_NAME, GIT_DESCRIBE_NAME,
GIT_SHA_NAME,
},
emitter::{EmitBuilder, RustcEnvMap},
key::VergenKey,
utils::fns::{add_default_map_entry, add_map_entry},
};
#[cfg(test)]
use anyhow::anyhow;
use anyhow::{Error, Result};
use git2_rs::{BranchType, Commit, DescribeFormatOptions, DescribeOptions, Reference, Repository};
use std::{
env,
path::{Path, PathBuf},
str::FromStr,
};
use time::{
format_description::{self, well_known::Iso8601},
OffsetDateTime,
};
#[derive(Clone, Copy, Debug, Default)]
#[allow(clippy::struct_excessive_bools)]
pub(crate) struct Config {
pub(crate) git_branch: bool,
pub(crate) git_commit_author_name: bool,
pub(crate) git_commit_author_email: bool,
pub(crate) git_commit_count: bool,
pub(crate) git_commit_message: bool,
pub(crate) git_commit_date: bool,
pub(crate) git_commit_timestamp: bool,
pub(crate) git_describe: bool,
git_describe_dirty: bool,
git_describe_tags: bool,
pub(crate) git_sha: bool,
git_sha_short: bool,
#[cfg(test)]
fail: bool,
}
#[cfg_attr(docsrs, doc(cfg(feature = "git")))]
impl EmitBuilder {
pub fn all_git(&mut self) -> &mut Self {
self.git_branch()
.git_commit_author_email()
.git_commit_author_name()
.git_commit_count()
.git_commit_date()
.git_commit_message()
.git_commit_timestamp()
.git_describe(false, false)
.git_sha(false)
}
fn any(&self) -> bool {
let cfg = self.git_config;
cfg.git_branch
|| cfg.git_commit_author_email
|| cfg.git_commit_author_name
|| cfg.git_commit_count
|| cfg.git_commit_date
|| cfg.git_commit_message
|| cfg.git_commit_timestamp
|| cfg.git_describe
|| cfg.git_sha
}
pub fn git_branch(&mut self) -> &mut Self {
self.git_config.git_branch = true;
self
}
pub fn git_commit_author_email(&mut self) -> &mut Self {
self.git_config.git_commit_author_email = true;
self
}
pub fn git_commit_author_name(&mut self) -> &mut Self {
self.git_config.git_commit_author_name = true;
self
}
pub fn git_commit_count(&mut self) -> &mut Self {
self.git_config.git_commit_count = true;
self
}
pub fn git_commit_date(&mut self) -> &mut Self {
self.git_config.git_commit_date = true;
self
}
pub fn git_commit_message(&mut self) -> &mut Self {
self.git_config.git_commit_message = true;
self
}
pub fn git_commit_timestamp(&mut self) -> &mut Self {
self.git_config.git_commit_timestamp = true;
self
}
pub fn git_describe(&mut self, dirty: bool, tags: bool) -> &mut Self {
self.git_config.git_describe = true;
self.git_config.git_describe_dirty = dirty;
self.git_config.git_describe_tags = tags;
self
}
pub fn git_sha(&mut self, short: bool) -> &mut Self {
self.git_config.git_sha = true;
self.git_config.git_sha_short = short;
self
}
pub(crate) fn add_git_default(
&self,
e: Error,
fail_on_error: bool,
map: &mut RustcEnvMap,
warnings: &mut Vec<String>,
rerun_if_changed: &mut Vec<String>,
) -> Result<()> {
if fail_on_error {
Err(e)
} else {
warnings.clear();
rerun_if_changed.clear();
if self.git_config.git_branch {
add_default_map_entry(VergenKey::GitBranch, map, warnings);
}
if self.git_config.git_commit_author_email {
add_default_map_entry(VergenKey::GitCommitAuthorEmail, map, warnings);
}
if self.git_config.git_commit_author_name {
add_default_map_entry(VergenKey::GitCommitAuthorName, map, warnings);
}
if self.git_config.git_commit_count {
add_default_map_entry(VergenKey::GitCommitCount, map, warnings);
}
if self.git_config.git_commit_date {
add_default_map_entry(VergenKey::GitCommitDate, map, warnings);
}
if self.git_config.git_commit_message {
add_default_map_entry(VergenKey::GitCommitMessage, map, warnings);
}
if self.git_config.git_commit_timestamp {
add_default_map_entry(VergenKey::GitCommitTimestamp, map, warnings);
}
if self.git_config.git_describe {
add_default_map_entry(VergenKey::GitDescribe, map, warnings);
}
if self.git_config.git_sha {
add_default_map_entry(VergenKey::GitSha, map, warnings);
}
Ok(())
}
}
#[cfg(not(test))]
pub(crate) fn add_git_map_entries(
&self,
path: Option<PathBuf>,
idempotent: bool,
map: &mut RustcEnvMap,
warnings: &mut Vec<String>,
rerun_if_changed: &mut Vec<String>,
) -> Result<()> {
self.inner_add_git_map_entries(path, idempotent, map, warnings, rerun_if_changed)
}
#[cfg(test)]
pub(crate) fn add_git_map_entries(
&self,
path: Option<PathBuf>,
idempotent: bool,
map: &mut RustcEnvMap,
warnings: &mut Vec<String>,
rerun_if_changed: &mut Vec<String>,
) -> Result<()> {
if self.git_config.fail {
Err(anyhow!("failed to create entries"))
} else {
self.inner_add_git_map_entries(path, idempotent, map, warnings, rerun_if_changed)
}
}
fn inner_add_git_map_entries(
&self,
path: Option<PathBuf>,
idempotent: bool,
map: &mut RustcEnvMap,
warnings: &mut Vec<String>,
rerun_if_changed: &mut Vec<String>,
) -> Result<()> {
let curr_dir = if let Some(path) = path {
path
} else {
env::current_dir()?
};
let repo = Repository::discover(curr_dir)?;
let ref_head = repo.find_reference("HEAD")?;
let git_path = repo.path().to_path_buf();
let commit = ref_head.peel_to_commit()?;
if !idempotent && self.any() {
self.add_rerun_if_changed(&ref_head, &git_path, rerun_if_changed);
}
if self.git_config.git_branch {
if let Ok(value) = env::var(GIT_BRANCH_NAME) {
add_map_entry(VergenKey::GitBranch, value, map);
} else {
add_branch_name(false, &repo, map, warnings)?;
}
}
if self.git_config.git_commit_author_email {
if let Ok(value) = env::var(GIT_COMMIT_AUTHOR_EMAIL) {
add_map_entry(VergenKey::GitCommitAuthorEmail, value, map);
} else {
add_opt_value(
commit.author().email(),
VergenKey::GitCommitAuthorEmail,
map,
warnings,
);
}
}
if self.git_config.git_commit_author_name {
if let Ok(value) = env::var(GIT_COMMIT_AUTHOR_NAME) {
add_map_entry(VergenKey::GitCommitAuthorName, value, map);
} else {
add_opt_value(
commit.author().name(),
VergenKey::GitCommitAuthorName,
map,
warnings,
);
}
}
if self.git_config.git_commit_count {
if let Ok(value) = env::var(GIT_COMMIT_COUNT) {
add_map_entry(VergenKey::GitCommitCount, value, map);
} else {
add_commit_count(false, &repo, map, warnings);
}
}
self.add_git_timestamp_entries(&commit, idempotent, map, warnings)?;
if self.git_config.git_commit_message {
if let Ok(value) = env::var(GIT_COMMIT_MESSAGE) {
add_map_entry(VergenKey::GitCommitMessage, value, map);
} else {
add_opt_value(commit.message(), VergenKey::GitCommitMessage, map, warnings);
}
}
if self.git_config.git_sha {
if let Ok(value) = env::var(GIT_SHA_NAME) {
add_map_entry(VergenKey::GitSha, value, map);
} else if self.git_config.git_sha_short {
let obj = repo.revparse_single("HEAD")?;
add_opt_value(obj.short_id()?.as_str(), VergenKey::GitSha, map, warnings);
} else {
add_map_entry(VergenKey::GitSha, commit.id().to_string(), map);
}
}
if self.git_config.git_describe {
if let Ok(value) = env::var(GIT_DESCRIBE_NAME) {
add_map_entry(VergenKey::GitDescribe, value, map);
} else {
let mut describe_opts = DescribeOptions::new();
let mut format_opts = DescribeFormatOptions::new();
let _ = describe_opts.show_commit_oid_as_fallback(true);
if self.git_config.git_describe_dirty {
let _ = format_opts.dirty_suffix("-dirty");
}
if self.git_config.git_describe_tags {
let _ = describe_opts.describe_tags();
}
let describe = repo
.describe(&describe_opts)
.map(|x| x.format(Some(&format_opts)).map_err(Error::from))??;
add_map_entry(VergenKey::GitDescribe, describe, map);
}
}
Ok(())
}
fn add_git_timestamp_entries(
&self,
commit: &Commit<'_>,
idempotent: bool,
map: &mut RustcEnvMap,
warnings: &mut Vec<String>,
) -> Result<()> {
let (sde, ts) = match env::var("SOURCE_DATE_EPOCH") {
Ok(v) => (
true,
OffsetDateTime::from_unix_timestamp(i64::from_str(&v)?)?,
),
Err(std::env::VarError::NotPresent) => (
false,
OffsetDateTime::from_unix_timestamp(commit.time().seconds())?,
),
Err(e) => return Err(e.into()),
};
if let Ok(value) = env::var(GIT_COMMIT_DATE_NAME) {
add_map_entry(VergenKey::GitCommitDate, value, map);
} else {
self.add_git_date_entry(idempotent, sde, &ts, map, warnings)?;
}
if let Ok(value) = env::var(GIT_COMMIT_TIMESTAMP_NAME) {
add_map_entry(VergenKey::GitCommitTimestamp, value, map);
} else {
self.add_git_timestamp_entry(idempotent, sde, &ts, map, warnings)?;
}
Ok(())
}
fn add_git_date_entry(
&self,
idempotent: bool,
source_date_epoch: bool,
ts: &OffsetDateTime,
map: &mut RustcEnvMap,
warnings: &mut Vec<String>,
) -> Result<()> {
if self.git_config.git_commit_date {
if idempotent && !source_date_epoch {
add_default_map_entry(VergenKey::GitCommitDate, map, warnings);
} else {
let format = format_description::parse("[year]-[month]-[day]")?;
add_map_entry(VergenKey::GitCommitDate, ts.format(&format)?, map);
}
}
Ok(())
}
fn add_git_timestamp_entry(
&self,
idempotent: bool,
source_date_epoch: bool,
ts: &OffsetDateTime,
map: &mut RustcEnvMap,
warnings: &mut Vec<String>,
) -> Result<()> {
if self.git_config.git_commit_timestamp {
if idempotent && !source_date_epoch {
add_default_map_entry(VergenKey::GitCommitTimestamp, map, warnings);
} else {
add_map_entry(
VergenKey::GitCommitTimestamp,
ts.format(&Iso8601::DEFAULT)?,
map,
);
}
}
Ok(())
}
#[allow(clippy::unused_self)]
fn add_rerun_if_changed(
&self,
ref_head: &Reference<'_>,
git_path: &Path,
rerun_if_changed: &mut Vec<String>,
) {
let mut head_path = git_path.to_path_buf();
head_path.push("HEAD");
if head_path.exists() {
rerun_if_changed.push(format!("{}", head_path.display()));
}
if let Ok(resolved) = ref_head.resolve() {
if let Some(name) = resolved.name() {
let ref_path = git_path.to_path_buf();
let path = ref_path.join(name);
if path.exists() {
rerun_if_changed.push(format!("{}", ref_path.display()));
}
}
}
}
}
#[allow(clippy::map_unwrap_or)]
fn add_opt_value(
value: Option<&str>,
key: VergenKey,
map: &mut RustcEnvMap,
warnings: &mut Vec<String>,
) {
value
.map(|val| add_map_entry(key, val, map))
.unwrap_or_else(|| add_default_map_entry(key, map, warnings));
}
fn add_commit_count(
add_default: bool,
repo: &Repository,
map: &mut RustcEnvMap,
warnings: &mut Vec<String>,
) {
let key = VergenKey::GitCommitCount;
if !add_default {
if let Ok(mut revwalk) = repo.revwalk() {
if revwalk.push_head().is_ok() {
add_map_entry(key, revwalk.count().to_string(), map);
return;
}
}
}
add_default_map_entry(key, map, warnings);
}
fn add_branch_name(
add_default: bool,
repo: &Repository,
map: &mut RustcEnvMap,
warnings: &mut Vec<String>,
) -> Result<()> {
if repo.head_detached()? {
if add_default {
add_default_map_entry(VergenKey::GitBranch, map, warnings);
} else {
add_map_entry(VergenKey::GitBranch, "HEAD", map);
}
} else {
let locals = repo.branches(Some(BranchType::Local))?;
let mut found_head = false;
for (local, _bt) in locals.filter_map(std::result::Result::ok) {
if local.is_head() {
if let Some(name) = local.name()? {
add_map_entry(VergenKey::GitBranch, name, map);
found_head = !add_default;
break;
}
}
}
if !found_head {
add_default_map_entry(VergenKey::GitBranch, map, warnings);
}
}
Ok(())
}
#[cfg(test)]
mod test {
use super::{add_branch_name, add_commit_count, add_opt_value};
use crate::{
emitter::test::count_idempotent,
key::VergenKey,
utils::repo::{clone_path, clone_test_repo, create_test_repo},
EmitBuilder,
};
use anyhow::Result;
use git2_rs::Repository;
use std::{collections::BTreeMap, env, vec};
fn repo_exists() -> Result<bool> {
let curr_dir = env::current_dir()?;
let _repo = Repository::discover(curr_dir)?;
Ok(true)
}
#[test]
#[serial_test::serial]
fn empty_email_is_default() -> Result<()> {
let mut map = BTreeMap::new();
let mut warnings = vec![];
add_opt_value(
None,
VergenKey::GitCommitAuthorEmail,
&mut map,
&mut warnings,
);
assert_eq!(1, map.len());
assert_eq!(1, warnings.len());
Ok(())
}
#[test]
#[serial_test::serial]
fn bad_revwalk_is_default() -> Result<()> {
let mut map = BTreeMap::new();
let mut warnings = vec![];
if let Ok(repo) = Repository::discover(env::current_dir()?) {
add_commit_count(true, &repo, &mut map, &mut warnings);
assert_eq!(1, map.len());
assert_eq!(1, warnings.len());
}
Ok(())
}
#[test]
#[serial_test::serial]
fn head_not_found_is_default() -> Result<()> {
create_test_repo();
clone_test_repo();
let mut map = BTreeMap::new();
let mut warnings = vec![];
if let Ok(repo) = Repository::discover(env::current_dir()?) {
add_branch_name(true, &repo, &mut map, &mut warnings)?;
assert_eq!(1, map.len());
assert_eq!(1, warnings.len());
}
let mut map = BTreeMap::new();
let mut warnings = vec![];
if let Ok(repo) = Repository::discover(clone_path()) {
add_branch_name(true, &repo, &mut map, &mut warnings)?;
assert_eq!(1, map.len());
assert_eq!(1, warnings.len());
}
Ok(())
}
#[test]
#[serial_test::serial]
fn git_all_idempotent() -> Result<()> {
let config = EmitBuilder::builder()
.idempotent()
.all_git()
.test_emit_at(None)?;
assert_eq!(9, config.cargo_rustc_env_map.len());
if repo_exists().is_ok() && !config.failed {
assert_eq!(2, count_idempotent(&config.cargo_rustc_env_map));
assert_eq!(2, config.warnings.len());
} else {
assert_eq!(9, count_idempotent(&config.cargo_rustc_env_map));
assert_eq!(9, config.warnings.len());
}
Ok(())
}
#[test]
#[serial_test::serial]
fn git_all_idempotent_no_warn() -> Result<()> {
let config = EmitBuilder::builder()
.idempotent()
.quiet()
.all_git()
.test_emit_at(None)?;
assert_eq!(9, config.cargo_rustc_env_map.len());
if repo_exists().is_ok() && !config.failed {
assert_eq!(2, count_idempotent(&config.cargo_rustc_env_map));
assert_eq!(2, config.warnings.len());
} else {
assert_eq!(9, count_idempotent(&config.cargo_rustc_env_map));
assert_eq!(9, config.warnings.len());
}
Ok(())
}
#[test]
#[serial_test::serial]
fn git_all() -> Result<()> {
let config = EmitBuilder::builder().all_git().test_emit_at(None)?;
assert_eq!(9, config.cargo_rustc_env_map.len());
if repo_exists().is_ok() && !config.failed {
assert_eq!(0, count_idempotent(&config.cargo_rustc_env_map));
assert_eq!(0, config.warnings.len());
} else {
assert_eq!(9, count_idempotent(&config.cargo_rustc_env_map));
assert_eq!(9, config.warnings.len());
}
Ok(())
}
#[test]
#[serial_test::serial]
fn git_error_fails() -> Result<()> {
let mut config = EmitBuilder::builder();
let _ = config.fail_on_error();
let _ = config.all_git();
config.git_config.fail = true;
assert!(config.test_emit().is_err());
Ok(())
}
#[test]
#[serial_test::serial]
fn git_error_defaults() -> Result<()> {
let mut config = EmitBuilder::builder();
let _ = config.all_git();
config.git_config.fail = true;
let emitter = config.test_emit()?;
assert_eq!(9, emitter.cargo_rustc_env_map.len());
assert_eq!(9, count_idempotent(&emitter.cargo_rustc_env_map));
assert_eq!(9, emitter.warnings.len());
Ok(())
}
}