1use super::capture_invocation::InvocationCapture;
17use super::commit_args::{resolve_private_commit_args, split_commit_invocation, without_all_flag};
18use super::commit_landing_loop::{LoopInput, LoopOutcome, land_with_replay};
19use super::commit_facts::PrivateIndexFacts;
20use super::commit_index::write_private_tree;
21use super::commit_no_change::{NoChangeInput, complete_no_change_transaction};
22use super::commit_preparation::{PREPARED_FILENAME, PreparationHooks, PreparationRequest, read_prepared_commit, run_native_commit};
23use super::commit_replay_options::{ReplayOptions, replay_options};
24use super::commit_transaction::{CommitInvocation, Selection, TransactionResult, TransactionStop, failed, write_journal_record};
25use super::commit_workspace::CommitWorkspace;
26use super::commit_worktree_records::{HookChanges, describe_hook_changes, selected_worktree_records};
27use super::diagnostic_log::debug;
28use super::global_arguments::global_layout;
29use super::hook_dispatch_entries::write_hook_dispatch;
30use super::lfs_pre_push::uses_lfs;
31use super::hook_dispatch_plan::{HookDispatchPlan, HookPlanInputs, SILENCED_HOOK_EVENTS, compute_hook_dispatch_plan, new_preparation_lease};
32use super::policy_checks::ShippedChecks;
33use super::policy_engine::StageRequest;
34use super::policy_events::PolicyEvent;
35use super::policy_pass::PassResult;
36use super::post_landing::{LandedTransaction, PostLandingRequest, complete_landed_transaction};
37use super::repository_facts::RepositoryFacts;
38use super::transaction_git::GitContext;
39use super::transaction_journal::{AddedPath, PreparedRecord};
40use super::transaction_journal_encode::encode_prepared;
41use super::transaction_phase::{Phase, reach_transaction_phase};
42use std::ffi::OsString;
43use std::path::Path;
44
45const TAG: &str = "concludeCommitTransaction";
47
48pub struct ConclusionInput<'a, F: RepositoryFacts> {
58 pub invocation: &'a CommitInvocation<'a>,
60 pub workspace: &'a mut CommitWorkspace,
62 pub capture: &'a InvocationCapture,
64 pub selection: &'a Selection,
66 pub request: &'a StageRequest,
68 pub checks: &'a mut ShippedChecks<PrivateIndexFacts<F>>,
70 pub pass: PassResult,
72 pub changed_passes: u64,
74 pub changed_paths: Vec<Vec<u8>>,
76 pub committed_paths: Vec<Vec<u8>>,
78 pub pathspecs: &'a [String],
80}
81
82pub fn events_with_changed_summary(events: Vec<PolicyEvent>, passes: u64, changed_paths: &[Vec<u8>]) -> Vec<PolicyEvent> {
92 if passes == 0 {
93 return events;
94 }
95 return events_with_summary(events, passes, changed_paths);
96}
97
98pub fn events_with_summary(mut events: Vec<PolicyEvent>, passes: u64, changed_paths: &[Vec<u8>]) -> Vec<PolicyEvent> {
108 let mut sorted: Vec<Vec<u8>> = changed_paths.to_vec();
109 sorted.sort();
110 events.push(PolicyEvent::FixSummary {
111 trigger: super::policy_trigger::Trigger::PreForward,
112 passes,
113 changed_paths: sorted
114 .iter()
115 .map(|path| { return super::event_path::EventPath::from_git_bytes(path.as_slice()); })
116 .collect(),
117 });
118 return events;
119}
120
121fn private_invocation(pass: &PassResult, pathspecs: &[String], selection: &Selection) -> (Vec<String>, Vec<String>) {
131 let text: Vec<String> = pass.arguments.iter().map(|token| { return token.to_string_lossy().into_owned(); }).collect();
132 let private_args: Vec<String> =
133 resolve_private_commit_args(text.as_slice(), pathspecs, selection.mode, selection.read_only);
134 let owned: Vec<OsString> = private_args.iter().map(OsString::from).collect();
135 let index: usize = global_layout(owned.as_slice()).prefix_len;
136 return split_commit_invocation(private_args.as_slice(), index);
137}
138
139pub fn conclude_commit_transaction<F: RepositoryFacts>(
149 input: ConclusionInput<'_, F>,
150) -> Result<TransactionResult, TransactionStop> {
151 let ConclusionInput {
152 invocation,
153 workspace,
154 capture,
155 selection,
156 request,
157 checks,
158 pass,
159 changed_passes,
160 changed_paths,
161 committed_paths,
162 pathspecs,
163 } = input;
164 let context: &GitContext = invocation.context;
165 let cwd: &Path = invocation.cwd;
166 let repository_root: &Path = capture.layout.repository_root.as_path();
167 let timeout_ms: u64 = invocation.config.concurrency.index_lock.unproven_owner_timeout_ms;
168 let intended_tree: String = write_private_tree(
169 context,
170 cwd,
171 workspace.landing.commit_index.as_path(),
172 workspace.object_directory.as_path(),
173 )?;
174 let changes = checks.content.install_changes();
175 let selected: Vec<AddedPath> = selected_worktree_records(
176 context,
177 cwd,
178 repository_root,
179 workspace.landing.commit_index.as_path(),
180 changes.as_slice(),
181 )?;
182 let eligible: bool = !changes.is_empty()
183 && !invocation.region.allow_empty
184 && !invocation.region.amend
185 && !selection.concludes_sequencer;
186 if let Some(result) = complete_no_change_transaction(NoChangeInput {
187 invocation,
188 workspace,
189 capture,
190 mode: selection.mode,
191 eligible,
192 intended_tree: intended_tree.as_str(),
193 committed_paths: committed_paths.as_slice(),
194 selected_worktree_paths: selected.as_slice(),
195 events: &pass.events,
196 changed_passes,
197 changed_paths: changed_paths.as_slice(),
198 index_lock_timeout_ms: timeout_ms,
199 })? {
200 return Ok(result);
201 }
202 let (global_args, commit_args): (Vec<String>, Vec<String>) = private_invocation(&pass, pathspecs, selection);
203 let global_os: Vec<OsString> = global_args.iter().map(OsString::from).collect();
204 let lease: String = new_preparation_lease(workspace.landing.directory.as_path())
205 .map_err(|error| { return failed(format!("{error:?}")); })?;
206 let plan: HookDispatchPlan = compute_hook_dispatch_plan(&HookPlanInputs {
207 context,
208 cwd,
209 global_arguments: global_os.as_slice(),
210 common_directory: capture.layout.common_dir.as_path(),
211 worktree_root: repository_root,
212 lease: lease.as_str(),
213 concurrent_commits: invocation.config.concurrency.hooks.concurrent_commits,
214 inherited_lease_valid: invocation.inherited_lease_valid,
215 environment: invocation.environment,
216 snapshot: capture.layout.config_snapshot.as_ref(),
217 })
218 .map_err(|error| { return failed(format!("{error:?}")); })?;
219 write_hook_dispatch(workspace.hooks_directory.as_path(), &plan, invocation.executable)
220 .map_err(|error| { return failed(error.to_string()); })?;
221 let hooks: PreparationHooks = PreparationHooks {
222 directory: workspace.hooks_directory.clone(),
223 silenced_events: SILENCED_HOOK_EVENTS.iter().map(|event| { return String::from(*event); }).collect(),
224 };
225 let native_args: Vec<String> = if selection.staged_all {
226 without_all_flag(commit_args.as_slice())
227 } else {
228 commit_args.clone()
229 };
230 let preparation: PreparationRequest<'_> = PreparationRequest {
231 capture,
232 shadow: workspace.landing.shadow.as_path(),
233 hooks: &hooks,
234 commit_index: workspace.landing.commit_index.as_path(),
235 global_args: global_args.as_slice(),
236 commit_args: native_args.as_slice(),
237 };
238 let status: i32 = run_native_commit(context, cwd, &preparation).map_err(|error| { return TransactionStop::Message(error.to_string()); })?;
239 if status != 0 {
240 return Err(TransactionStop::NativeExit(status));
241 }
242 let prepared = read_prepared_commit(context, workspace.landing.shadow.as_path())?;
243 let hook_changes: HookChanges = describe_hook_changes(
244 context,
245 cwd,
246 workspace.object_directory.as_path(),
247 intended_tree.as_str(),
248 prepared.tree_oid.as_str(),
249 )?;
250 debug(
251 TAG,
252 format!("prepared {}; hooks changed {} paths", prepared.oid, hook_changes.paths.len()).as_str(),
253 );
254 let mut prepared_records: Vec<AddedPath> = selected.clone();
255 prepared_records.extend(hook_changes.worktree_records.iter().cloned());
256 write_journal_record(
257 workspace.landing.directory.as_path(),
258 PREPARED_FILENAME,
259 encode_prepared(&PreparedRecord {
260 shadow_path: workspace.landing.shadow.display().to_string(),
261 prepared_oid: prepared.oid.clone(),
262 signed: prepared.signed,
263 intended_tree_oid: intended_tree.clone(),
264 committed_paths: committed_paths.clone(),
265 added_paths: Vec::new(),
266 selected_worktree_paths: prepared_records.clone(),
267 })
268 .as_str(),
269 )?;
270 reach_transaction_phase(Phase::PreparationDone, None)?;
271 let settled_events: Vec<PolicyEvent> = events_with_changed_summary(pass.events.clone(), changed_passes, changed_paths.as_slice());
272 let options: ReplayOptions = replay_options(commit_args.as_slice());
273 let outcome: LoopOutcome = land_with_replay(LoopInput {
274 invocation,
275 workspace,
276 capture,
277 mode: selection.mode,
278 request,
279 checks,
280 prepared: &prepared,
281 settled_events: settled_events.clone(),
282 options: &options,
283 global_args: global_args.as_slice(),
284 read_only_selection: selection.read_only,
285 named_hooks_possible: plan.named_hooks_possible,
286 committed_paths: committed_paths.clone(),
287 added_paths: Vec::new(),
288 worktree_records: prepared_records.clone(),
289 hook_changes,
290 index_lock_timeout_ms: timeout_ms,
291 reserve_after_lost_races: invocation.config.concurrency.landing.reserve_after_lost_races,
292 })?;
293 let (oid, loop_events, final_worktree, final_added): (String, Vec<PolicyEvent>, Vec<AddedPath>, Vec<AddedPath>) =
294 match outcome {
295 LoopOutcome::Failed { events, exit_code } => {
296 return Ok(TransactionResult {
297 events,
298 exit_code,
299 landed_oid: None,
300 uses_lfs: false,
301 });
302 }
303 LoopOutcome::Landed {
304 oid,
305 events,
306 worktree_records,
307 added_paths,
308 } => (oid, events, worktree_records, added_paths),
309 };
310 let landed: LandedTransaction = LandedTransaction {
312 landed_oid: oid.clone(),
313 branch: capture.layout.target_ref.clone(),
314 cwd: cwd.to_path_buf(),
315 repository_root: repository_root.to_path_buf(),
316 directory: workspace.landing.directory.clone(),
317 shadow: workspace.landing.shadow.clone(),
318 git_dir: capture.layout.git_dir.clone(),
319 registry_root: capture.layout.registry_root.clone(),
320 real_index: capture.layout.real_index_path.clone(),
321 added_paths: final_added,
322 worktree_records: final_worktree,
323 config_snapshot: capture.layout.config_snapshot.clone(),
324 };
325 let completed = complete_landed_transaction(PostLandingRequest {
326 context,
327 plan: &plan,
328 landed: &landed,
329 });
330 if let Err(error) = completed {
331 return Err(TransactionStop::Message(error.to_string()));
332 }
333 workspace.finish_transaction();
336 workspace.mark_disposed();
337 let mut events: Vec<PolicyEvent> = settled_events;
338 events.extend(loop_events);
339 return Ok(TransactionResult {
340 events,
341 exit_code: 0,
342 landed_oid: Some(oid),
343 uses_lfs: capture.layout.config_snapshot.as_ref().is_none_or(uses_lfs),
344 });
345}
346
347#[cfg(test)]
349#[path = "commit_conclusion_tests.rs"]
350mod tests;