Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Support for Submodule Versioning #23

Merged
merged 22 commits into from
Dec 11, 2023
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .gitmodules
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
[submodule "test-child-repo"]
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it would be nice to set up a custom testing repo for this instead of adding submodules. But I will take care of this part, since I also think they should be in the same organization :)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed submodules and added more effective test. Also fixed some issues that popped up when this macro is ran in a project with no submodules.

path = test-child-repo
url = [email protected]:baxterjo/test-child-repo.git
[submodule "test_outer_directory/test-child-repo"]
path = test_outer_directory/test-child-repo
url = [email protected]:baxterjo/test-child-repo.git
140 changes: 140 additions & 0 deletions git-version-macro/src/describe_submodules.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
extern crate proc_macro;
use crate::git_dependencies;
use crate::utils::describe_modules;
use proc_macro2::{Span, TokenStream as TokenStream2};
use quote::{quote, ToTokens};
use syn::{
bracketed,
parse::{Parse, ParseStream},
punctuated::Punctuated,
token::{Comma, Eq},
Expr, Ident, LitStr,
};

macro_rules! error {
($($args:tt)*) => {
syn::Error::new(Span::call_site(), format!($($args)*))
};
}

#[derive(Default)]
pub(crate) struct GitModArgs {
describe_args: Option<Punctuated<LitStr, Comma>>,
foreach_args: Option<Punctuated<LitStr, Comma>>,
prefix: Option<LitStr>,
suffix: Option<LitStr>,
fallback: Option<Expr>,
}

impl Parse for GitModArgs {
fn parse(input: ParseStream) -> syn::Result<Self> {
let mut result = GitModArgs::default();
loop {
if input.is_empty() {
break;
}
let ident: Ident = input.parse()?;
let _: Eq = input.parse()?;
let check_dup = |dup: bool| {
if dup {
Err(error!("`{} = ` can only appear once", ident))
} else {
Ok(())
}
};
match ident.to_string().as_str() {
"describe_args" => {
check_dup(result.describe_args.is_some())?;
let content;
bracketed!(content in input);
result.describe_args = Some(Punctuated::parse_terminated(&content)?);
}
"foreach_args" => {
check_dup(result.foreach_args.is_some())?;
let content;
bracketed!(content in input);
result.foreach_args = Some(Punctuated::parse_terminated(&content)?);
}
"prefix" => {
check_dup(result.prefix.is_some())?;
result.prefix = Some(input.parse()?);
}
"suffix" => {
check_dup(result.suffix.is_some())?;
result.suffix = Some(input.parse()?);
}
"fallback" => {
check_dup(result.fallback.is_some())?;
result.fallback = Some(input.parse()?);
}
x => Err(error!("Unexpected argument name `{}`", x))?,
}
if input.is_empty() {
break;
}
let _: Comma = input.parse()?;
}
Ok(result)
}
}

