1use super::candidate_object::{ObjectId, parse_object_id};
15use super::capture_invocation::InvocationCapture;
16use super::commit_convergence::{CommitConvergence, CommitConvergenceInput, converge_commit_policies};
17use super::commit_facts::PrivateIndexFacts;
18use super::commit_index::{list_changed_index_paths, write_private_tree};
19use super::commit_preparation::{NATIVE_UNSET_VARIABLES, install_guard_when_free};
20use super::commit_transaction::{CommitInvocation, TransactionStop, failed, settled_passes, staged_at_base};
21use super::commit_workspace::CommitWorkspace;
22use super::commit_worktree_records::{HookChanges, describe_hook_changes};
23use super::diagnostic_log::debug;
24use super::forwarding::{exit_code, git_command};
25use super::signal_relay::SignalGuard;
26use super::hook_dispatch_plan::SILENCED_HOOK_EVENTS;
27use super::policy_checks::ShippedChecks;
28use super::policy_content::{ContentState, LifecycleContent};
29use super::policy_engine::{StageRequest, pass_exit_code};
30use super::policy_events::PolicyEvent;
31use super::policy_pass::{PassResult, run_policy_pass};
32use super::private_storage::create_private_directory;
33use super::replay_revalidation::{move_shadow_head, pre_commit_rerun_required, read_replayed_index, replayed_index_path};
34use super::repository_facts::RepositoryFacts;
35use super::transaction_git::GitContext;
36use super::transaction_journal::SymbolicHead;
37use std::ffi::OsString;
38use std::path::{Path, PathBuf};
39
40const TAG: &str = "revalidateReplay";
42
43const HOOK_REJECTION_EXIT: i32 = 1;
45
46pub enum Revalidation {
56 Revalidated {
58 tree: String,
60 index: PathBuf,
62 events: Vec<PolicyEvent>,
64 committed_paths: Vec<String>,
66 hook_changes: HookChanges,
68 },
69 Blocked {
71 events: Vec<PolicyEvent>,
73 exit_code: i32,
75 },
76}
77
78pub struct RevalidationContext<'a> {
88 pub invocation: &'a CommitInvocation<'a>,
90 pub capture: &'a InvocationCapture,
92 pub read_only_selection: bool,
94 pub global_args: &'a [String],
96 pub no_verify: bool,
98}
99
100fn rerun_pre_commit(
111 context: &GitContext,
112 worktree_root: &Path,
113 global_args: &[String],
114 shadow: &Path,
115 hooks_directory: &Path,
116 index: &Path,
117) -> Result<(), TransactionStop> {
118 let mut arguments: Vec<OsString> = global_args.iter().map(OsString::from).collect();
119 arguments.push(OsString::from(format!("--git-dir={}", shadow.display())));
120 arguments.push(OsString::from(format!("--work-tree={}", worktree_root.display())));
121 arguments.push(OsString::from("-c"));
122 arguments.push(OsString::from(format!("core.hooksPath={}", hooks_directory.display())));
123 for event in SILENCED_HOOK_EVENTS {
124 arguments.push(OsString::from("-c"));
125 arguments.push(OsString::from(format!("hook.{event}.enabled=false")));
126 }
127 for word in ["hook", "run", "--ignore-missing", "pre-commit"] {
128 arguments.push(OsString::from(word));
129 }
130 let mut command = git_command(context.real_git.as_path(), arguments.as_slice(), context.overlay.as_slice());
131 command
132 .current_dir(worktree_root)
133 .env("GIT_INDEX_FILE", index)
134 .env("GIT_EDITOR", ":");
136 for name in NATIVE_UNSET_VARIABLES {
137 command.env_remove(name);
138 }
139 let guard: SignalGuard = install_guard_when_free()
140 .map_err(|error| { return failed(format!("installing the signal guard failed: {error}")); })?;
141 let outcome = guard
142 .run_child(&mut command)
143 .map_err(|error| { return TransactionStop::Message(format!("git hook run could not start: {error}")); })?;
144 debug(TAG, format!("pre-commit re-run ended {outcome:?}").as_str());
145 if exit_code(outcome) != 0 {
146 return Err(TransactionStop::NativeExit(HOOK_REJECTION_EXIT));
147 }
148 return Ok(());
149}
150
151pub fn revalidate_replay<F: RepositoryFacts>(
162 context: &RevalidationContext<'_>,
163 workspace: &CommitWorkspace,
164 request: &StageRequest,
165 checks: &mut ShippedChecks<PrivateIndexFacts<F>>,
166 replay: u64,
167 merged_tree: &str,
168 onto: &str,
169 approved_tree: &str,
170) -> Result<Revalidation, TransactionStop> {
171 let git: &GitContext = context.invocation.context;
172 let cwd: &Path = context.invocation.cwd;
173 let root: &Path = context.capture.layout.repository_root.as_path();
174 let index: PathBuf = replayed_index_path(workspace.landing.directory.as_path(), replay);
175 let directory: PathBuf = workspace.landing.directory.join(format!("replay-{replay}"));
176 ensure_replay_directory(directory.as_path())
177 .map_err(|error| { return failed(format!("creating {} failed: {error}", directory.display())); })?;
178 read_replayed_index(git, cwd, workspace.object_directory.as_path(), index.as_path(), merged_tree)?;
179 move_shadow_head(
180 git,
181 workspace.landing.shadow.as_path(),
182 cwd,
183 context.capture.layout.target_ref.as_str(),
184 onto,
185 is_detached(&context.capture.layout.symbolic_head),
186 )?;
187 let candidate_paths: Vec<String> =
188 list_changed_index_paths(git, cwd, index.as_path(), onto, workspace.object_directory.as_path())?;
189 let base: Option<ObjectId> = parse_object_id(onto.as_bytes());
190 checks.facts.use_index(index.as_path(), workspace.object_directory.as_path());
191 checks.content = ContentState::new();
192 checks.candidates = LifecycleContent::Requested(staged_at_base(base, candidate_paths.as_slice()));
193 let (pass, changed_passes, changed_paths): (PassResult, u64, Vec<String>) = if context.read_only_selection {
194 checks.can_apply_patches = false;
195 (run_policy_pass(request, checks), 0, Vec::new())
196 } else {
197 let outcome: CommitConvergence = converge_commit_policies(CommitConvergenceInput {
198 request,
199 checks,
200 stage: super::private_index_stage::PrivateStage {
201 context: git,
202 cwd,
203 index: index.as_path(),
204 object_directory: Some(workspace.object_directory.as_path()),
205 },
206 });
207 let passes: u64 = settled_passes(&outcome.convergence);
208 (outcome.pass, passes, outcome.changed_paths)
209 };
210 let code: i32 = pass_exit_code(pass.events.as_slice(), pass.end);
211 if code != 0 {
212 return Ok(Revalidation::Blocked {
213 events: pass.events,
214 exit_code: code,
215 });
216 }
217 let policy_tree: String =
218 write_private_tree(git, cwd, index.as_path(), workspace.object_directory.as_path())?;
219 let rerun: bool = pre_commit_rerun_required(context.no_verify, policy_tree.as_str(), approved_tree);
220 if rerun {
221 rerun_pre_commit(
222 git,
223 root,
224 context.global_args,
225 workspace.landing.shadow.as_path(),
226 workspace.hooks_directory.as_path(),
227 index.as_path(),
228 )?;
229 }
230 let tree: String = if rerun {
231 write_private_tree(git, cwd, index.as_path(), workspace.object_directory.as_path())?
232 } else {
233 policy_tree.clone()
234 };
235 let hook_changes: HookChanges = if rerun {
236 describe_hook_changes(git, cwd, workspace.object_directory.as_path(), policy_tree.as_str(), tree.as_str())?
237 } else {
238 HookChanges::default()
239 };
240 debug(
241 TAG,
242 format!("revalidated replay {replay} onto {onto}: tree {tree}{}", if rerun { " after pre-commit" } else { "" }).as_str(),
243 );
244 return Ok(Revalidation::Revalidated {
245 tree,
246 index,
247 events: super::commit_conclusion::events_with_changed_summary(pass.events, changed_passes, changed_paths.as_slice()),
248 committed_paths: candidate_paths,
249 hook_changes,
250 });
251}
252
253pub fn ensure_replay_directory(directory: &Path) -> std::io::Result<()> {
263 match create_private_directory(directory) {
264 Err(error) if error.kind() != std::io::ErrorKind::AlreadyExists => return Err(error),
265 Ok(()) | Err(_) => return Ok(()),
266 }
267}
268
269pub fn is_detached(head: &SymbolicHead) -> bool {
279 return *head == SymbolicHead::Detached;
280}
281
282#[cfg(test)]
284#[path = "commit_revalidation_tests.rs"]
285mod tests;