use super::commit_transaction::{CommitInvocation, TransactionStop, failed};
use super::service_commit_args::{FixupKind, ServiceCommitArgs, cleanup_message};
use super::transaction_git::{GitContext, GitRequest, run_git};
use std::ffi::OsString;
use std::path::Path;
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct Built {
pub message: String,
pub reused_author: Option<String>,
}
pub fn identity(context: &GitContext, cwd: &Path, variable: &str, overrides: &[(&str, String)]) -> Result<Option<String>, TransactionStop> {
let mut request: GitRequest = GitRequest::new(cwd, &["var", variable]);
request.environment = overrides.iter().map(|(name, value)| { return (OsString::from(name), OsString::from(value)); }).collect();
let output = run_git(context, &request).map_err(|error| { return failed(format!("reading the commit identity failed: {error:?}")); })?;
if output.code != Some(0) {
return Ok(None);
}
return Ok(Some(String::from_utf8_lossy(output.stdout.as_slice()).trim().to_string()));
}
pub fn author_overrides(parsed: &ServiceCommitArgs) -> Vec<(&'static str, String)> {
let mut overrides: Vec<(&'static str, String)> = Vec::new();
if let Some(author) = parsed.author.as_deref() {
let (name, rest): (&str, &str) = author.split_once('<').unwrap_or((author, ""));
overrides.push(("GIT_AUTHOR_NAME", name.trim().to_string()));
overrides.push(("GIT_AUTHOR_EMAIL", rest.trim_end().trim_end_matches('>').to_string()));
}
if let Some(date) = parsed.date.as_deref() {
overrides.push(("GIT_AUTHOR_DATE", date.to_string()));
}
return overrides;
}
fn query(context: &GitContext, cwd: &Path, arguments: &[&str]) -> Result<Option<Vec<u8>>, TransactionStop> {
let output = run_git(context, &GitRequest::new(cwd, arguments)).map_err(|error| { return failed(format!("reading the repository failed: {error:?}")); })?;
return Ok(if output.code == Some(0) { Some(output.stdout) } else { None });
}
fn reused_commit(context: &GitContext, cwd: &Path, revision: &str) -> Result<Option<(String, String)>, TransactionStop> {
if revision.starts_with('-') {
return Ok(None);
}
let Some(oid) = query(context, cwd, &["rev-parse", "--verify", "--quiet", format!("{revision}^{{commit}}").as_str()])? else { return Ok(None) };
let Some(raw) = query(context, cwd, &["cat-file", "commit", String::from_utf8_lossy(oid.as_slice()).trim()])? else { return Ok(None) };
let Ok(text) = String::from_utf8(raw) else { return Ok(None) };
let Some((header, message)) = text.split_once("\n\n") else { return Ok(None) };
if header.lines().any(|line| { return line.starts_with("encoding "); }) {
return Ok(None);
}
let Some(author) = header.lines().find_map(|line| { return line.strip_prefix("author "); }) else { return Ok(None) };
return Ok(Some((author.to_string(), message.to_string())));
}
fn subject_of(context: &GitContext, cwd: &Path, revision: &str) -> Result<Option<String>, TransactionStop> {
if revision.starts_with('-') {
return Ok(None);
}
let Some(bytes) = query(context, cwd, &["log", "-1", "--format=%s", revision, "--"])? else { return Ok(None) };
return Ok(String::from_utf8(bytes).ok().map(|text| { return text.trim_end_matches('\n').to_string(); }));
}
pub fn strip_comment_lines(text: &str) -> String {
return text.split('\n').filter(|line| { return !line.starts_with('#'); }).collect::<Vec<&str>>().join("\n");
}
pub fn build_message(
invocation: &CommitInvocation<'_>,
parsed: &ServiceCommitArgs,
committer: &str,
previous_message: Option<&str>,
) -> Result<Option<Built>, TransactionStop> {
let context: &GitContext = invocation.context;
let cwd: &Path = invocation.cwd;
let mut reused_author: Option<String> = None;
let sources: Vec<String> = if let Some(file) = parsed.file.as_ref() {
match std::fs::read(cwd.join(file)).ok().and_then(|bytes| { return String::from_utf8(bytes).ok(); }) {
Some(text) => vec![text],
None => return Ok(None),
}
} else if let Some(revision) = parsed.reuse.as_deref() {
let Some((author, message)) = reused_commit(context, cwd, revision)? else { return Ok(None) };
reused_author = Some(author);
vec![message]
} else if let Some((kind, revision)) = parsed.fixup.as_ref() {
let Some(subject) = subject_of(context, cwd, revision.as_str())? else { return Ok(None) };
let prefix: &str = match kind {
FixupKind::Fixup => "fixup!",
};
vec![format!("{prefix} {subject}")]
} else if parsed.messages.is_empty() && let Some(previous) = previous_message.filter(|_| { return parsed.amend; }) {
vec![previous.to_string()]
} else {
parsed.messages.clone()
};
let texts: Vec<String> = if parsed.strip { sources.iter().map(|text| { return strip_comment_lines(text.as_str()); }).collect() } else { sources };
let cleaned: Option<String> = if parsed.verbatim {
Some(texts.iter().map(|text| { return if text.ends_with('\n') { text.clone() } else { format!("{text}\n") }; }).collect::<Vec<String>>().join("\n"))
} else {
cleanup_message(texts.as_slice())
};
let message: String = match cleaned {
Some(text) if !text.trim().is_empty() || parsed.verbatim => text,
_ if parsed.allow_empty_message => String::new(),
_ => return Ok(None),
};
let mut trailers: Vec<String> = Vec::new();
if parsed.signoff {
let who: &str = committer.split_once('>').map_or(committer, |(name_and_email, _)| { return &committer[..name_and_email.len() + 1]; });
trailers.push(format!("Signed-off-by: {who}"));
}
trailers.extend(parsed.trailers.iter().cloned());
if trailers.is_empty() {
return Ok(Some(Built { message, reused_author }));
}
let mut arguments: Vec<String> = vec![String::from("interpret-trailers")];
for trailer in &trailers {
arguments.push(String::from("--trailer"));
arguments.push(trailer.clone());
}
let mut request: GitRequest = GitRequest::new(cwd, arguments.as_slice());
request.input = Some(message.into_bytes());
let output = run_git(context, &request).map_err(|error| { return failed(format!("preparing the commit message failed: {error:?}")); })?;
if output.code != Some(0) {
return Ok(None);
}
return Ok(String::from_utf8(output.stdout).ok().map(|trailed| { return Built { message: trailed, reused_author }; }));
}