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