use super::candidate_object::{ObjectId, parse_object_id};
use super::capture_invocation::InvocationCapture;
use super::commit_convergence::{CommitConvergence, CommitConvergenceInput, converge_commit_policies};
use super::commit_facts::PrivateIndexFacts;
use super::commit_index::{list_changed_index_paths, write_private_tree};
use super::commit_preparation::{NATIVE_UNSET_VARIABLES, install_guard_when_free};
use super::commit_transaction::{CommitInvocation, TransactionStop, failed, settled_passes, staged_at_base};
use super::commit_workspace::CommitWorkspace;
use super::commit_worktree_records::{HookChanges, describe_hook_changes};
use super::diagnostic_log::debug;
use super::forwarding::{exit_code, git_command};
use super::signal_relay::SignalGuard;
use super::hook_dispatch_plan::SILENCED_HOOK_EVENTS;
use super::policy_checks::ShippedChecks;
use super::policy_content::{ContentState, LifecycleContent};
use super::policy_engine::{StageRequest, pass_exit_code};
use super::policy_events::PolicyEvent;
use super::policy_pass::{PassResult, run_policy_pass};
use super::private_storage::create_private_directory;
use super::replay_revalidation::{move_shadow_head, pre_commit_rerun_required, read_replayed_index, replayed_index_path};
use super::repository_facts::RepositoryFacts;
use super::transaction_git::GitContext;
use super::transaction_journal::SymbolicHead;
use std::ffi::OsString;
use std::path::{Path, PathBuf};
const TAG: &str = "revalidateReplay";
const HOOK_REJECTION_EXIT: i32 = 1;
pub enum Revalidation {
Revalidated {
tree: String,
index: PathBuf,
events: Vec<PolicyEvent>,
committed_paths: Vec<Vec<u8>>,
hook_changes: HookChanges,
},
Blocked {
events: Vec<PolicyEvent>,
exit_code: i32,
},
}
pub struct RevalidationContext<'a> {
pub invocation: &'a CommitInvocation<'a>,
pub capture: &'a InvocationCapture,
pub read_only_selection: bool,
pub global_args: &'a [String],
pub no_verify: bool,
}
fn rerun_pre_commit(
context: &GitContext,
worktree_root: &Path,
global_args: &[String],
shadow: &Path,
hooks_directory: &Path,
index: &Path,
) -> Result<(), TransactionStop> {
let mut arguments: Vec<OsString> = global_args.iter().map(OsString::from).collect();
arguments.push(OsString::from(format!("--git-dir={}", shadow.display())));
arguments.push(OsString::from(format!("--work-tree={}", worktree_root.display())));
arguments.push(OsString::from("-c"));
arguments.push(OsString::from(format!("core.hooksPath={}", hooks_directory.display())));
for event in SILENCED_HOOK_EVENTS {
arguments.push(OsString::from("-c"));
arguments.push(OsString::from(format!("hook.{event}.enabled=false")));
}
for word in ["hook", "run", "--ignore-missing", "pre-commit"] {
arguments.push(OsString::from(word));
}
let mut command = git_command(context.real_git.as_path(), arguments.as_slice(), context.overlay.as_slice());
command
.current_dir(worktree_root)
.env("GIT_INDEX_FILE", index)
.env("GIT_EDITOR", ":");
for name in NATIVE_UNSET_VARIABLES {
command.env_remove(name);
}
let guard: SignalGuard = install_guard_when_free()
.map_err(|error| { return failed(format!("installing the signal guard failed: {error}")); })?;
let outcome = guard
.run_child(&mut command)
.map_err(|error| { return TransactionStop::Message(format!("git hook run could not start: {error}")); })?;
debug(TAG, format!("pre-commit re-run ended {outcome:?}").as_str());
if exit_code(outcome) != 0 {
return Err(TransactionStop::NativeExit(HOOK_REJECTION_EXIT));
}
return Ok(());
}
pub fn revalidate_replay<F: RepositoryFacts>(
context: &RevalidationContext<'_>,
workspace: &CommitWorkspace,
request: &StageRequest,
checks: &mut ShippedChecks<PrivateIndexFacts<F>>,
replay: u64,
merged_tree: &str,
onto: &str,
approved_tree: &str,
) -> Result<Revalidation, TransactionStop> {
let git: &GitContext = context.invocation.context;
let cwd: &Path = context.invocation.cwd;
let root: &Path = context.capture.layout.repository_root.as_path();
let index: PathBuf = replayed_index_path(workspace.landing.directory.as_path(), replay);
let directory: PathBuf = workspace.landing.directory.join(format!("replay-{replay}"));
ensure_replay_directory(directory.as_path())
.map_err(|error| { return failed(format!("creating {} failed: {error}", directory.display())); })?;
read_replayed_index(git, cwd, workspace.object_directory.as_path(), index.as_path(), merged_tree)?;
move_shadow_head(
git,
workspace.landing.shadow.as_path(),
cwd,
context.capture.layout.target_ref.as_str(),
onto,
is_detached(&context.capture.layout.symbolic_head),
)?;
let candidate_paths: Vec<Vec<u8>> =
list_changed_index_paths(git, cwd, index.as_path(), onto, workspace.object_directory.as_path())?;
let base: Option<ObjectId> = parse_object_id(onto.as_bytes());
checks.facts.use_index(index.as_path(), workspace.object_directory.as_path());
checks.content = ContentState::new();
checks.candidates = LifecycleContent::Requested(staged_at_base(base, candidate_paths.as_slice()));
let (pass, changed_passes, changed_paths): (PassResult, u64, Vec<Vec<u8>>) = if context.read_only_selection {
checks.can_apply_patches = false;
(run_policy_pass(request, checks), 0, Vec::new())
} else {
let outcome: CommitConvergence = converge_commit_policies(CommitConvergenceInput {
request,
checks,
stage: super::private_index_stage::PrivateStage {
context: git,
cwd,
index: index.as_path(),
object_directory: Some(workspace.object_directory.as_path()),
},
});
let passes: u64 = settled_passes(&outcome.convergence);
(outcome.pass, passes, outcome.changed_paths)
};
let code: i32 = pass_exit_code(pass.events.as_slice(), pass.end);
if code != 0 {
return Ok(Revalidation::Blocked {
events: pass.events,
exit_code: code,
});
}
let policy_tree: String =
write_private_tree(git, cwd, index.as_path(), workspace.object_directory.as_path())?;
let rerun: bool = pre_commit_rerun_required(context.no_verify, policy_tree.as_str(), approved_tree);
if rerun {
rerun_pre_commit(
git,
root,
context.global_args,
workspace.landing.shadow.as_path(),
workspace.hooks_directory.as_path(),
index.as_path(),
)?;
}
let tree: String = if rerun {
write_private_tree(git, cwd, index.as_path(), workspace.object_directory.as_path())?
} else {
policy_tree.clone()
};
let hook_changes: HookChanges = if rerun {
describe_hook_changes(git, cwd, workspace.object_directory.as_path(), policy_tree.as_str(), tree.as_str())?
} else {
HookChanges::default()
};
debug(
TAG,
format!("revalidated replay {replay} onto {onto}: tree {tree}{}", if rerun { " after pre-commit" } else { "" }).as_str(),
);
return Ok(Revalidation::Revalidated {
tree,
index,
events: super::commit_conclusion::events_with_changed_summary(pass.events, changed_passes, changed_paths.as_slice()),
committed_paths: candidate_paths,
hook_changes,
});
}
pub fn ensure_replay_directory(directory: &Path) -> std::io::Result<()> {
match create_private_directory(directory) {
Err(error) if error.kind() != std::io::ErrorKind::AlreadyExists => return Err(error),
Ok(()) | Err(_) => return Ok(()),
}
}
pub fn is_detached(head: &SymbolicHead) -> bool {
return *head == SymbolicHead::Detached;
}
#[cfg(test)]
#[path = "commit_revalidation_tests.rs"]
mod tests;