Skip to main content

git_policy_cli/landing/
service_prepare.rs

1//! What:
2//!  The preparation of a commit that goes through the resident service: the private index, the selection UI, the policies,
3//!  the settled changes, and the facts the landing needs, for every command line.
4//! Why:
5//!  The service lands what is prepared here, so the preparation covers every shape of commit: a branch, a detached head or a
6//!  branch that does not exist yet, an ordinary commit or the conclusion of a merge, a cherry-pick or a revert, a selection
7//!  by path, by `--include`, by `--patch`/`--interactive` or by a pathspec file, and a policy that rewrote a file. Whatever
8//!  Git alone can answer (an unmerged index, nothing to commit) is marked for Git to answer in the scratch repository, so its
9//!  words and its exit status are the person's. Nothing here locks anything, and nothing is registered
10//!  (`doc/planning/cli-git-resident-service.md`, "Landing every command line").
11//!
12//! In TS you'd write (pseudocode):
13//! ```ts
14//! const prepared = await prepare(invocation, checks, request, scratchName, attempt);
15//! ```
16
17use crate::capture_invocation::{InvocationCapture, base_revision, capture_invocation_base, capture_invocation_layout};
18use crate::private_index_request::{CaptureIndexes, indexed_request};
19use crate::commit_conclusion::{events_with_changed_summary, events_with_summary};
20use crate::commit_convergence::{CommitConvergence, CommitConvergenceInput, converge_commit_policies};
21use crate::commit_facts::PrivateIndexFacts;
22use crate::commit_index::{
23    IndexSelection, initialize_commit_index, list_changed_index_paths, list_unmerged_index_paths, prepare_interactive_selection,
24    write_private_tree,
25};
26use crate::commit_no_change::{NO_CHANGE_CODE, NO_CHANGE_CORE_ID, no_change_message};
27use crate::commit_staged_deletions::{STAGED_DELETION_CODE, STAGED_DELETION_CORE_ID, staged_deletion_message, staged_deletions_present_on_disk};
28use crate::commit_transaction::{
29    CommitInvocation, Selection, TransactionResult, TransactionStop, base_object, decide_selection, failed, private_checks, settled_passes, staged_at_base,
30};
31use crate::commit_worktree_records::selected_worktree_records;
32use crate::conclusion_cleanup::{CONCLUSION_STATE_FILES, SEQUENCER_DIRECTORY};
33use crate::config_snapshot::ConfigSnapshot;
34use crate::lfs_pre_push::uses_lfs;
35use crate::policy_checks::ShippedChecks;
36use crate::policy_engine::{StageRequest, pass_exit_code};
37use crate::policy_events::PolicyEvent;
38use crate::policy_pass::{PassResult, run_policy_pass};
39use crate::private_index_stage::PrivateStage;
40use crate::private_storage::{create_private_directory, remove_tree, write_private_file};
41use crate::repository_facts::RepositoryFacts;
42use crate::service_land_follow::{IndexPaths, update_index};
43use crate::service_pruned::reach_service_phase;
44use crate::transaction_phase::Phase;
45use crate::service_protocol::{ALLOWED_MODES, Change, Cleanup, MAX_CHANGES, is_object_name, is_plain_path};
46use crate::merge_second_pass::Merged as MergedFile;
47use crate::service_replay::{MergeSite, Merged, ReplayConflict, Retry, already_committed_result, capture_stamp, first_landed, merge_captured, word_merge_notice};
48use crate::external_hooks::warn_external_hooks;
49use crate::traditional_hook_warning::warn_traditional_hooks;
50use crate::transaction_git::{GitContext, run_git_checked};
51use crate::transaction_journal::{AddedPath, Base, Conclusion, RefFormat, SymbolicHead, TransactionMode};
52use crate::worktree_completion::install_added_worktree_files;
53use std::ffi::OsString;
54use std::path::{Path, PathBuf};
55
56/// A scratch directory for the private indexes and Git's scratch repository, removed when it goes out of scope.
57pub struct Scratch(pub PathBuf);
58
59impl Drop for Scratch {
60    fn drop(&mut self) {
61        let _removed = remove_tree(self.0.as_path());
62    }
63}
64
65/// What a preparation came to.
66pub enum Prepared {
67    /// The policies stopped the commit, or the preparation itself found the commit's ending (a staged deletion that is
68    ///  still on disk, a normalization that removed every change).
69    Ended(TransactionResult),
70    /// The change is ready to land.
71    Ready(Box<Ready>),
72    /// A retry could not merge the captured change onto the branch; nothing was changed.
73    Conflict(Box<ReplayConflict>),
74}
75
76/// What real Git needs from the preparation to build the commit in a scratch repository.
77pub struct NativeInputs {
78    /// The invocation capture the scratch repository is made from.
79    pub capture: InvocationCapture,
80    /// The arguments after the policies' fixed transforms.
81    pub arguments: Vec<OsString>,
82    /// The positional pathspecs of the command line, which the private index already holds.
83    pub pathspecs: Vec<String>,
84    /// Whether the line commits named paths or the index.
85    pub mode: TransactionMode,
86    /// Whether `-a` staged tracked changes into the private index.
87    pub staged_all: bool,
88    /// Whether the private index already holds a selection by `--include`, `--patch`, `--interactive`.
89    pub selected_privately: bool,
90    /// The private index the policies settled.
91    pub commit_index: PathBuf,
92}
93
94/// Everything a landing needs, from one preparation.
95pub struct Ready {
96    /// The worktree root.
97    pub repository: PathBuf,
98    /// The common Git directory.
99    pub common_dir: PathBuf,
100    /// The branch, or `HEAD` for a detached head.
101    pub reference: String,
102    /// The commit prepared against; the all-zero name for a branch that does not exist yet.
103    pub base: String,
104    /// The changes between the base and the private index, when the embedded route can carry them; nothing when a name is
105    ///  not text, a mode is not an ordinary file, a symbolic link or an executable, or there are too many.
106    pub changes: Option<Vec<Change>>,
107    /// The policies' events, with the fix summary when a correction changed a file.
108    pub events: Vec<PolicyEvent>,
109    /// Whether the repository uses Git LFS.
110    pub uses_lfs: bool,
111    /// The commit's scratch directory, kept until the commit is landed because Git's scratch repository lives in it.
112    pub scratch: Scratch,
113    /// Whether only Git can build this commit: the configuration asks for something the service does not embed, or the
114    ///  head, the branch or the state is not an ordinary one, or Git alone can say what the line means.
115    pub needs_native: bool,
116    /// What real Git needs to build the commit, or what the embedded route needs to make its tree.
117    pub inputs: NativeInputs,
118    /// The files a policy rewrote that the worktree receives once the commit has landed.
119    pub worktree_records: Vec<AddedPath>,
120    /// The conclusion state the landing removes, for the commit that concludes a merge, a cherry-pick or a revert.
121    pub cleanup: Option<Cleanup>,
122    /// Whether the person chose what to commit interactively, so that a moved branch cannot be answered by choosing again.
123    pub interactive: bool,
124    /// Whether a moved branch makes another build on the new tip meaningless (a conclusion, a branch that did not exist).
125    pub fixed_parents: bool,
126    /// When the worktree's bytes were captured (`service_replay::capture_stamp`); a retry keeps the first capture's stamp.
127    pub captured_at: u64,
128    /// The paths this commit commits that the unfinished merge or pick left in conflict. Their worktree bytes, conflict markers
129    ///  included, are what is committed; the person is warned once the commit has landed (`conflicted_path_warning`).
130    pub committed_conflicted: Vec<Vec<u8>>,
131    /// The operation a commit of named paths leaves in progress: the merge or the cherry-pick whose state the commit does not
132    ///  conclude. The person is told once the commit has landed.
133    pub leaves_open: Option<Conclusion>,
134}
135
136/// The all-zero object name of the repository's hash length, standing for a branch that does not exist yet.
137pub fn zero_object(empty_tree: &str) -> String {
138    return "0".repeat(empty_tree.len());
139}
140
141/// The changes between the base and the private index: path, mode and object, deletions as no object. Nothing when the
142///  embedded route cannot carry them.
143pub fn changes_of(context: &GitContext, cwd: &Path, indexes: &CaptureIndexes, base: &str) -> Result<Option<Vec<Change>>, TransactionStop> {
144    let arguments: Vec<String> = ["diff-index", "--cached", "-r", "--raw", "-z", "--no-renames", "--no-abbrev", base].map(String::from).to_vec();
145    let output = run_git_checked(
146        context,
147        &indexed_request(cwd, &arguments, Some(indexes.commit_index.as_path()), Some(indexes.object_directory.as_path())),
148    )?;
149    let text: String = match String::from_utf8(output.stdout) {
150        Ok(text) => text,
151        Err(_) => return Ok(None),
152    };
153    let mut fields = text.split('\0').filter(|field| { return !field.is_empty(); });
154    let mut changes: Vec<Change> = Vec::new();
155    while let Some(header) = fields.next() {
156        let Some(path) = fields.next() else { return Ok(None) };
157        let parts: Vec<&str> = header.trim_start_matches(':').split(' ').collect();
158        let [_, new_mode, _, new_name, status] = parts.as_slice() else { return Ok(None) };
159        let deletion: bool = status.starts_with('D');
160        let mode: u32 = if deletion { ALLOWED_MODES[0] } else { u32::from_str_radix(new_mode, 8).unwrap_or(0) };
161        if !is_plain_path(path) || !ALLOWED_MODES.contains(&mode) || (!deletion && !is_object_name(new_name)) {
162            return Ok(None);
163        }
164        changes.push(Change { path: path.to_string(), mode, blob: if deletion { None } else { Some((*new_name).to_string()) } });
165        if changes.len() > MAX_CHANGES {
166            return Ok(None);
167        }
168    }
169    return Ok(Some(changes));
170}
171
172/// Whether the repository's configuration makes native `git commit` do something the service does not: sign, apply a
173///  template or a clean-up mode, use another message encoding or another comment character, or change trailers.
174pub fn config_needs_native(snapshot: Option<&ConfigSnapshot>) -> bool {
175    const KEYS: [&str; 7] = ["commit.template", "commit.cleanup", "i18n.commitencoding", "core.commentchar", "core.commentstring", "commit.signoff", "commit.verbose"];
176    let Some(found) = snapshot else { return true };
177    return found.entries().iter().any(|entry| {
178        let value: String = entry.value.as_deref().map(|bytes| { return String::from_utf8_lossy(bytes).to_lowercase(); }).unwrap_or_default();
179        let signs: bool = entry.key == "commit.gpgsign" && !["false", "no", "off", "0"].contains(&value.as_str());
180        return signs || KEYS.contains(&entry.key.as_str()) || entry.key.starts_with("trailer.");
181    });
182}
183
184/// What:
185///  The pathspec file the private index reads, for a source named on the command line.
186/// Why:
187///   A regular file is read where it is named; standard input was read once, before the first preparation, and is written
188///  into each preparation's scratch directory, because a second read would see nothing.
189///
190/// In TS you'd write (pseudocode):
191/// ```ts
192/// pathspecFileFor(scratch, cwd, source, stdinBytes)
193/// ```
194fn pathspec_file_for(scratch: &Path, cwd: &Path, source: Option<&str>, standard_input: Option<&[u8]>) -> std::io::Result<Option<PathBuf>> {
195    let Some(name) = source else { return Ok(None) };
196    if name != "-" {
197        let path: PathBuf = PathBuf::from(name);
198        return Ok(Some(if path.is_absolute() { path } else { cwd.join(path) }));
199    }
200    let snapshot: PathBuf = scratch.join(crate::commit_index::PATHSPEC_INPUT_NAME);
201    write_private_file(snapshot.as_path(), standard_input.unwrap_or_default())?;
202    return Ok(Some(snapshot));
203}
204
205/// What the preparation reads that does not change between two preparations of one command.
206pub struct PrepareInputs<'a> {
207    /// The name of the commit's scratch directory under the Git directory.
208    pub scratch_name: &'a str,
209    /// Whether an empty commit is asked for.
210    pub allow_empty: bool,
211    /// The bytes of standard input when a pathspec file is read from it.
212    pub standard_input: Option<&'a [u8]>,
213    /// Whether this is the first preparation of the command, which says what a configuration warns about.
214    pub first: bool,
215    /// The change the first preparation captured, when the service sent the commit back because the branch moved: it is
216    ///  merged onto the branch as it is now, and the worktree is not read again.
217    pub replay: Option<Retry<'a>>,
218}
219
220/// Prepares the commit once against the branch as it is now.
221pub fn prepare<F: RepositoryFacts>(
222    invocation: &CommitInvocation<'_>,
223    checks: &mut ShippedChecks<F>,
224    request: &StageRequest,
225    inputs: &PrepareInputs<'_>,
226) -> Result<Prepared, TransactionStop> {
227    let context: &GitContext = invocation.context;
228    let cwd: &Path = invocation.cwd;
229    let region = invocation.region;
230    let layout = capture_invocation_layout(context, cwd, region.amend)?;
231    if inputs.first {
232        warn_traditional_hooks(layout.config_snapshot.as_ref(), &mut std::io::stderr());
233        warn_external_hooks(layout.config_snapshot.as_ref(), &mut std::io::stderr());
234    }
235    // Named paths beside another session's unfinished merge or stopped cherry-pick land as an ordinary commit, one in conflict
236    //  included: the owner's rule is "Committing a conflicted path is fine. Warn and commit anyway, even if conflict markers will
237    //  be committed." The paths in conflict are listed here so the warning can name those the commit takes. `-a` stages the working
238    //  copy of every tracked file, an unmerged one included, so it commits their markers too and is listed the same way.
239    let names_paths: bool = !region.pathspecs.is_empty() || region.pathspec_from_file.is_some();
240    let unmerged_before: Vec<Vec<u8>> = if region.all || (matches!(layout.conclusion, Conclusion::Merge | Conclusion::CherryPick) && names_paths) {
241        list_unmerged_index_paths(context, cwd, layout.real_index_path.as_path())?
242    } else {
243        Vec::new()
244    };
245    let selection: Selection = decide_selection(region, layout.conclusion);
246    let reference: String = match &layout.symbolic_head {
247        SymbolicHead::Branch(name) => name.clone(),
248        SymbolicHead::Detached => String::from("HEAD"),
249    };
250    let detached: bool = layout.symbolic_head == SymbolicHead::Detached;
251    let named: bool = selection.mode == TransactionMode::ExplicitPath;
252    let capture: InvocationCapture = capture_invocation_base(context, cwd, layout)?;
253    let unborn: bool = matches!(capture.base, Base::Unborn);
254    // Read from the snapshot the layout already holds, so nothing starts Git for the message or the identities before the
255    //  cheap decisions above are made, and before a service is provisioned.
256    let config_native: bool = config_needs_native(capture.layout.config_snapshot.as_ref());
257    let base: String = base_revision(&capture);
258    let announced_base: String = match &capture.base {
259        Base::Commit(oid) => oid.clone(),
260        Base::Unborn => zero_object(capture.layout.empty_tree_oid.as_str()),
261    };
262    let region_arguments: &[OsString] = &invocation.arguments[invocation.subcommand_index + 1..];
263    let pathspecs: Vec<OsString> = region.pathspecs.iter().map(|at| { return region_arguments[*at].clone(); }).collect();
264    let retry: Option<&Retry<'_>> = inputs.replay.as_ref();
265    if named && retry.is_none() {
266        // Before any private staging, so Git's "nothing to commit" or its force-add hint never stands in for the reason.
267        let present: Vec<Vec<u8>> = staged_deletions_present_on_disk(
268            context,
269            cwd,
270            capture.layout.repository_root.as_path(),
271            capture.layout.real_index_path.as_path(),
272            base.as_str(),
273            pathspecs.as_slice(),
274        )?;
275        if !present.is_empty() {
276            return Ok(Prepared::Ended(TransactionResult {
277                events: vec![PolicyEvent::CoreFinding {
278                    core_id: STAGED_DELETION_CORE_ID,
279                    code: STAGED_DELETION_CODE,
280                    message: staged_deletion_message(present.as_slice()),
281                }],
282                exit_code: 1,
283                landed_oid: None,
284                uses_lfs: false,
285            }));
286        }
287    }
288    let amends: bool = region.amend;
289    let scratch: Scratch = Scratch(capture.layout.git_dir.join(inputs.scratch_name));
290    create_private_directory(scratch.0.as_path()).map_err(|error| { return failed(format!("preparing the commit failed: {error}")); })?;
291    let indexes: CaptureIndexes = CaptureIndexes {
292        captured_index: scratch.0.join("captured.index"),
293        commit_index: scratch.0.join("commit.index"),
294        object_directory: capture.layout.object_directory.clone(),
295    };
296    let pathspec_source: Option<String> = region.pathspec_from_file.map(|value| {
297        return String::from_utf8_lossy(crate::command_options_query::value_bytes(region_arguments, value)).into_owned();
298    });
299    let pathspec_file: Option<PathBuf> = pathspec_file_for(scratch.0.as_path(), cwd, pathspec_source.as_deref(), inputs.standard_input)
300        .map_err(|error| { return failed(format!("reading the pathspec file failed: {error}")); })?;
301    let index_selection: IndexSelection<'_> = IndexSelection {
302        mode: selection.mode,
303        pathspecs: pathspecs.as_slice(),
304        pathspec_file: pathspec_file.as_deref(),
305        pathspec_file_nul: region.pathspec_file_nul,
306        stage_into_index: region.include,
307        stage_tracked_changes: selection.staged_all,
308        base_revision: base.as_str(),
309    };
310    // The parts the word-level second pass merged when a retry combined the change with the branch, told with the commit.
311    let mut word_merge_events: Option<PolicyEvent> = None;
312    if let Some(again) = retry {
313        // The change this command captured when it started, on the branch as it is now; the worktree is not read.
314        let site: MergeSite<'_> = MergeSite {
315            context,
316            cwd,
317            common_dir: capture.layout.common_dir.as_path(),
318            object_directory: indexes.object_directory.as_path(),
319            scratch: scratch.0.as_path(),
320        };
321        let (tree, combined): (String, Vec<MergedFile>) = match merge_captured(&site, again.captured, base.as_str(), again.attempt)? {
322            Merged::Conflict(paths) => {
323                let landed = first_landed(context, cwd, again.captured.base.as_str(), base.as_str(), paths.as_slice())?;
324                return Ok(Prepared::Conflict(Box::new(ReplayConflict { paths, landed, git_dir: capture.layout.git_dir.clone() })));
325            }
326            Merged::Tree(tree) => (tree, Vec::new()),
327            Merged::Combined { tree, files } => (tree, files),
328        };
329        run_git_checked(
330            context,
331            &indexed_request(cwd, &["read-tree", tree.as_str()], Some(indexes.commit_index.as_path()), Some(indexes.object_directory.as_path())),
332        )?;
333        // Every part the word pass merged is told to the person with the commit and sent to the journal.
334        word_merge_events = word_merge_notice(combined.as_slice());
335    } else {
336        initialize_commit_index(context, cwd, capture.layout.real_index_path.as_path(), &indexes, &index_selection)
337            .map_err(|error| { return failed(error.0); })?;
338    }
339    // The bytes are captured now; a retry keeps the stamp of the capture it replays.
340    let captured_at: u64 = retry.map_or_else(capture_stamp, |again| { return again.captured.captured; });
341    // A test removes the staged content here, between the writing and the reading by the policies.
342    reach_service_phase(Phase::ServiceStaged)?;
343    if (region.interactive || region.patch) && retry.is_none() {
344        prepare_interactive_selection(context, cwd, indexes.commit_index.as_path(), indexes.object_directory.as_path(), region.patch, &index_selection)?;
345    }
346    let interactive: bool = region.interactive || region.patch;
347    let reftable: bool = capture.layout.ref_format == RefFormat::Reftable;
348    // Whatever state a merge or a pick left (also a squash message with no merge in progress) is removed as native Git removes it.
349    let state_present: bool = selection.concludes_sequencer || CONCLUSION_STATE_FILES.iter().chain([SEQUENCER_DIRECTORY].iter()).any(|entry| { return capture.layout.git_dir.join(entry).exists(); });
350    let cleanup: Option<Cleanup> = state_present.then(|| {
351        return Cleanup { git_dir: capture.layout.git_dir.clone(), scratch: scratch.0.clone(), reftable };
352    });
353    let selected_privately: bool = selection.read_only;
354    let make_inputs = |arguments: Vec<OsString>, captured: InvocationCapture| -> NativeInputs {
355        return NativeInputs {
356            arguments,
357            pathspecs: pathspecs.iter().map(|token| { return token.to_string_lossy().into_owned(); }).collect(),
358            mode: selection.mode,
359            staged_all: selection.staged_all,
360            selected_privately,
361            commit_index: indexes.commit_index.clone(),
362            capture: captured,
363        };
364    };
365    let fixed_parents: bool = selection.concludes_sequencer || unborn;
366    let by_git = |taken: InvocationCapture, events: Vec<PolicyEvent>, changes: Option<Vec<Change>>, records: Vec<AddedPath>, arguments: Vec<OsString>, scratch_dir: Scratch| -> Box<Ready> {
367        return Box::new(Ready {
368            repository: taken.layout.repository_root.clone(),
369            common_dir: taken.layout.common_dir.clone(),
370            reference: reference.clone(),
371            base: announced_base.clone(),
372            changes,
373            events,
374            uses_lfs: taken.layout.config_snapshot.as_ref().is_none_or(uses_lfs),
375            scratch: scratch_dir,
376            needs_native: true,
377            inputs: make_inputs(arguments, taken),
378            worktree_records: records,
379            cleanup: cleanup.clone(),
380            interactive,
381            fixed_parents,
382            captured_at,
383            committed_conflicted: Vec::new(),
384            leaves_open: None,
385        });
386    };
387    // Files still unmerged when the commit names no paths: Git refuses here (`builtin/commit.c:348`, `refresh_cache_or_die`, which
388    //  exits with "Committing is not possible because you have unmerged files"). The owner's rule is "Warn and finish the merge"
389    //  (2026-11-03), so the working copy of each unmerged file is staged in the commit's private index, exactly as `git add` stages
390    //  it, and the commit that follows is the one Git would have built for a resolved merge. The files are named in the warning.
391    let unmerged_files: Vec<Vec<u8>> = list_unmerged_index_paths(context, cwd, indexes.commit_index.as_path())?;
392    if !unmerged_files.is_empty() {
393        let mut staging: Vec<OsString> = vec![OsString::from("add"), OsString::from("--all"), OsString::from("--")];
394        for path in &unmerged_files {
395            let mut spec: Vec<u8> = b":(top,literal)".to_vec();
396            spec.extend_from_slice(path.as_slice());
397            staging.push(<OsString as std::os::unix::ffi::OsStringExt>::from_vec(spec));
398        }
399        run_git_checked(context, &indexed_request(cwd, staging.as_slice(), Some(indexes.commit_index.as_path()), Some(indexes.object_directory.as_path())))?;
400    }
401    let candidate_paths: Vec<Vec<u8>> =
402        list_changed_index_paths(context, cwd, indexes.commit_index.as_path(), base.as_str(), indexes.object_directory.as_path())?;
403    // Nothing to commit is Git's to say, and an amend or an empty commit that was asked for is not nothing.
404    if candidate_paths.is_empty() && !inputs.allow_empty && !amends {
405        if retry.is_some() {
406            // Commits that landed meanwhile hold everything this one changes.
407            return Ok(Prepared::Ended(already_committed_result(reference.trim_start_matches("refs/heads/"))));
408        }
409        return Ok(Prepared::Ready(by_git(capture, Vec::new(), None, Vec::new(), invocation.arguments.to_vec(), scratch)));
410    }
411    let mut private: ShippedChecks<PrivateIndexFacts<&mut F>> = private_checks(
412        checks,
413        indexes.commit_index.as_path(),
414        indexes.object_directory.as_path(),
415        staged_at_base(base_object(&capture)?, candidate_paths.as_slice()),
416    );
417    // A selection the person made leaves the policies read-only: they report, and the commit is exactly what was chosen.
418    let (pass, changed_passes, changed_paths): (PassResult, u64, Vec<Vec<u8>>) = if selection.read_only {
419        private.can_apply_patches = false;
420        (run_policy_pass(request, &mut private), 0, Vec::new())
421    } else {
422        let outcome: CommitConvergence = converge_commit_policies(CommitConvergenceInput {
423            request,
424            checks: &mut private,
425            stage: PrivateStage { context, cwd, index: indexes.commit_index.as_path(), object_directory: Some(indexes.object_directory.as_path()) },
426        });
427        let passes: u64 = settled_passes(&outcome.convergence);
428        (outcome.pass, passes, outcome.changed_paths)
429    };
430    let code: i32 = pass_exit_code(pass.events.as_slice(), pass.end);
431    if code != 0 {
432        return Ok(Prepared::Ended(TransactionResult { events: pass.events, exit_code: code, landed_oid: None, uses_lfs: false }));
433    }
434    let repository_root: &Path = capture.layout.repository_root.as_path();
435    let install = private.content.install_changes();
436    let records: Vec<AddedPath> = selected_worktree_records(context, cwd, repository_root, indexes.commit_index.as_path(), install.as_slice())?;
437    // Corrections that removed every change leave nothing to commit: the copies are reconciled and the person is told.
438    let eligible: bool = !install.is_empty() && !region.allow_empty && !amends && !selection.concludes_sequencer;
439    if eligible && !unborn {
440        let intended: String = write_private_tree(context, cwd, indexes.commit_index.as_path(), indexes.object_directory.as_path())?;
441        let mut tree_request = indexed_request(cwd, &["rev-parse", "--verify", format!("{base}^{{tree}}").as_str()], None, None);
442        tree_request.without_prefix = true;
443        let base_tree: String = String::from_utf8_lossy(run_git_checked(context, &tree_request)?.stdout.as_slice()).trim().to_string();
444        if base_tree == intended {
445            return normalized(invocation, &capture, base.as_str(), &candidate_paths, records.as_slice(), &(pass.events, changed_passes, changed_paths));
446        }
447    }
448    let mut events: Vec<PolicyEvent> = events_with_changed_summary(pass.events.clone(), changed_passes, changed_paths.as_slice());
449    events.extend(word_merge_events);
450    let changes: Option<Vec<Change>> = if config_native || unborn || detached || state_present { None } else { changes_of(context, cwd, &indexes, base.as_str())? };
451    let forced: bool = config_native || unborn || detached || state_present || changes.is_none();
452    // A commit of named paths leaves the merge or cherry-pick whose state it does not conclude in progress.
453    let leaves_open: Option<Conclusion> = Some(capture.layout.conclusion).filter(|kind| { return named && matches!(kind, Conclusion::Merge | Conclusion::CherryPick); });
454    let mut ready: Box<Ready> = by_git(capture, events, changes, records, pass.arguments.clone(), scratch);
455    ready.needs_native = forced;
456    // A path in conflict that the commit takes is committed with the bytes the worktree holds, markers included.
457    ready.committed_conflicted = unmerged_before.into_iter().filter(|path| { return candidate_paths.contains(path); }).collect();
458    ready.committed_conflicted.extend(unmerged_files);
459    ready.leaves_open = leaves_open;
460    return Ok(Prepared::Ready(ready));
461}
462
463/// The ending of a commit whose corrections removed every change: the index entries and the worktree copies of the
464///  corrected files are reconciled with the base, and the command says so and exits 1.
465fn normalized(
466    invocation: &CommitInvocation<'_>,
467    capture: &InvocationCapture,
468    base: &str,
469    committed: &[Vec<u8>],
470    records: &[AddedPath],
471    settled: &(Vec<PolicyEvent>, u64, Vec<Vec<u8>>),
472) -> Result<Prepared, TransactionStop> {
473    let (events, passes, changed_paths) = settled;
474    update_index(invocation.context, capture.layout.repository_root.as_path(), capture.layout.real_index_path.as_path(), base, IndexPaths::Named(committed))
475        .map_err(|code| { return failed(format!("the index could not be reconciled with the corrected files: {}", code.word())); })?;
476    let completion = install_added_worktree_files(
477        invocation.context,
478        invocation.cwd,
479        capture.layout.repository_root.as_path(),
480        records,
481        Some(capture.layout.object_directory.as_path()),
482    )
483    .map_err(|error| { return TransactionStop::Message(error.0); })?;
484    let mut summary: Vec<PolicyEvent> = events_with_summary(events.clone(), *passes, changed_paths.as_slice());
485    summary.push(PolicyEvent::CoreFinding {
486        core_id: NO_CHANGE_CORE_ID,
487        code: NO_CHANGE_CODE,
488        message: no_change_message(completion.conflicted.as_slice()),
489    });
490    return Ok(Prepared::Ended(TransactionResult { events: summary, exit_code: 1, landed_oid: None, uses_lfs: false }));
491}