#![allow(clippy::let_unit_value)]
#![warn(clippy::print_stderr, clippy::print_stdout)]
use std::borrow::Cow;
use std::ffi::OsStr;
use std::io::stdout;
use std::io::Write;
use std::path::Path;
use std::path::PathBuf;
use std::process::Command;
use std::process::Stdio;
use anyhow::bail;
use anyhow::Context;
use anyhow::Result;
const GIT: &str = "git";
fn git_command<A>(args: &[A]) -> String
where
A: AsRef<OsStr>,
{
args.iter().fold(GIT.to_string(), |mut cmd, arg| {
cmd += " ";
cmd += &arg.as_ref().to_string_lossy();
cmd
})
}
fn git_raw_output<A>(directory: &Path, args: &[A]) -> Result<Vec<u8>>
where
A: AsRef<OsStr>,
{
let git = Command::new(GIT)
.current_dir(directory)
.stdin(Stdio::null())
.args(args)
.output()
.with_context(|| format!("failed to run `{}`", git_command(args)))?;
if !git.status.success() {
let code = if let Some(code) = git.status.code() {
format!(" ({})", code)
} else {
String::new()
};
bail!(
"`{}` reported non-zero exit-status{}",
git_command(args),
code
);
}
Ok(git.stdout)
}
fn git_output<A>(directory: &Path, args: &[A]) -> Result<String>
where
A: AsRef<OsStr>,
{
let output = git_raw_output(directory, args)?;
let output = String::from_utf8(output).with_context(|| {
format!(
"failed to read `{}` output as UTF-8 string",
git_command(args)
)
})?;
Ok(output)
}
fn git_run<A>(directory: &Path, args: &[A]) -> Result<bool>
where
A: AsRef<OsStr>,
{
Command::new(GIT)
.current_dir(directory)
.stdin(Stdio::null())
.stdout(Stdio::null())
.stderr(Stdio::null())
.args(args)
.status()
.with_context(|| format!("failed to run `{}`", git_command(args)))
.map(|status| status.success())
}
#[cfg(unix)]
fn bytes_to_path(bytes: &[u8]) -> Result<Cow<'_, Path>> {
use std::os::unix::ffi::OsStrExt as _;
Ok(AsRef::<Path>::as_ref(OsStr::from_bytes(bytes)).into())
}
#[cfg(not(unix))]
fn bytes_to_path(bytes: &[u8]) -> Result<Cow<'_, Path>> {
use std::path::PathBuf;
use std::str::from_utf8;
Ok(PathBuf::from(from_utf8(bytes)?).into())
}
fn print_rerun_if_changed<S, I, W>(directory: &Path, sources: S, writer: &mut W) -> Result<()>
where
S: IntoIterator<Item = I>,
I: AsRef<Path>,
W: Write,
{
let git_dir = git_raw_output(directory, &["rev-parse", "--absolute-git-dir"])?;
let git_dir = bytes_to_path(&git_dir[..git_dir.len() - 1])?;
static PATHS: [&str; 3] = ["HEAD", "index", "refs/"];
let () = PATHS.iter().try_for_each(|path| {
writeln!(
writer,
"cargo:rerun-if-changed={}",
git_dir.join(path).display()
)
})?;
let () = sources.into_iter().try_for_each(|path| {
writeln!(
writer,
"cargo:rerun-if-changed={}",
git_dir.join(path.as_ref()).display()
)
})?;
Ok(())
}
fn with_valid_git<W, F>(dir: &Path, writer: W, f: F) -> Result<Option<String>>
where
W: Write,
F: FnOnce(&Path, W) -> Result<Option<String>>,
{
let mut w = writer;
match git_run(dir, &["rev-parse", "--git-dir"]) {
Ok(true) => (),
Ok(false) => {
writeln!(
w,
"cargo:warning=Not in a git repository; unable to embed git revision"
)?;
return Ok(None)
},
Err(err) => {
writeln!(
w,
"cargo:warning=Failed to invoke `git`; unable to embed git revision: {}",
err
)?;
return Ok(None)
},
}
f(dir, w)
}
fn revision_bare_impl<S, I, W>(dir: &Path, sources: S, writer: W) -> Result<Option<String>>
where
S: IntoIterator<Item = I>,
I: AsRef<Path>,
W: Write,
{
let mut w = writer;
let () = print_rerun_if_changed(dir, sources, &mut w)?;
let revision = if let Ok(tag) = git_output(dir, &["describe", "--exact-match", "--tags", "HEAD"])
{
tag
} else {
git_output(dir, &["rev-parse", "--short", "HEAD"])?
};
Ok(Some(revision.trim().to_string()))
}
fn revision_impl<S, I, W>(dir: &Path, sources: S, writer: W) -> Result<Option<String>>
where
S: IntoIterator<Item = I>,
I: AsRef<Path>,
W: Write,
{
if let Some(revision) = revision_bare_impl(dir, sources, writer)? {
let local_changes = git_raw_output(dir, &["status", "--porcelain", "--untracked-files=no"])?;
let modified = !local_changes.is_empty();
let revision = format!("{}{}", revision, if modified { "+" } else { "" });
Ok(Some(revision))
} else {
Ok(None)
}
}
#[deprecated(note = "use git_revision() function instead")]
pub fn get_revision<P, W>(directory: P, writer: W) -> Result<Option<String>>
where
P: AsRef<Path>,
W: Write,
{
with_valid_git(directory.as_ref(), writer, |directory, writer| {
let sources = [OsStr::new(""); 0];
revision_impl(directory, sources.iter(), writer)
})
}
pub fn git_revision_bare<D>(directory: D) -> Result<Option<String>>
where
D: AsRef<Path>,
{
with_valid_git(directory.as_ref(), stdout().lock(), |directory, writer| {
let sources = [OsStr::new(""); 0];
revision_bare_impl(directory, sources.iter(), writer)
})
}
#[deprecated(note = "use git_revision_auto() function instead")]
pub fn git_revision<D, S, I>(directory: D, sources: S) -> Result<Option<String>>
where
D: AsRef<Path>,
S: IntoIterator<Item = I>,
I: AsRef<Path>,
{
with_valid_git(directory.as_ref(), stdout().lock(), |directory, writer| {
revision_impl(directory, sources, writer)
})
}
fn list_tracked_objects(directory: &Path) -> Result<Vec<PathBuf>> {
let top_level = git_raw_output(directory, &["rev-parse", "--show-toplevel"])?;
let top_level = bytes_to_path(&top_level[..top_level.len() - 1])?;
let args = &[
OsStr::new("-C"),
top_level.as_os_str(),
OsStr::new("ls-files"),
OsStr::new("--full-name"),
OsStr::new("-z"),
];
let output = git_raw_output(directory, args)?;
let paths = output
.split(|byte| *byte == b'\0')
.filter(|object| !object.is_empty())
.map(|object| Ok(top_level.join(bytes_to_path(object)?)))
.collect::<Result<_>>()?;
Ok(paths)
}
pub fn git_revision_auto<D>(directory: D) -> Result<Option<String>>
where
D: AsRef<Path>,
{
with_valid_git(directory.as_ref(), stdout().lock(), |directory, writer| {
let sources = list_tracked_objects(directory)?;
revision_impl(directory, sources, writer)
})
}