pub(crate) fn git_version_modules_impl(args: GitModArgs) -> syn::Result<TokenStream2> {
let git_describe_args = args.describe_args.map_or_else(
|| vec!["--always".to_string(), "--dirty=-modified".to_string()],
|list| list.iter().map(|x| x.value()).collect(),
);

let mut git_foreach_args = args.foreach_args.map_or_else(
|| vec!["--quiet".to_string(), "--recursive".to_string()],
|list| list.iter().map(|x| x.value()).collect(),
);

if !git_foreach_args.contains(&"--quiet".to_string()) {
git_foreach_args.push("--quiet".to_string())
}

let prefix = match args.prefix {
Some(x) => x.value(),
_ => "".to_string(),
};
let suffix = match args.suffix {
Some(x) => x.value(),
_ => "".to_string(),
};

let descibe_args = format!("echo $displaypath:`git describe {}`", git_describe_args.join(" "));

let mut git_args: Vec<String> = vec!["submodule".to_string(), "foreach".to_string()];
git_args.append(&mut git_foreach_args);
git_args.push(descibe_args);

match describe_modules(&git_args) {
Ok(version) => {
let dependencies = git_dependencies()?;

let mut output: Vec<Vec<String>> = vec![];
let newline_split = version.split('\n');

for line in newline_split {
let line = line.to_string();
let line_split: Vec<&str> = line.split(':').collect();
assert!(
Copy link
Collaborator

@de-vri-es de-vri-es Nov 21, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The proc macro should never panic. This should be a compile error instead. But I also think the check should probably be removed. If people use colons in their tags, it should still just work.

But maybe we shouldn't use git submodule foreach, but just get a list of submodules and do the foreach ourselves. Then we avoid the need to parse output at all (what if the submodule path also has a colon?).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed, and removed foreach args. Also renamed describe_args back to args to be more inline with the git_version macro.

line_split.len() == 2,
// NOTE: I Don't love this split, but I think I have protected against weirdness
// by adding the only arbitrary text allowed (prefix, suffix) after the split happens.
// so unless people are using colons in their tag names, it should be fine.
"Found an unexpected colon ':' in git describe output - {}",
version
);
output.push(vec![line_split[0].to_string(), format!("{}{}{}", prefix, line_split[1], suffix)])
}

Ok(quote!({
#dependencies;
[#([#(#output),*]),*]
}))
}
Err(_) if args.fallback.is_some() => Ok(args.fallback.to_token_stream()),
Err(e) => Err(error!("{}", e)),
}
}
94 changes: 74 additions & 20 deletions git-version-macro/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ use syn::parse::{Parse, ParseStream};
use syn::punctuated::Punctuated;
use syn::token::{Comma, Eq};
use syn::{Expr, Ident, LitStr};

pub(crate) mod describe_submodules;
mod utils;
use self::utils::{describe_cwd, git_dir_cwd};

Expand All @@ -17,8 +17,7 @@ macro_rules! error {
}

fn canonicalize_path(path: &Path) -> syn::Result<String> {
path
.canonicalize()
path.canonicalize()
.map_err(|e| error!("failed to canonicalize {}: {}", path.display(), e))?
.into_os_string()
.into_string()
Expand All @@ -29,12 +28,18 @@ fn canonicalize_path(path: &Path) -> syn::Result<String> {
fn git_dependencies() -> syn::Result<TokenStream2> {
let git_dir = git_dir_cwd().map_err(|e| error!("failed to determine .git directory: {}", e))?;

let deps: Vec<_> = ["logs/HEAD", "index"].iter().flat_map(|&file| {
canonicalize_path(&git_dir.join(file)).map(Some).unwrap_or_else(|e| {
eprintln!("Failed to add dependency on the git state: {}. Git state changes might not trigger a rebuild.", e);
None
let deps: Vec<_> = ["logs/HEAD", "index"]
.iter()
.flat_map(|&file| {
canonicalize_path(&git_dir.join(file)).map(Some).unwrap_or_else(|e| {
eprintln!(
"Failed to add dependency on the git state: {}. Git state changes might not trigger a rebuild.",
e
);
None
})
})
}).collect();
.collect();

Ok(quote! {
#( include_bytes!(#deps); )*
Expand All @@ -55,7 +60,9 @@ impl Parse for Args {
fn parse(input: ParseStream) -> syn::Result<Self> {
let mut result = Args::default();
loop {
if input.is_empty() { break; }
if input.is_empty() {
break;
}
let ident: Ident = input.parse()?;
let _: Eq = input.parse()?;
let check_dup = |dup: bool| {
Expand Down Expand Up @@ -94,7 +101,9 @@ impl Parse for Args {
}
x => Err(error!("Unexpected argument name `{}`", x))?,
}
if input.is_empty() { break; }
if input.is_empty() {
break;
}
let _: Comma = input.parse()?;
}
Ok(result)
Expand Down Expand Up @@ -151,7 +160,7 @@ pub fn git_version(input: TokenStream) -> TokenStream {
fn git_version_impl(args: Args) -> syn::Result<TokenStream2> {
let git_args = args.git_args.map_or_else(
|| vec!["--always".to_string(), "--dirty=-modified".to_string()],
|list| list.iter().map(|x| x.value()).collect()
|list| list.iter().map(|x| x.value()).collect(),
);

let cargo_fallback = args.cargo_prefix.is_some() || args.cargo_suffix.is_some();
Expand All @@ -170,20 +179,65 @@ fn git_version_impl(args: Args) -> syn::Result<TokenStream2> {
if let Ok(version) = std::env::var("CARGO_PKG_VERSION") {
let prefix = args.cargo_prefix.iter();
let suffix = args.cargo_suffix;
Ok(quote!(
concat!(#(#prefix,)* #version, #suffix)
))
Ok(quote!(concat!(#(#prefix,)* #version, #suffix)))
} else if let Some(fallback) = args.fallback {
Ok(fallback.to_token_stream())
} else {
Err(error!("Unable to get git or cargo version"))
}
}
Err(_) if args.fallback.is_some() => {
Ok(args.fallback.to_token_stream())
}
Err(e) => {
Err(error!("{}", e))
}
Err(_) if args.fallback.is_some() => Ok(args.fallback.to_token_stream()),
Err(e) => Err(error!("{}", e)),
}
}

/// Get the git version for submodules below the cargo project.
///
/// This is achieved by running `git foreach` in tandem with `git describe`.
/// The arguments for both git commands are exposed as macro arguments.
///
/// This macro expands to a 2D array that looks like the following:
///
/// `[["relative/path/to/submodule", "{prefix}{git_describe_output}{suffix}"]]`
///
/// The following (named) arguments can be given:
///
/// - `foreach_args`: The arguments to call `git submodule foreach` with. Default: `foreach_args = ["--quiet", "--recursive"]`
/// - NOTE: `"--quiet"` is a required argument. If `"--quiet"` is not in the list of provided args, it will be added automatically.
///
/// - `describe_args`: The arguments to call `git describe` with.
/// Default: `describe_args = ["--always", "--dirty=-modified"]`
///
/// - `prefix`, `suffix`:
/// The git version for each submodule will be prefixed/suffixed
/// by these strings.
///
/// - `fallback`:
/// If all else fails, this string will be given instead of reporting an
/// error.
///
/// # Examples
///
/// ```
/// const MODULE_VERSIONS: [[&str, 2], 4] = git_version_modules!();
/// ```
///
/// ```
/// const MODULE_VERSIONS: [[&str, 2], 4] = git_version_modules!(describe_args = ["--abbrev=40", "--always"]);
/// ```
///
/// ```
/// # use git_version::git_version_modules;
/// const MODULE_VERSIONS: [[&str, 2], 4] = git_version_modules!(prefix = "git:", fallback = "unknown");
/// ```
#[proc_macro]
pub fn git_version_modules(input: TokenStream) -> TokenStream {
let args = syn::parse_macro_input!(input as describe_submodules::GitModArgs);

let tokens = match describe_submodules::git_version_modules_impl(args) {
Ok(x) => x,
Err(e) => e.to_compile_error(),
};

TokenStream::from(tokens)
}
19 changes: 15 additions & 4 deletions git-version-macro/src/utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,8 @@ fn run_git(program: &str, command: &mut std::process::Command) -> Result<String,

let output = collect_output(program, output)?;
let output = strip_trailing_newline(output);
let output = String::from_utf8(output)
.map_err(|_| format!("Failed to parse output of `{}`: output contains invalid UTF-8", program))?;
let output =
String::from_utf8(output).map_err(|_| format!("Failed to parse output of `{}`: output contains invalid UTF-8", program))?;
Ok(output)
}

Expand All @@ -48,7 +48,9 @@ fn collect_output(program: &str, output: std::process::Output) -> Result<Vec<u8>
// If the command terminated with non-zero exit code, return an error.
} else if let Some(status) = output.status.code() {
// Include the first line of stderr in the error message, if it's valid UTF-8 and not empty.
let message = output.stderr.split(|c| *c == b'\n')
let message = output
.stderr
.split(|c| *c == b'\n')
.next()
.and_then(|x| std::str::from_utf8(x).ok())
.filter(|x| !x.is_empty());
Expand Down Expand Up @@ -80,10 +82,19 @@ fn strip_trailing_newline(mut input: Vec<u8>) -> Vec<u8> {
input
}

/// Run `git describe` for submodules in the current working directory with custom flags to get version information from git.
pub fn describe_modules<I, S>(args: I) -> Result<String, String>
where
I: IntoIterator<Item = S>,
S: AsRef<OsStr>,
{
run_git("git submodule", Command::new("git").args(args))
}

#[test]
fn test_git_dir() {
use std::path::Path;
use assert2::{assert, let_assert};
use std::path::Path;

let_assert!(Ok(git_dir) = git_dir_cwd());
let_assert!(Ok(git_dir) = git_dir.canonicalize());
Expand Down
2 changes: 1 addition & 1 deletion src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
//! These macros do not depend on libgit, but simply uses the `git` binary directly.
//! So you must have `git` installed somewhere in your `PATH`.

pub use git_version_macro::git_version;
pub use git_version_macro::{git_version, git_version_modules};

/// Run `git describe` at compile time with custom flags.
///
Expand Down
1 change: 1 addition & 0 deletions test-child-repo
Submodule test-child-repo added at da418b
1 change: 1 addition & 0 deletions test_outer_directory/test-child-repo
Submodule test-child-repo added at da418b
12 changes: 11 additions & 1 deletion tests/version.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use git_version::{git_describe, git_version};
use git_version::{git_describe, git_version, git_version_modules};

#[test]
fn git_describe_is_right() {
Expand All @@ -14,3 +14,13 @@ fn git_describe_is_right() {
assert_eq!(git_describe!("--always", "--dirty=-modified"), name);
assert_eq!(git_version!(prefix = "[", suffix = "]"), format!("[{}]", name));
}

#[test]
fn test_modules_macro_gives_expected_output() {
let module_versions = git_version_modules!(
prefix = "pre-",
suffix = "-suff",
describe_args = ["--always", "--dirty=-modified", "--tags"]
);
println!("{module_versions:?}");
}
Loading