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<Vec<u8>>,
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 pub named_hooks_possible: bool,
100}
101
102fn rerun_pre_commit(
114 context: &GitContext,
115 worktree_root: &Path,
116 global_args: &[String],
117 shadow: &Path,
118 hooks_directory: &Path,
119 named_hooks_possible: bool,
120 index: &Path,
121) -> Result<(), TransactionStop> {
122 if !named_hooks_possible {
124 debug(TAG, "the configuration defines no hook by name; the re-run is skipped");
125 return Ok(());
126 }
127 let mut arguments: Vec<OsString> = global_args.iter().map(OsString::from).collect();
128 arguments.push(OsString::from(format!("--git-dir={}", shadow.display())));
129 arguments.push(OsString::from(format!("--work-tree={}", worktree_root.display())));
130 arguments.push(OsString::from("-c"));
131 arguments.push(OsString::from(format!("core.hooksPath={}", hooks_directory.display())));
132 for event in SILENCED_HOOK_EVENTS {
133 arguments.push(OsString::from("-c"));
134 arguments.push(OsString::from(format!("hook.{event}.enabled=false")));
135 }
136 for word in ["hook", "run", "--ignore-missing", "pre-commit"] {
137 arguments.push(OsString::from(word));
138 }
139 let mut command = git_command(context.real_git.as_path(), arguments.as_slice(), context.overlay.as_slice());
140 command
141 .current_dir(worktree_root)
142 .env("GIT_INDEX_FILE", index)
143 .env("GIT_EDITOR", ":");
145 for name in NATIVE_UNSET_VARIABLES {
146 command.env_remove(name);
147 }
148 let guard: SignalGuard = install_guard_when_free()
149 .map_err(|error| { return failed(format!("installing the signal guard failed: {error}")); })?;
150 let outcome = guard
151 .run_child(&mut command)
152 .map_err(|error| { return TransactionStop::Message(format!("git hook run could not start: {error}")); })?;
153 debug(TAG, format!("pre-commit re-run ended {outcome:?}").as_str());
154 if exit_code(outcome) != 0 {
155 return Err(TransactionStop::NativeExit(HOOK_REJECTION_EXIT));
156 }
157 return Ok(());
158}
159
160pub fn revalidate_replay<F: RepositoryFacts>(
171 context: &RevalidationContext<'_>,
172 workspace: &CommitWorkspace,
173 request: &StageRequest,
174 checks: &mut ShippedChecks<PrivateIndexFacts<F>>,
175 replay: u64,
176 merged_tree: &str,
177 onto: &str,
178 approved_tree: &str,
179) -> Result<Revalidation, TransactionStop> {
180 let git: &GitContext = context.invocation.context;
181 let cwd: &Path = context.invocation.cwd;
182 let root: &Path = context.capture.layout.repository_root.as_path();
183 let index: PathBuf = replayed_index_path(workspace.landing.directory.as_path(), replay);
184 let directory: PathBuf = workspace.landing.directory.join(format!("replay-{replay}"));
185 ensure_replay_directory(directory.as_path())
186 .map_err(|error| { return failed(format!("creating {} failed: {error}", directory.display())); })?;
187 read_replayed_index(git, cwd, workspace.object_directory.as_path(), index.as_path(), merged_tree)?;
188 move_shadow_head(
189 git,
190 workspace.landing.shadow.as_path(),
191 cwd,
192 context.capture.layout.target_ref.as_str(),
193 onto,
194 is_detached(&context.capture.layout.symbolic_head),
195 )?;
196 let candidate_paths: Vec<Vec<u8>> =
197 list_changed_index_paths(git, cwd, index.as_path(), onto, workspace.object_directory.as_path())?;
198 let base: Option<ObjectId> = parse_object_id(onto.as_bytes());
199 checks.facts.use_index(index.as_path(), workspace.object_directory.as_path());
200 checks.content = ContentState::new();
201 checks.candidates = LifecycleContent::Requested(staged_at_base(base, candidate_paths.as_slice()));
202 let (pass, changed_passes, changed_paths): (PassResult, u64, Vec<Vec<u8>>) = if context.read_only_selection {
203 checks.can_apply_patches = false;
204 (run_policy_pass(request, checks), 0, Vec::new())
205 } else {
206 let outcome: CommitConvergence = converge_commit_policies(CommitConvergenceInput {
207 request,
208 checks,
209 stage: super::private_index_stage::PrivateStage {
210 context: git,
211 cwd,
212 index: index.as_path(),
213 object_directory: Some(workspace.object_directory.as_path()),
214 },
215 });
216 let passes: u64 = settled_passes(&outcome.convergence);
217 (outcome.pass, passes, outcome.changed_paths)
218 };
219 let code: i32 = pass_exit_code(pass.events.as_slice(), pass.end);
220 if code != 0 {
221 return Ok(Revalidation::Blocked {
222 events: pass.events,
223 exit_code: code,
224 });
225 }
226 let policy_tree: String =
227 write_private_tree(git, cwd, index.as_path(), workspace.object_directory.as_path())?;
228 let rerun: bool = pre_commit_rerun_required(context.no_verify, policy_tree.as_str(), approved_tree);
229 if rerun {
230 rerun_pre_commit(
231 git,
232 root,
233 context.global_args,
234 workspace.landing.shadow.as_path(),
235 workspace.hooks_directory.as_path(),
236 context.named_hooks_possible,
237 index.as_path(),
238 )?;
239 }
240 let tree: String = if rerun {
241 write_private_tree(git, cwd, index.as_path(), workspace.object_directory.as_path())?
242 } else {
243 policy_tree.clone()
244 };
245 let hook_changes: HookChanges = if rerun {
246 describe_hook_changes(git, cwd, workspace.object_directory.as_path(), policy_tree.as_str(), tree.as_str())?
247 } else {
248 HookChanges::default()
249 };
250 debug(
251 TAG,
252 format!("revalidated replay {replay} onto {onto}: tree {tree}{}", if rerun { " after pre-commit" } else { "" }).as_str(),
253 );
254 return Ok(Revalidation::Revalidated {
255 tree,
256 index,
257 events: super::commit_conclusion::events_with_changed_summary(pass.events, changed_passes, changed_paths.as_slice()),
258 committed_paths: candidate_paths,
259 hook_changes,
260 });
261}
262
263pub fn ensure_replay_directory(directory: &Path) -> std::io::Result<()> {
273 match create_private_directory(directory) {
274 Err(error) if error.kind() != std::io::ErrorKind::AlreadyExists => return Err(error),
275 Ok(()) | Err(_) => return Ok(()),
276 }
277}
278
279pub fn is_detached(head: &SymbolicHead) -> bool {
289 return *head == SymbolicHead::Detached;
290}
291
292#[cfg(test)]
294#[path = "commit_revalidation_tests.rs"]
295mod tests;