Skip to main content

git_policy_cli/
commit_transaction.rs

1//! What:
2//!  The commit transaction in the incumbent's order, up to the settled policies:
3//!  layout capture, the published workspace, the preparation base, `preparing.json`, the shadow repository, the
4//!  private index under capture order, interactive selection, and the bounded policy convergence.
5//! Why:
6//!  The incumbent (`src/policy-engine/commit-transaction.ts`) captures before it mutates, publishes the directory
7//!  before the base is read, and takes the landing lock only inside the landing attempt, never through preparation:
8//!  holding it here would serialize concurrent commits.
9//!  The conclusion (`commit_conclusion.rs`) takes the settled state to a landed commit.
10//!
11//! In TS you'd write (pseudocode):
12//! ```ts
13//! const result = await runCommitTransaction({ args, gitPath, policyOptions, concurrency });
14//! ```
15
16use super::candidate_object::{ObjectId, parse_object_id};
17use super::candidate_prediction::CandidateRequest;
18use super::capture_invocation::{
19    InvocationCapture, InvocationLayout, base_revision, capture_invocation_base, capture_invocation_layout,
20};
21use super::capture_order::{CaptureIndexes, CaptureOrderError, capture_commit_content};
22use super::capture_store::read_next_capture_sequence;
23use super::command_commit::CommitRegion;
24use super::command_options::OptionValue;
25use super::command_options_query::value_bytes;
26use super::commit_conclusion::{ConclusionInput, conclude_commit_transaction};
27use super::commit_convergence::{CommitConvergence, CommitConvergenceInput, converge_commit_policies};
28use super::commit_facts::PrivateIndexFacts;
29use super::commit_index::{
30    IndexSelection, initialize_commit_index, list_changed_index_paths, list_unmerged_index_paths,
31    materialize_pathspec_file, prepare_interactive_selection,
32};
33use super::commit_workspace::{CommitWorkspace, WorkspaceError, create_commit_workspace};
34use super::config_schema::CliGitConfig;
35use super::diagnostic_log::debug;
36use super::diagnostics::EngineFailureCode;
37use super::landing_attempt::LandingError;
38use super::markdown_linter::executable_linter;
39use super::policy_checks::{ShippedChecks, shipped_checks};
40use super::policy_forbidden_strings::default_scanner_settings;
41use super::policy_markdown::markdown_state;
42use super::policy_content::LifecycleContent;
43use super::policy_convergence::Convergence;
44use super::policy_engine::{StageRequest, pass_exit_code};
45use super::policy_events::PolicyEvent;
46use super::policy_pass::{PassResult, run_policy_pass};
47use super::policy_trigger::Trigger;
48use super::private_storage::{sync_directory, write_private_file};
49use super::repository_facts::RepositoryFacts;
50use super::transaction_git::{GitContext, GitFailure};
51use super::transaction_journal::{Base, Conclusion, PREPARING_FILENAME, PreparingRecord, TransactionMode};
52use super::transaction_journal_encode::encode_preparing;
53use super::transaction_phase::PhaseMarkerError;
54use super::wrapper_controls::Controls;
55use std::ffi::OsString;
56use std::path::{Path, PathBuf};
57
58/// Module tag for diagnostics.
59const TAG: &str = "runCommitTransaction";
60
61/// What:
62///  Everything the invocation fixes before the transaction starts.
63/// Why:
64///   The steps share one identity, one parsed commit region and one configuration; passing them together keeps a
65///   step from reading another invocation's state.
66///
67/// In TS you'd write (pseudocode):
68/// ```ts
69/// type CommitInvocation = { args; gitPath; cwd; region; policyOptions; concurrency };
70/// ```
71pub struct CommitInvocation<'a> {
72    /// Real Git, the child environment overlay, and the invocation's global options.
73    pub context: &'a GitContext,
74    /// The invocation's environment, read for leases and inherited configuration.
75    pub environment: &'a [(OsString, OsString)],
76    /// The effective invocation directory (the process directory moved by every `-C`).
77    pub cwd: &'a Path,
78    /// The arguments with the wrapper's controls removed.
79    pub arguments: &'a [OsString],
80    /// The index of `commit` within `arguments`.
81    pub subcommand_index: usize,
82    /// The commit options Git's table read.
83    pub region: &'a CommitRegion,
84    /// The configuration of the worktree.
85    pub config: &'a CliGitConfig,
86    /// The wrapper controls given on the command line.
87    pub controls: &'a Controls,
88    /// Whether this invocation runs under a valid outer preparation lease.
89    pub inherited_lease_valid: bool,
90    /// This executable, which the hook entries link to.
91    pub executable: &'a Path,
92}
93
94/// What:
95///  Why the transaction stopped without a policy result.
96/// Why:
97///   The incumbent's boundary maps each to a different ending: Git's own exit code with no wrapper text, a message
98///   with exit status 1, a finding with exit status 2, or a verbatim stop text.
99///
100/// In TS you'd write (pseudocode):
101/// ```ts
102/// NativeCommitFailedError | CommitTransactionGitError | IndexLockUnprovenOwnerError | Error
103/// ```
104#[derive(Debug)]
105pub enum TransactionStop {
106    /// Native `git commit` exited nonzero during preparation: its status is the invocation's, and Git printed why.
107    NativeExit(i32),
108    /// A Git step failed: the message reaches standard error and the status is 1.
109    Message(String),
110    /// A transaction failure reported as an engine failure event with status 2.
111    Failed {
112        /// The failure's stable code.
113        code: EngineFailureCode,
114        /// What went wrong.
115        message: String,
116    },
117}
118
119impl From<GitFailure> for TransactionStop {
120    /// A Git step failure keeps its message and ends the command with status 1.
121    fn from(failure: GitFailure) -> Self {
122        return TransactionStop::Message(failure.0);
123    }
124}
125
126impl From<WorkspaceError> for TransactionStop {
127    /// An unpublishable workspace fails the transaction.
128    fn from(failure: WorkspaceError) -> Self {
129        return failed(failure.to_string());
130    }
131}
132
133impl From<CaptureOrderError> for TransactionStop {
134    /// A capture failure fails the transaction.
135    fn from(failure: CaptureOrderError) -> Self {
136        return match failure {
137            CaptureOrderError::Git(git) => TransactionStop::Message(git.0),
138            other => failed(other.to_string()),
139        };
140    }
141}
142
143impl From<PhaseMarkerError> for TransactionStop {
144    /// A malformed test marker fails the transaction.
145    fn from(failure: PhaseMarkerError) -> Self {
146        return failed(failure.to_string());
147    }
148}
149
150impl From<LandingError> for TransactionStop {
151    /// A failed landing attempt: an unproven lock owner has its own code, the rest are transaction failures.
152    fn from(failure: LandingError) -> Self {
153        return match failure {
154            LandingError::Lock(super::index_writer_coordination::CoordinationFailure::UnprovenOwner(text)) => {
155                TransactionStop::Failed {
156                    code: EngineFailureCode::IndexLockUnprovenOwner,
157                    message: text,
158                }
159            }
160            LandingError::Git(git) => TransactionStop::Message(git.0),
161            other => failed(other.to_string()),
162        };
163    }
164}
165
166/// What:
167///  A transaction failure with the generic code.
168/// Why:
169///   Most unexpected failures are reported the same way.
170///
171/// In TS you'd write (pseudocode):
172/// ```ts
173/// initialTransactionFailure({ args, message })
174/// ```
175pub fn failed(message: String) -> TransactionStop {
176    return TransactionStop::Failed {
177        code: EngineFailureCode::TransactionFailed,
178        message,
179    };
180}
181
182/// What:
183///  What the transaction decided.
184/// Why:
185///   The command prints the events, runs the post-commit lifecycle for a landed commit, and exits with the code.
186///
187/// In TS you'd write (pseudocode):
188/// ```ts
189/// type CommitTransactionResult = { policyResult: PolicyEngineResult; committed: boolean; landedOid?: string };
190/// ```
191#[derive(Clone, Debug, Eq, PartialEq)]
192pub struct TransactionResult {
193    /// The events the invocation reports, in order.
194    pub events: Vec<PolicyEvent>,
195    /// The exit status when nothing landed; 0 after a landed commit.
196    pub exit_code: i32,
197    /// The landed commit, when the transaction landed one.
198    pub landed_oid: Option<String>,
199}
200
201/// What:
202///  The transaction directory's names, derived from the registry root and the transaction ID.
203/// Why:
204///   Callers that build facts before the transaction creates anything use the same names.
205///
206/// In TS you'd write (pseudocode):
207/// ```ts
208/// transactionPaths({ registryRoot, commonDir, transactionId });
209/// ```
210pub fn transaction_directory(registry_root: &Path, transaction_id: &str) -> PathBuf {
211    return registry_root.join(transaction_id);
212}
213
214/// What:
215///  Write one journal record durably: exclusively, then the directory entry.
216/// Why:
217///   A state file is never rewritten, so a crash leaves the newest complete state readable.
218///
219/// In TS you'd write (pseudocode):
220/// ```ts
221/// await writeJournalRecord({ directory, filename, record });
222/// ```
223pub fn write_journal_record(directory: &Path, filename: &str, text: &str) -> Result<(), TransactionStop> {
224    let path: PathBuf = directory.join(filename);
225    write_private_file(path.as_path(), text.as_bytes())
226        .map_err(|error| { return failed(format!("writing {} failed: {error}", path.display())); })?;
227    sync_directory(directory)
228        .map_err(|error| { return failed(format!("syncing {} failed: {error}", directory.display())); })?;
229    return Ok(());
230}
231
232/// What:
233///  The commit-region facts the transaction decides from, derived once.
234/// Why:
235///   The mode and the selection flags are read in several steps.
236///
237/// In TS you'd write (pseudocode):
238/// ```ts
239/// const { concludesSequencer, readOnlySelection, selectsPaths, stagedAll, mode } = ...;
240/// ```
241#[derive(Clone, Debug, Eq, PartialEq)]
242pub struct Selection {
243    /// Whether a pathless commit concludes a merge, cherry-pick or revert.
244    pub concludes_sequencer: bool,
245    /// Whether the selection UI (include, interactive, patch) leaves the policies read-only.
246    pub read_only: bool,
247    /// Whether any path was selected.
248    pub selects_paths: bool,
249    /// Whether `-a` stages privately.
250    pub staged_all: bool,
251    /// The private index mode.
252    pub mode: TransactionMode,
253}
254
255/// What:
256///  Decide the selection facts of one commit invocation.
257/// Why:
258///   Invalid combinations reach native Git unchanged, so it rejects them.
259///
260/// In TS you'd write (pseudocode):
261/// ```ts
262/// const mode = region.hasNoOnlyFlag || concludesSequencer || readOnlySelection || region.hasAllFlag || (!selectsPaths) ? 'index' : 'explicit-path';
263/// ```
264pub fn decide_selection(region: &CommitRegion, conclusion: Conclusion) -> Selection {
265    let concludes_sequencer: bool = conclusion != Conclusion::None && conclusion != Conclusion::Amend;
266    let read_only: bool = region.include || region.interactive || region.patch;
267    let selects_paths: bool = !region.pathspecs.is_empty() || region.pathspec_from_file.is_some();
268    let staged_all: bool = region.all && !selects_paths && !read_only && region.only != Some(true);
269    let index_mode: bool = region.only == Some(false)
270        || concludes_sequencer
271        || read_only
272        || region.all
273        || !selects_paths;
274    return Selection {
275        concludes_sequencer,
276        read_only,
277        selects_paths,
278        staged_all,
279        mode: if index_mode { TransactionMode::Index } else { TransactionMode::ExplicitPath },
280    };
281}
282
283/// What:
284///  The positional pathspec tokens of the commit region as text.
285/// Why:
286///   The private index selection and the private commit arguments name them.
287///
288/// In TS you'd write (pseudocode):
289/// ```ts
290/// region.pathspecs
291/// ```
292fn pathspec_texts(invocation: &CommitInvocation<'_>) -> Vec<String> {
293    let region_arguments: &[OsString] = &invocation.arguments[invocation.subcommand_index + 1..];
294    return invocation
295        .region
296        .pathspecs
297        .iter()
298        .map(|index| { return region_arguments[*index].to_string_lossy().into_owned(); })
299        .collect();
300}
301
302/// What:
303///  The pathspec-file source the commit names, as text.
304/// Why:
305///   Standard input is copied once into the transaction so no second read sees different bytes.
306///
307/// In TS you'd write (pseudocode):
308/// ```ts
309/// region.pathspecFile
310/// ```
311fn pathspec_file_source(invocation: &CommitInvocation<'_>) -> Option<String> {
312    let region_arguments: &[OsString] = &invocation.arguments[invocation.subcommand_index + 1..];
313    let value: OptionValue = invocation.region.pathspec_from_file?;
314    return Some(String::from_utf8_lossy(value_bytes(region_arguments, value)).into_owned());
315}
316
317/// What:
318///  The prepared state a converged commit hands to its conclusion.
319/// Why:
320///   The conclusion needs the pass that settled and the paths the commit carries.
321///
322/// In TS you'd write (pseudocode):
323/// ```ts
324/// type SettledCommitTransaction = { workspace; capture; mode; pass; changedPasses; changedPaths; ... };
325/// ```
326struct Converged {
327    /// The settled pass, whose events the invocation reports.
328    pass: PassResult,
329    /// How many passes changed content before it settled.
330    changed_passes: u64,
331    /// The paths the corrections wrote.
332    changed_paths: Vec<String>,
333}
334
335/// What:
336///  Rebuild the shipped checks over the private commit index: rule facts stay real, the candidates come from the
337///  private index against the recorded base.
338/// Why:
339///   The incumbent's rules ask real Git about the real index, while the content policies read what the transaction
340///   corrects.
341///
342/// In TS you'd write (pseudocode):
343/// ```ts
344/// const facts = createPrivateIndexFacts({ gitPath, cwd, indexPath, paths, baseRevision, objectDirectory });
345/// ```
346pub fn private_checks<'a, F: RepositoryFacts>(
347    checks: &'a mut ShippedChecks<F>,
348    index: &Path,
349    object_directory: &Path,
350    candidates: CandidateRequest,
351) -> ShippedChecks<PrivateIndexFacts<&'a mut F>> {
352    // The scanner settings and the Markdown linter move to the rebuilt checks; the originals are placeholders that
353    // nothing reads again, because the rebuilt checks run every policy of this transaction.
354    let scanner_settings = std::mem::replace(&mut checks.scanner_settings, default_scanner_settings());
355    let markdown = std::mem::replace(&mut checks.markdown, markdown_state(Box::new(executable_linter(&[]))));
356    let arguments: Vec<OsString> = checks.arguments.clone();
357    let allowed_worktree_dirs: Vec<PathBuf> = checks.allowed_worktree_dirs.clone();
358    let mut rebuilt: ShippedChecks<PrivateIndexFacts<&'a mut F>> = shipped_checks(
359        PrivateIndexFacts::new(&mut checks.facts, index, object_directory),
360        arguments,
361        LifecycleContent::Requested(candidates),
362        allowed_worktree_dirs,
363    );
364    rebuilt.scanner_settings = scanner_settings;
365    rebuilt.markdown = markdown;
366    return rebuilt;
367}
368
369/// What:
370///  Whether the capture lists worktree changes that the commit stages: `-a` stages every tracked change and
371///  `--include` stages the named paths.
372/// Why:
373///   The capture record names those paths, so recovery copies exactly the worktree files the commit staged.
374///
375/// In TS you'd write (pseudocode):
376/// ```ts
377/// stagedAll || include
378/// ```
379pub fn stages_worktree(staged_all: bool, include: bool) -> bool {
380    return staged_all || include;
381}
382
383/// What:
384///  How many passes changed the state when the convergence settled; any other ending counts none.
385/// Why:
386///   The fix summary names the passes of a settled convergence only.
387///
388/// In TS you'd write (pseudocode):
389/// ```ts
390/// convergence.kind === 'settled' ? convergence.changedPasses : 0
391/// ```
392pub fn settled_passes(convergence: &Convergence) -> u64 {
393    return match convergence {
394        Convergence::Settled { changed_passes } => *changed_passes as u64,
395        _ => 0,
396    };
397}
398
399/// What:
400///  The candidate request for the paths a private index changes against the recorded base.
401/// Why:
402///   The policies check exactly the paths the commit changes; each path is literal and rooted at the top level, so a
403///   name with glob characters or a directory below the current one still names one file.
404///
405/// In TS you'd write (pseudocode):
406/// ```ts
407/// createPrivateIndexFacts({ paths: candidatePaths, baseRevision })
408/// ```
409pub fn staged_at_base(base: Option<ObjectId>, paths: &[String]) -> CandidateRequest {
410    return CandidateRequest::StagedAtBase {
411        base,
412        pathspecs: paths.iter().map(|path| { return OsString::from(format!(":(top,literal){path}")); }).collect(),
413    };
414}
415
416/// What:
417///  The recorded base as an object ID, or nothing for an unborn branch.
418/// Why:
419///   The candidate listing compares the private index with this commit.
420///
421/// In TS you'd write (pseudocode):
422/// ```ts
423/// capture.base.kind === 'commit' ? capture.base.oid : undefined
424/// ```
425pub fn base_object(capture: &InvocationCapture) -> Result<Option<ObjectId>, TransactionStop> {
426    return match &capture.base {
427        Base::Unborn => Ok(None),
428        Base::Commit(oid) => match parse_object_id(oid.as_bytes()) {
429            Some(found) => Ok(Some(found)),
430            None => Err(failed(format!("the preparation base {oid} is not an object name"))),
431        },
432    };
433}
434
435/// What:
436///  Run the pre-forward policies over the private index until they settle.
437/// Why:
438///   A read-only selection commits exactly what the person chose, so its single pass applies no patch.
439///
440/// In TS you'd write (pseudocode):
441/// ```ts
442/// readOnlySelection ? { kind: 'settled', pass: firstPass } : await convergeCommitPolicies({ ... });
443/// ```
444fn converge<G: RepositoryFacts>(
445    request: &StageRequest,
446    checks: &mut ShippedChecks<PrivateIndexFacts<G>>,
447    context: &GitContext,
448    cwd: &Path,
449    workspace: &CommitWorkspace,
450    read_only: bool,
451) -> Converged {
452    if read_only {
453        checks.can_apply_patches = false;
454        let pass: PassResult = run_policy_pass(request, checks);
455        return Converged {
456            pass,
457            changed_passes: 0,
458            changed_paths: Vec::new(),
459        };
460    }
461    let outcome: CommitConvergence = converge_commit_policies(CommitConvergenceInput {
462        request,
463        checks,
464        stage: super::private_index_stage::PrivateStage {
465            context,
466            cwd,
467            index: workspace.landing.commit_index.as_path(),
468            object_directory: Some(workspace.object_directory.as_path()),
469        },
470    });
471    let changed_passes: u64 = settled_passes(&outcome.convergence);
472    return Converged {
473        pass: outcome.pass,
474        changed_passes,
475        changed_paths: outcome.changed_paths,
476    };
477}
478
479/// What:
480///  Run one commit through the transaction: capture, private preparation, serial landing and post-landing completion.
481/// Why:
482///   Every non-dry-run commit runs through it, including clean commits and `commit -a`, because native `git commit`
483///   keeps `index.lock` on disk through hooks and the editor.
484///
485/// In TS you'd write (pseudocode):
486/// ```ts
487/// return await runCommitTransaction({ args, gitPath, policyOptions, concurrency });
488/// ```
489pub fn run_commit_transaction<F: RepositoryFacts>(
490    invocation: &CommitInvocation<'_>,
491    checks: &mut ShippedChecks<F>,
492    request: &StageRequest,
493) -> Result<TransactionResult, TransactionStop> {
494    let context: &GitContext = invocation.context;
495    let cwd: &Path = invocation.cwd;
496    let layout: InvocationLayout = capture_invocation_layout(context, cwd, invocation.region.amend)?;
497    let selection: Selection = decide_selection(invocation.region, layout.conclusion);
498    let mut workspace: CommitWorkspace = create_commit_workspace(&layout)?;
499    // Read after publication and before the base, so landed-capture pruning keeps every record this transaction
500    // may replay over.
501    let next_sequence_before_base: i64 = read_next_capture_sequence(layout.git_dir.as_path())
502        .map_err(|error| { return failed(error.0); })?;
503    let capture: InvocationCapture = capture_invocation_base(context, cwd, layout)?;
504    let base: String = base_revision(&capture);
505    let pathspecs: Vec<String> = pathspec_texts(invocation);
506    write_journal_record(
507        workspace.landing.directory.as_path(),
508        PREPARING_FILENAME,
509        encode_preparing(&PreparingRecord {
510            transaction_id: workspace.landing.transaction_id.clone(),
511            mode: selection.mode,
512            base: capture.base.clone(),
513            symbolic_head: capture.layout.symbolic_head.clone(),
514            target_ref: capture.layout.target_ref.clone(),
515            conclusion: capture.layout.conclusion,
516            repository_root: capture.layout.repository_root.display().to_string(),
517            git_dir: capture.layout.git_dir.display().to_string(),
518            common_dir: capture.layout.common_dir.display().to_string(),
519            real_index_path: capture.layout.real_index_path.display().to_string(),
520            object_directory: capture.layout.object_directory.display().to_string(),
521            ref_format: capture.layout.ref_format,
522            empty_tree_oid: capture.layout.empty_tree_oid.clone(),
523            shadow_path: workspace.landing.shadow.display().to_string(),
524            selected_pathspecs: pathspecs.clone(),
525            invoked_at: capture.layout.invoked_at.clone(),
526        })
527        .as_str(),
528    )?;
529    // Before the private index exists, so every object preparation writes lands in the shadow store, which a
530    // concurrent real `gc --prune=now` never sees.
531    super::shadow_create::create_shadow_repository(
532        context,
533        cwd,
534        &capture,
535        workspace.landing.transaction_id.as_str(),
536        workspace.landing.directory.as_path(),
537    )?;
538    let pathspec_file: Option<PathBuf> = materialize_pathspec_file(
539        workspace.landing.directory.as_path(),
540        cwd,
541        pathspec_file_source(invocation).as_deref(),
542        &mut std::io::stdin(),
543    )
544    .map_err(|error| { return failed(format!("reading the pathspec file failed: {error}")); })?;
545    let indexes: CaptureIndexes = CaptureIndexes {
546        captured_index: workspace.landing.captured_index.clone(),
547        commit_index: workspace.landing.commit_index.clone(),
548        object_directory: workspace.object_directory.clone(),
549    };
550    let index_selection: IndexSelection<'_> = IndexSelection {
551        mode: selection.mode,
552        pathspecs: pathspecs.as_slice(),
553        pathspec_file: pathspec_file.as_deref(),
554        pathspec_file_nul: invocation.region.pathspec_file_nul,
555        stage_into_index: invocation.region.include,
556        stage_tracked_changes: selection.staged_all,
557        base_revision: base.as_str(),
558    };
559    capture_commit_content(
560        context,
561        cwd,
562        capture.layout.git_dir.as_path(),
563        workspace.landing.directory.as_path(),
564        &indexes,
565        selection.mode,
566        stages_worktree(selection.staged_all, invocation.region.include),
567        base.as_str(),
568        next_sequence_before_base,
569        || {
570            return initialize_commit_index(
571                context,
572                cwd,
573                capture.layout.real_index_path.as_path(),
574                &indexes,
575                &index_selection,
576            )
577            .map_err(CaptureOrderError::from);
578        },
579    )?;
580    if invocation.region.interactive || invocation.region.patch {
581        prepare_interactive_selection(
582            context,
583            cwd,
584            workspace.landing.commit_index.as_path(),
585            workspace.object_directory.as_path(),
586            invocation.region.patch,
587            &index_selection,
588        )?;
589    }
590    let unmerged: Vec<String> =
591        list_unmerged_index_paths(context, cwd, workspace.landing.commit_index.as_path())?;
592    if !unmerged.is_empty() {
593        return Ok(TransactionResult {
594            events: vec![PolicyEvent::EngineFailure {
595                code: EngineFailureCode::ContentUnavailable,
596                message: format!(
597                    "Automatic commit fixes do not support unmerged index paths: {}",
598                    unmerged.join(", ")
599                ),
600                trigger: Some(Trigger::PreForward),
601                policy: None,
602                path: None,
603            }],
604            exit_code: 2,
605            landed_oid: None,
606        });
607    }
608    let candidate_paths: Vec<String> = list_changed_index_paths(
609        context,
610        cwd,
611        workspace.landing.commit_index.as_path(),
612        base.as_str(),
613        workspace.object_directory.as_path(),
614    )?;
615    let mut private: ShippedChecks<PrivateIndexFacts<&mut F>> = private_checks(
616        checks,
617        workspace.landing.commit_index.as_path(),
618        workspace.object_directory.as_path(),
619        staged_at_base(base_object(&capture)?, candidate_paths.as_slice()),
620    );
621    let converged: Converged = converge(request, &mut private, context, cwd, &workspace, selection.read_only);
622    let code: i32 = pass_exit_code(converged.pass.events.as_slice(), converged.pass.end);
623    if code != 0 {
624        debug(TAG, "policies blocked the commit before preparation");
625        return Ok(TransactionResult {
626            events: converged.pass.events,
627            exit_code: code,
628            landed_oid: None,
629        });
630    }
631    return conclude_commit_transaction(ConclusionInput {
632        invocation,
633        workspace: &mut workspace,
634        capture: &capture,
635        selection: &selection,
636        request,
637        checks: &mut private,
638        pass: converged.pass,
639        changed_passes: converged.changed_passes,
640        changed_paths: converged.changed_paths,
641        committed_paths: candidate_paths,
642        pathspecs: pathspecs.as_slice(),
643    });
644}
645
646/// Controls for this module stay out of the release executable.
647#[cfg(test)]
648#[path = "commit_transaction_tests.rs"]
649mod tests;