comfy-git-version-macro 0.3.6

Internal macro crate for git-version.
Documentation
extern crate proc_macro;

use proc_macro::{Literal, TokenStream, TokenTree};

mod utils;
use self::utils::describe_cwd;

/// Get the git version for the source code.
///
/// # Examples
///
/// ```ignore
/// const VERSION: &str = git_version!();
/// // Return "unknown" instead of hard-failing the build when `git describe`
/// // can't run (e.g. no git binary, or building outside a git checkout).
/// const VERSION: &str = git_version!(fallback = "unknown");
/// ```
#[proc_macro]
pub fn git_version(input: TokenStream) -> TokenStream {
	let fallback = parse_fallback(input);
	let version = resolve_version(describe_cwd(), fallback);

	TokenTree::Literal(Literal::string(&version)).into()
}

/// Pick the version string: the `git describe` result on success, else the
/// caller's `fallback`. With no fallback a git failure is still fatal (a build
/// that asked for the real version and can't get it should fail loudly).
fn resolve_version(described: std::io::Result<String>, fallback: Option<String>) -> String {
	match described {
		Ok(version) => version,
		Err(e) => match fallback {
			Some(fallback) => fallback,
			None => panic!("git describe failed and no `fallback = \"...\"` was provided: {}", e),
		},
	}
}

/// Parse `fallback = "literal"` out of the macro input, if present.
fn parse_fallback(input: TokenStream) -> Option<String> {
	let mut iter = input.into_iter();
	while let Some(tok) = iter.next() {
		let is_fallback = matches!(&tok, TokenTree::Ident(id) if id.to_string() == "fallback");
		if !is_fallback {
			continue;
		}
		match iter.next() {
			Some(TokenTree::Punct(p)) if p.as_char() == '=' => {}
			other => panic!("expected `=` after `fallback`, got {:?}", other),
		}
		match iter.next() {
			Some(TokenTree::Literal(lit)) => return Some(unquote(&lit.to_string())),
			other => panic!("expected a string literal after `fallback =`, got {:?}", other),
		}
	}
	None
}

/// Strip the surrounding double quotes off a string-literal token's text.
fn unquote(lit: &str) -> String {
	let bytes = lit.as_bytes();
	if bytes.len() >= 2 && bytes[0] == b'"' && bytes[bytes.len() - 1] == b'"' {
		lit[1..lit.len() - 1].to_string()
	} else {
		lit.to_string()
	}
}

#[cfg(test)]
mod tests {
	use super::{resolve_version, unquote};

	fn err() -> std::io::Result<String> {
		Err(std::io::Error::new(std::io::ErrorKind::Other, "boom"))
	}

	#[test]
	fn fallback_used_when_git_fails() {
		assert_eq!(resolve_version(err(), Some("unknown".into())), "unknown");
	}

	#[test]
	fn ok_wins_over_fallback() {
		assert_eq!(resolve_version(Ok("v1.2-3-gabc".into()), Some("unknown".into())), "v1.2-3-gabc");
	}

	#[test]
	#[should_panic(expected = "no `fallback")]
	fn no_fallback_still_panics_on_err() {
		resolve_version(err(), None);
	}

	#[test]
	fn unquote_strips_quotes() {
		assert_eq!(unquote("\"unknown\""), "unknown");
		assert_eq!(unquote("bare"), "bare");
	}
}