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 13 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
165 changes: 165 additions & 0 deletions git-version-macro/src/describe_submodules.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
extern crate proc_macro;
use crate::canonicalize_path;
use crate::git_dependencies;
use crate::utils::run_git;
use proc_macro2::{Span, TokenStream as TokenStream2};
use quote::{quote, ToTokens};
use std::ffi::OsStr;
use std::path::Path;
use std::process::Command;
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 {
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() {
"args" => {
check_dup(result.args.is_some())?;
let content;
bracketed!(content in input);
result.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 modules = match get_modules() {
Ok(x) => x,
Err(err) => return Err(error!("{}", err)),
};

let mut describe_paths: Vec<(String, String)> = vec![];

for path in modules.into_iter() {
baxterjo marked this conversation as resolved.
Show resolved Hide resolved
let path_obj = Path::new(&path);
let path_obj = canonicalize_path(path_obj)?;
describe_paths.push((path, path_obj));
}

let git_describe_args = args.args.map_or_else(
|| vec!["--always".to_string(), "--dirty=-modified".to_string()],
|list| list.iter().map(|x| x.value()).collect(),
);

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

match describe_modules(describe_paths, &git_describe_args, prefix, suffix) {
Ok(result) => {
let dependencies = git_dependencies()?;
let (paths, versions) = result;

Ok(quote!({
#dependencies;

[#((#paths, #versions)),*]

}))
}
Err(_) if args.fallback.is_some() => Ok(args.fallback.to_token_stream()),
Err(e) => Err(error!("{}", e)),
}
}

/// Run `git submodule foreach` command to discover submodules in the project.
fn get_modules() -> Result<Vec<String>, String> {
let mut args: Vec<String> = "submodule foreach --quiet --recursive"
.to_string()
.split(' ')
.map(|x| x.to_string())
.collect();

args.push("echo $displaypath".to_string());

let result = run_git("git submodule", Command::new("git").args(args))?;
Comment on lines +127 to +135
Copy link
Collaborator

Choose a reason for hiding this comment

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

Suggested change
let mut args: Vec<String> = "submodule foreach --quiet --recursive"
.to_string()
.split(' ')
.map(|x| x.to_string())
.collect();
args.push("echo $displaypath".to_string());
let result = run_git("git submodule", Command::new("git").args(args))?;
let result = run_git(
"git submodule",
Command::new("git")
.arg("submodule")
.arg("foreach")
.arg("--quiet")
.arg("--recursive")
.arg("echo $displaypath"),
)?;

Copy link
Collaborator

@de-vri-es de-vri-es Dec 7, 2023

Choose a reason for hiding this comment

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

Does echo $displaypath work on windows though? Maybe we should parse the .gitmodules file instead relying on git submodule foreach.

Copy link
Contributor Author

@baxterjo baxterjo Dec 8, 2023

Choose a reason for hiding this comment

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

Lemme check real quick.

Edit: Yep! It works. The shell that executes the command must be implemented by git. Tested on Powershell and Command Prompt

Copy link
Collaborator

Choose a reason for hiding this comment

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

Ok :)


Ok(result.split('\n').map(|x| x.to_string()).collect())
}

/// Run `git describe` for each submodule to
fn describe_modules<I, S>(
paths: Vec<(String, String)>,
describe_args: I,
prefix: String,
suffix: String,
) -> Result<(Vec<String>, Vec<String>), String>
where
I: IntoIterator<Item = S> + Clone,
S: AsRef<OsStr>,
{
let mut paths_out: Vec<String> = vec![];
let mut versions: Vec<String> = vec![];

for (rel_path, abs_path) in paths.into_iter() {
let result = run_git(
"git describe",
Command::new("git")
.current_dir(abs_path)
.arg("describe")
.args(describe_args.clone()),
)?;
paths_out.push(rel_path);
versions.push(format!("{}{}{}", prefix, result, suffix))
}

Ok((paths_out, versions))
}
91 changes: 71 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,62 @@ 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 `git describe` are exposed as macro arguments.
///
/// This macro expands to an array of `(&str, &str)` tuples that look like the following:
///
/// `[("relative/path/to/submodule", "{prefix}{git_describe_output}{suffix}")]`
///
/// The following (named) arguments can be given:
///
/// - `args`: The arguments to call `git describe` with.
/// Default: `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, &str), N] = git_version_modules!();
/// ```
///
/// ```
/// const MODULE_VERSIONS: [(&str, &str), N] = git_version_modules!(args = ["--abbrev=40", "--always"]);
/// ```
///
/// ```
/// # use git_version::git_version_modules;
/// const MODULE_VERSIONS: [(&str, &str), N] = 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)
}
12 changes: 7 additions & 5 deletions git-version-macro/src/utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ pub fn git_dir_cwd() -> Result<PathBuf, String> {
Ok(PathBuf::from(path))
}

fn run_git(program: &str, command: &mut std::process::Command) -> Result<String, String> {
pub(crate) fn run_git(program: &str, command: &mut std::process::Command) -> Result<String, String> {
let output = command
.stdout(std::process::Stdio::piped())
.stderr(std::process::Stdio::piped())
Expand All @@ -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 @@ -82,8 +84,8 @@ fn strip_trailing_newline(mut input: Vec<u8>) -> Vec<u8> {

#[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",
args = ["--always", "--dirty=-modified", "--tags"]
);
println!("{module_versions:#?}");
}
Loading