use crate::capture_invocation::InvocationCapture;
use crate::hooks_off::NO_HOOKS_DIRECTORY;
use crate::shadow_git::shadow_request;
use crate::forwarding::git_command;
use crate::signal_relay::{SignalGuard, is_guard_active_refusal};
use crate::transaction_git::{GitContext, GitFailure, GitOutput, run_git_checked};
use std::ffi::OsString;
use std::path::Path;
pub const NATIVE_UNSET_VARIABLES: [&str; 4] = ["GIT_DIR", "GIT_WORK_TREE", "GIT_COMMON_DIR", "GIT_OBJECT_DIRECTORY"];
#[derive(Clone, Copy, Debug)]
pub struct PreparationRequest<'request> {
pub capture: &'request InvocationCapture,
pub shadow: &'request Path,
pub commit_index: &'request Path,
pub global_args: &'request [String],
pub commit_args: &'request [String],
pub object_directory: Option<&'request Path>,
}
#[derive(Debug)]
pub enum PreparationError {
Git(GitFailure),
}
impl std::fmt::Display for PreparationError {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
return match self {
Self::Git(error) => write!(formatter, "{error}"),
};
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct PreparedCommit {
pub oid: String,
pub tree_oid: String,
pub signed: bool,
pub raw: Vec<u8>,
}
fn native_commit_arguments(request: &PreparationRequest) -> Vec<String> {
let mut arguments: Vec<String> = request.global_args.to_vec();
arguments.push(format!("--git-dir={}", request.shadow.display()));
arguments.push(String::from("-c"));
arguments.push(format!("core.hooksPath={NO_HOOKS_DIRECTORY}"));
arguments.push(String::from("commit"));
arguments.extend(request.commit_args.iter().cloned());
return arguments;
}
pub fn install_guard_when_free() -> std::io::Result<SignalGuard> {
return retry_while_active(SignalGuard::install, GUARD_WAIT_LIMIT, GUARD_WAIT_DELAY);
}
const GUARD_WAIT_LIMIT: u32 = 1_200;
const GUARD_WAIT_DELAY: std::time::Duration = std::time::Duration::from_millis(50);
pub fn retry_while_active<Value>(
mut install: impl FnMut() -> std::io::Result<Value>,
limit: u32,
delay: std::time::Duration,
) -> std::io::Result<Value> {
let mut attempts: u32 = 0;
loop {
match install() {
Ok(value) => return Ok(value),
Err(error) if is_guard_active_refusal(&error) && attempts < limit => {
attempts += 1;
std::thread::sleep(delay);
}
Err(error) => return Err(error),
}
}
}
pub fn run_native_commit(
context: &GitContext,
cwd: &Path,
request: &PreparationRequest,
) -> Result<i32, PreparationError> {
let arguments: Vec<OsString> = native_commit_arguments(request).iter().map(OsString::from).collect();
let mut command = git_command(context.real_git.as_path(), arguments.as_slice(), context.overlay.as_slice());
command
.current_dir(cwd)
.env("GIT_INDEX_FILE", request.commit_index);
for name in NATIVE_UNSET_VARIABLES {
command.env_remove(name);
}
if let Some(directory) = request.object_directory {
command.env("GIT_OBJECT_DIRECTORY", directory);
}
return run_guarded_child(&mut command);
}
fn run_guarded_child(command: &mut std::process::Command) -> Result<i32, PreparationError> {
let guard: SignalGuard = install_guard_when_free().map_err(|error| {
return PreparationError::Git(GitFailure(format!("installing the signal guard failed: {error}")));
})?;
let outcome = guard.run_child(command).map_err(|error| {
return PreparationError::Git(GitFailure(format!("git commit could not start: {error}")));
})?;
return Ok(crate::forwarding::exit_code(outcome));
}
pub fn read_prepared_commit(context: &GitContext, shadow: &Path) -> Result<PreparedCommit, GitFailure> {
let oid_output: GitOutput = run_git_checked(
context,
&shadow_request(shadow, shadow, &["rev-parse", "--verify", "HEAD^{commit}"]),
)?;
let oid: String = String::from_utf8_lossy(oid_output.stdout.as_slice()).trim().to_string();
let raw: Vec<u8> = run_git_checked(
context,
&shadow_request(shadow, shadow, &["cat-file", "commit", oid.as_str()]),
)?
.stdout;
let header_end: usize = raw
.windows(2)
.position(|window| { return window == b"\n\n"; })
.unwrap_or(raw.len());
let header: String = String::from_utf8_lossy(&raw[..header_end]).into_owned();
let tree_oid: String = header
.lines()
.find_map(|line| { return line.strip_prefix("tree "); })
.unwrap_or("")
.to_string();
let signed: bool = header
.lines()
.any(|line| { return line.starts_with("gpgsig ") || line.starts_with("gpgsig-sha256 "); });
return Ok(PreparedCommit {
oid,
tree_oid,
signed,
raw,
});
}
#[cfg(test)]
#[path = "commit_preparation_tests.rs"]
mod tests;