1use super::capture_invocation::{InvocationCapture, base_revision, capture_invocation_base, capture_invocation_layout};
18use super::private_index_request::{CaptureIndexes, indexed_request};
19use super::commit_conclusion::{events_with_changed_summary, events_with_summary};
20use super::commit_convergence::{CommitConvergence, CommitConvergenceInput, converge_commit_policies};
21use super::commit_facts::PrivateIndexFacts;
22use super::commit_index::{
23 IndexSelection, initialize_commit_index, list_changed_index_paths, list_unmerged_index_paths, prepare_interactive_selection,
24 write_private_tree,
25};
26use super::commit_no_change::{NO_CHANGE_CODE, NO_CHANGE_CORE_ID, no_change_message};
27use super::commit_staged_deletions::{STAGED_DELETION_CODE, STAGED_DELETION_CORE_ID, staged_deletion_message, staged_deletions_present_on_disk};
28use super::commit_transaction::{
29 CommitInvocation, Selection, TransactionResult, TransactionStop, base_object, decide_selection, failed, private_checks, settled_passes, staged_at_base,
30};
31use super::commit_worktree_records::selected_worktree_records;
32use super::conclusion_cleanup::{CONCLUSION_STATE_FILES, SEQUENCER_DIRECTORY};
33use super::config_snapshot::ConfigSnapshot;
34use super::lfs_pre_push::uses_lfs;
35use super::policy_checks::ShippedChecks;
36use super::policy_engine::{StageRequest, pass_exit_code};
37use super::policy_events::PolicyEvent;
38use super::policy_pass::{PassResult, run_policy_pass};
39use super::private_index_stage::PrivateStage;
40use super::private_storage::{create_private_directory, remove_tree, write_private_file};
41use super::repository_facts::RepositoryFacts;
42use super::service_land_follow::{IndexPaths, update_index};
43use super::service_pruned::reach_service_phase;
44use super::transaction_phase::Phase;
45use super::service_protocol::{ALLOWED_MODES, Change, Cleanup, MAX_CHANGES, is_object_name, is_plain_path};
46use super::service_replay::{MergeSite, Merged, ReplayConflict, Retry, already_committed_result, capture_stamp, first_landed, merge_captured};
47use super::external_hooks::warn_external_hooks;
48use super::traditional_hook_warning::warn_traditional_hooks;
49use super::transaction_git::{GitContext, run_git_checked};
50use super::transaction_journal::{AddedPath, Base, RefFormat, SymbolicHead, TransactionMode};
51use super::worktree_completion::install_added_worktree_files;
52use std::ffi::OsString;
53use std::path::{Path, PathBuf};
54
55pub struct Scratch(pub PathBuf);
57
58impl Drop for Scratch {
59 fn drop(&mut self) {
60 let _removed = remove_tree(self.0.as_path());
61 }
62}
63
64pub enum Prepared {
66 Ended(TransactionResult),
69 Ready(Box<Ready>),
71 Conflict(Box<ReplayConflict>),
73}
74
75pub struct NativeInputs {
77 pub capture: InvocationCapture,
79 pub arguments: Vec<OsString>,
81 pub pathspecs: Vec<String>,
83 pub mode: TransactionMode,
85 pub staged_all: bool,
87 pub selected_privately: bool,
89 pub commit_index: PathBuf,
91}
92
93pub struct Ready {
95 pub repository: PathBuf,
97 pub common_dir: PathBuf,
99 pub reference: String,
101 pub base: String,
103 pub changes: Option<Vec<Change>>,
106 pub events: Vec<PolicyEvent>,
108 pub uses_lfs: bool,
110 pub scratch: Scratch,
112 pub needs_native: bool,
115 pub inputs: NativeInputs,
117 pub worktree_records: Vec<AddedPath>,
119 pub cleanup: Option<Cleanup>,
121 pub interactive: bool,
123 pub fixed_parents: bool,
125 pub captured_at: u64,
127}
128
129pub fn zero_object(empty_tree: &str) -> String {
131 return "0".repeat(empty_tree.len());
132}
133
134pub fn changes_of(context: &GitContext, cwd: &Path, indexes: &CaptureIndexes, base: &str) -> Result<Option<Vec<Change>>, TransactionStop> {
137 let arguments: Vec<String> = ["diff-index", "--cached", "-r", "--raw", "-z", "--no-renames", "--no-abbrev", base].map(String::from).to_vec();
138 let output = run_git_checked(
139 context,
140 &indexed_request(cwd, &arguments, Some(indexes.commit_index.as_path()), Some(indexes.object_directory.as_path())),
141 )?;
142 let text: String = match String::from_utf8(output.stdout) {
143 Ok(text) => text,
144 Err(_) => return Ok(None),
145 };
146 let mut fields = text.split('\0').filter(|field| { return !field.is_empty(); });
147 let mut changes: Vec<Change> = Vec::new();
148 while let Some(header) = fields.next() {
149 let Some(path) = fields.next() else { return Ok(None) };
150 let parts: Vec<&str> = header.trim_start_matches(':').split(' ').collect();
151 let [_, new_mode, _, new_name, status] = parts.as_slice() else { return Ok(None) };
152 let deletion: bool = status.starts_with('D');
153 let mode: u32 = if deletion { ALLOWED_MODES[0] } else { u32::from_str_radix(new_mode, 8).unwrap_or(0) };
154 if !is_plain_path(path) || !ALLOWED_MODES.contains(&mode) || (!deletion && !is_object_name(new_name)) {
155 return Ok(None);
156 }
157 changes.push(Change { path: path.to_string(), mode, blob: if deletion { None } else { Some((*new_name).to_string()) } });
158 if changes.len() > MAX_CHANGES {
159 return Ok(None);
160 }
161 }
162 return Ok(Some(changes));
163}
164
165pub fn config_needs_native(snapshot: Option<&ConfigSnapshot>) -> bool {
168 const KEYS: [&str; 7] = ["commit.template", "commit.cleanup", "i18n.commitencoding", "core.commentchar", "core.commentstring", "commit.signoff", "commit.verbose"];
169 let Some(found) = snapshot else { return true };
170 return found.entries().iter().any(|entry| {
171 let value: String = entry.value.as_deref().map(|bytes| { return String::from_utf8_lossy(bytes).to_lowercase(); }).unwrap_or_default();
172 let signs: bool = entry.key == "commit.gpgsign" && !["false", "no", "off", "0"].contains(&value.as_str());
173 return signs || KEYS.contains(&entry.key.as_str()) || entry.key.starts_with("trailer.");
174 });
175}
176
177fn pathspec_file_for(scratch: &Path, cwd: &Path, source: Option<&str>, standard_input: Option<&[u8]>) -> std::io::Result<Option<PathBuf>> {
188 let Some(name) = source else { return Ok(None) };
189 if name != "-" {
190 let path: PathBuf = PathBuf::from(name);
191 return Ok(Some(if path.is_absolute() { path } else { cwd.join(path) }));
192 }
193 let snapshot: PathBuf = scratch.join(super::commit_index::PATHSPEC_INPUT_NAME);
194 write_private_file(snapshot.as_path(), standard_input.unwrap_or_default())?;
195 return Ok(Some(snapshot));
196}
197
198pub struct PrepareInputs<'a> {
200 pub scratch_name: &'a str,
202 pub allow_empty: bool,
204 pub standard_input: Option<&'a [u8]>,
206 pub first: bool,
208 pub replay: Option<Retry<'a>>,
211}
212
213pub fn prepare<F: RepositoryFacts>(
215 invocation: &CommitInvocation<'_>,
216 checks: &mut ShippedChecks<F>,
217 request: &StageRequest,
218 inputs: &PrepareInputs<'_>,
219) -> Result<Prepared, TransactionStop> {
220 let context: &GitContext = invocation.context;
221 let cwd: &Path = invocation.cwd;
222 let region = invocation.region;
223 let layout = capture_invocation_layout(context, cwd, region.amend)?;
224 if inputs.first {
225 warn_traditional_hooks(layout.config_snapshot.as_ref(), &mut std::io::stderr());
226 warn_external_hooks(layout.config_snapshot.as_ref(), &mut std::io::stderr());
227 }
228 let selection: Selection = decide_selection(region, layout.conclusion);
229 let reference: String = match &layout.symbolic_head {
230 SymbolicHead::Branch(name) => name.clone(),
231 SymbolicHead::Detached => String::from("HEAD"),
232 };
233 let detached: bool = layout.symbolic_head == SymbolicHead::Detached;
234 let named: bool = selection.mode == TransactionMode::ExplicitPath;
235 let capture: InvocationCapture = capture_invocation_base(context, cwd, layout)?;
236 let unborn: bool = matches!(capture.base, Base::Unborn);
237 let config_native: bool = config_needs_native(capture.layout.config_snapshot.as_ref());
240 let base: String = base_revision(&capture);
241 let announced_base: String = match &capture.base {
242 Base::Commit(oid) => oid.clone(),
243 Base::Unborn => zero_object(capture.layout.empty_tree_oid.as_str()),
244 };
245 let region_arguments: &[OsString] = &invocation.arguments[invocation.subcommand_index + 1..];
246 let pathspecs: Vec<OsString> = region.pathspecs.iter().map(|at| { return region_arguments[*at].clone(); }).collect();
247 let retry: Option<&Retry<'_>> = inputs.replay.as_ref();
248 if named && retry.is_none() {
249 let present: Vec<Vec<u8>> = staged_deletions_present_on_disk(
251 context,
252 cwd,
253 capture.layout.repository_root.as_path(),
254 capture.layout.real_index_path.as_path(),
255 base.as_str(),
256 pathspecs.as_slice(),
257 )?;
258 if !present.is_empty() {
259 return Ok(Prepared::Ended(TransactionResult {
260 events: vec![PolicyEvent::CoreFinding {
261 core_id: STAGED_DELETION_CORE_ID,
262 code: STAGED_DELETION_CODE,
263 message: staged_deletion_message(present.as_slice()),
264 }],
265 exit_code: 1,
266 landed_oid: None,
267 uses_lfs: false,
268 }));
269 }
270 }
271 let amends: bool = region.amend;
272 let scratch: Scratch = Scratch(capture.layout.git_dir.join(inputs.scratch_name));
273 create_private_directory(scratch.0.as_path()).map_err(|error| { return failed(format!("preparing the commit failed: {error}")); })?;
274 let indexes: CaptureIndexes = CaptureIndexes {
275 captured_index: scratch.0.join("captured.index"),
276 commit_index: scratch.0.join("commit.index"),
277 object_directory: capture.layout.object_directory.clone(),
278 };
279 let pathspec_source: Option<String> = region.pathspec_from_file.map(|value| {
280 return String::from_utf8_lossy(super::command_options_query::value_bytes(region_arguments, value)).into_owned();
281 });
282 let pathspec_file: Option<PathBuf> = pathspec_file_for(scratch.0.as_path(), cwd, pathspec_source.as_deref(), inputs.standard_input)
283 .map_err(|error| { return failed(format!("reading the pathspec file failed: {error}")); })?;
284 let index_selection: IndexSelection<'_> = IndexSelection {
285 mode: selection.mode,
286 pathspecs: pathspecs.as_slice(),
287 pathspec_file: pathspec_file.as_deref(),
288 pathspec_file_nul: region.pathspec_file_nul,
289 stage_into_index: region.include,
290 stage_tracked_changes: selection.staged_all,
291 base_revision: base.as_str(),
292 };
293 if let Some(again) = retry {
294 let site: MergeSite<'_> = MergeSite {
296 context,
297 cwd,
298 common_dir: capture.layout.common_dir.as_path(),
299 object_directory: indexes.object_directory.as_path(),
300 scratch: scratch.0.as_path(),
301 };
302 match merge_captured(&site, again.captured, base.as_str(), again.attempt)? {
303 Merged::Conflict(paths) => {
304 let landed = first_landed(context, cwd, again.captured.base.as_str(), base.as_str(), paths.as_slice())?;
305 return Ok(Prepared::Conflict(Box::new(ReplayConflict { paths, landed, git_dir: capture.layout.git_dir.clone() })));
306 }
307 Merged::Tree(tree) => {
308 run_git_checked(
309 context,
310 &indexed_request(cwd, &["read-tree", tree.as_str()], Some(indexes.commit_index.as_path()), Some(indexes.object_directory.as_path())),
311 )?;
312 }
313 }
314 } else {
315 initialize_commit_index(context, cwd, capture.layout.real_index_path.as_path(), &indexes, &index_selection)
316 .map_err(|error| { return failed(error.0); })?;
317 }
318 let captured_at: u64 = retry.map_or_else(capture_stamp, |again| { return again.captured.captured; });
320 reach_service_phase(Phase::ServiceStaged)?;
322 if (region.interactive || region.patch) && retry.is_none() {
323 prepare_interactive_selection(context, cwd, indexes.commit_index.as_path(), indexes.object_directory.as_path(), region.patch, &index_selection)?;
324 }
325 let interactive: bool = region.interactive || region.patch;
326 let reftable: bool = capture.layout.ref_format == RefFormat::Reftable;
327 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(); });
329 let cleanup: Option<Cleanup> = state_present.then(|| {
330 return Cleanup { git_dir: capture.layout.git_dir.clone(), scratch: scratch.0.clone(), reftable };
331 });
332 let selected_privately: bool = selection.read_only;
333 let make_inputs = |arguments: Vec<OsString>, captured: InvocationCapture| -> NativeInputs {
334 return NativeInputs {
335 arguments,
336 pathspecs: pathspecs.iter().map(|token| { return token.to_string_lossy().into_owned(); }).collect(),
337 mode: selection.mode,
338 staged_all: selection.staged_all,
339 selected_privately,
340 commit_index: indexes.commit_index.clone(),
341 capture: captured,
342 };
343 };
344 let fixed_parents: bool = selection.concludes_sequencer || unborn;
345 let by_git = |taken: InvocationCapture, events: Vec<PolicyEvent>, changes: Option<Vec<Change>>, records: Vec<AddedPath>, arguments: Vec<OsString>, scratch_dir: Scratch| -> Box<Ready> {
346 return Box::new(Ready {
347 repository: taken.layout.repository_root.clone(),
348 common_dir: taken.layout.common_dir.clone(),
349 reference: reference.clone(),
350 base: announced_base.clone(),
351 changes,
352 events,
353 uses_lfs: taken.layout.config_snapshot.as_ref().is_none_or(uses_lfs),
354 scratch: scratch_dir,
355 needs_native: true,
356 inputs: make_inputs(arguments, taken),
357 worktree_records: records,
358 cleanup: cleanup.clone(),
359 interactive,
360 fixed_parents,
361 captured_at,
362 });
363 };
364 if !list_unmerged_index_paths(context, cwd, indexes.commit_index.as_path())?.is_empty() {
366 return Ok(Prepared::Ready(by_git(capture, Vec::new(), None, Vec::new(), invocation.arguments.to_vec(), scratch)));
367 }
368 let candidate_paths: Vec<Vec<u8>> =
369 list_changed_index_paths(context, cwd, indexes.commit_index.as_path(), base.as_str(), indexes.object_directory.as_path())?;
370 if candidate_paths.is_empty() && !inputs.allow_empty && !amends {
372 if retry.is_some() {
373 return Ok(Prepared::Ended(already_committed_result(reference.trim_start_matches("refs/heads/"))));
375 }
376 return Ok(Prepared::Ready(by_git(capture, Vec::new(), None, Vec::new(), invocation.arguments.to_vec(), scratch)));
377 }
378 let mut private: ShippedChecks<PrivateIndexFacts<&mut F>> = private_checks(
379 checks,
380 indexes.commit_index.as_path(),
381 indexes.object_directory.as_path(),
382 staged_at_base(base_object(&capture)?, candidate_paths.as_slice()),
383 );
384 let (pass, changed_passes, changed_paths): (PassResult, u64, Vec<Vec<u8>>) = if selection.read_only {
386 private.can_apply_patches = false;
387 (run_policy_pass(request, &mut private), 0, Vec::new())
388 } else {
389 let outcome: CommitConvergence = converge_commit_policies(CommitConvergenceInput {
390 request,
391 checks: &mut private,
392 stage: PrivateStage { context, cwd, index: indexes.commit_index.as_path(), object_directory: Some(indexes.object_directory.as_path()) },
393 });
394 let passes: u64 = settled_passes(&outcome.convergence);
395 (outcome.pass, passes, outcome.changed_paths)
396 };
397 let code: i32 = pass_exit_code(pass.events.as_slice(), pass.end);
398 if code != 0 {
399 return Ok(Prepared::Ended(TransactionResult { events: pass.events, exit_code: code, landed_oid: None, uses_lfs: false }));
400 }
401 let repository_root: &Path = capture.layout.repository_root.as_path();
402 let install = private.content.install_changes();
403 let records: Vec<AddedPath> = selected_worktree_records(context, cwd, repository_root, indexes.commit_index.as_path(), install.as_slice())?;
404 let eligible: bool = !install.is_empty() && !region.allow_empty && !amends && !selection.concludes_sequencer;
406 if eligible && !unborn {
407 let intended: String = write_private_tree(context, cwd, indexes.commit_index.as_path(), indexes.object_directory.as_path())?;
408 let mut tree_request = indexed_request(cwd, &["rev-parse", "--verify", format!("{base}^{{tree}}").as_str()], None, None);
409 tree_request.without_prefix = true;
410 let base_tree: String = String::from_utf8_lossy(run_git_checked(context, &tree_request)?.stdout.as_slice()).trim().to_string();
411 if base_tree == intended {
412 return normalized(invocation, &capture, base.as_str(), &candidate_paths, records.as_slice(), &(pass.events, changed_passes, changed_paths));
413 }
414 }
415 let events: Vec<PolicyEvent> = events_with_changed_summary(pass.events.clone(), changed_passes, changed_paths.as_slice());
416 let changes: Option<Vec<Change>> = if config_native || unborn || detached || state_present { None } else { changes_of(context, cwd, &indexes, base.as_str())? };
417 let forced: bool = config_native || unborn || detached || state_present || changes.is_none();
418 let mut ready: Box<Ready> = by_git(capture, events, changes, records, pass.arguments.clone(), scratch);
419 ready.needs_native = forced;
420 return Ok(Prepared::Ready(ready));
421}
422
423fn normalized(
426 invocation: &CommitInvocation<'_>,
427 capture: &InvocationCapture,
428 base: &str,
429 committed: &[Vec<u8>],
430 records: &[AddedPath],
431 settled: &(Vec<PolicyEvent>, u64, Vec<Vec<u8>>),
432) -> Result<Prepared, TransactionStop> {
433 let (events, passes, changed_paths) = settled;
434 update_index(invocation.context, capture.layout.repository_root.as_path(), base, IndexPaths::Named(committed))
435 .map_err(|code| { return failed(format!("the index could not be reconciled with the corrected files: {}", code.word())); })?;
436 let completion = install_added_worktree_files(
437 invocation.context,
438 invocation.cwd,
439 capture.layout.repository_root.as_path(),
440 records,
441 Some(capture.layout.object_directory.as_path()),
442 )
443 .map_err(|error| { return TransactionStop::Message(error.0); })?;
444 let mut summary: Vec<PolicyEvent> = events_with_summary(events.clone(), *passes, changed_paths.as_slice());
445 summary.push(PolicyEvent::CoreFinding {
446 core_id: NO_CHANGE_CORE_ID,
447 code: NO_CHANGE_CODE,
448 message: no_change_message(completion.conflicted.as_slice()),
449 });
450 return Ok(Prepared::Ended(TransactionResult { events: summary, exit_code: 1, landed_oid: None, uses_lfs: false }));
451}