1use 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::path_bytes::{os_from_path_bytes, path_message_text};
39use super::markdown_linter::executable_linter;
40use super::policy_checks::{ShippedChecks, shipped_checks};
41use super::policy_forbidden_strings::default_scanner_settings;
42use super::policy_markdown::markdown_state;
43use super::policy_content::LifecycleContent;
44use super::policy_convergence::Convergence;
45use super::policy_engine::{StageRequest, pass_exit_code};
46use super::policy_events::PolicyEvent;
47use super::policy_pass::{PassResult, run_policy_pass};
48use super::policy_trigger::Trigger;
49use super::private_storage::{sync_directory, write_private_file};
50use super::repository_facts::RepositoryFacts;
51use super::transaction_git::{GitContext, GitFailure};
52use super::transaction_journal::{Base, Conclusion, PREPARING_FILENAME, PreparingRecord, TransactionMode};
53use super::transaction_journal_encode::encode_preparing;
54use super::transaction_phase::PhaseMarkerError;
55use super::wrapper_controls::Controls;
56use std::ffi::OsString;
57use std::path::{Path, PathBuf};
58
59const TAG: &str = "runCommitTransaction";
61
62pub struct CommitInvocation<'a> {
73 pub context: &'a GitContext,
75 pub environment: &'a [(OsString, OsString)],
77 pub cwd: &'a Path,
79 pub arguments: &'a [OsString],
81 pub subcommand_index: usize,
83 pub region: &'a CommitRegion,
85 pub config: &'a CliGitConfig,
87 pub controls: &'a Controls,
89 pub inherited_lease_valid: bool,
91 pub executable: &'a Path,
93}
94
95#[derive(Debug)]
106pub enum TransactionStop {
107 NativeExit(i32),
109 Message(String),
111 Failed {
113 code: EngineFailureCode,
115 message: String,
117 },
118}
119
120impl From<GitFailure> for TransactionStop {
121 fn from(failure: GitFailure) -> Self {
123 return TransactionStop::Message(failure.0);
124 }
125}
126
127impl From<WorkspaceError> for TransactionStop {
128 fn from(failure: WorkspaceError) -> Self {
130 return failed(failure.to_string());
131 }
132}
133
134impl From<CaptureOrderError> for TransactionStop {
135 fn from(failure: CaptureOrderError) -> Self {
137 return match failure {
138 CaptureOrderError::Git(git) => TransactionStop::Message(git.0),
139 other => failed(other.to_string()),
140 };
141 }
142}
143
144impl From<PhaseMarkerError> for TransactionStop {
145 fn from(failure: PhaseMarkerError) -> Self {
147 return failed(failure.to_string());
148 }
149}
150
151impl From<LandingError> for TransactionStop {
152 fn from(failure: LandingError) -> Self {
154 return match failure {
155 LandingError::Lock(super::index_writer_coordination::CoordinationFailure::UnprovenOwner(text)) => {
156 TransactionStop::Failed {
157 code: EngineFailureCode::IndexLockUnprovenOwner,
158 message: text,
159 }
160 }
161 LandingError::Git(git) => TransactionStop::Message(git.0),
162 other => failed(other.to_string()),
163 };
164 }
165}
166
167pub fn failed(message: String) -> TransactionStop {
177 return TransactionStop::Failed {
178 code: EngineFailureCode::TransactionFailed,
179 message,
180 };
181}
182
183#[derive(Clone, Debug, Eq, PartialEq)]
193pub struct TransactionResult {
194 pub events: Vec<PolicyEvent>,
196 pub exit_code: i32,
198 pub landed_oid: Option<String>,
200}
201
202pub fn transaction_directory(registry_root: &Path, transaction_id: &str) -> PathBuf {
212 return registry_root.join(transaction_id);
213}
214
215pub fn write_journal_record(directory: &Path, filename: &str, text: &str) -> Result<(), TransactionStop> {
225 let path: PathBuf = directory.join(filename);
226 write_private_file(path.as_path(), text.as_bytes())
227 .map_err(|error| { return failed(format!("writing {} failed: {error}", path.display())); })?;
228 sync_directory(directory)
229 .map_err(|error| { return failed(format!("syncing {} failed: {error}", directory.display())); })?;
230 return Ok(());
231}
232
233#[derive(Clone, Debug, Eq, PartialEq)]
243pub struct Selection {
244 pub concludes_sequencer: bool,
246 pub read_only: bool,
248 pub selects_paths: bool,
250 pub staged_all: bool,
252 pub mode: TransactionMode,
254}
255
256pub fn decide_selection(region: &CommitRegion, conclusion: Conclusion) -> Selection {
266 let concludes_sequencer: bool = conclusion != Conclusion::None && conclusion != Conclusion::Amend;
267 let read_only: bool = region.include || region.interactive || region.patch;
268 let selects_paths: bool = !region.pathspecs.is_empty() || region.pathspec_from_file.is_some();
269 let staged_all: bool = region.all && !selects_paths && !read_only && region.only != Some(true);
270 let index_mode: bool = region.only == Some(false)
271 || concludes_sequencer
272 || read_only
273 || region.all
274 || !selects_paths;
275 return Selection {
276 concludes_sequencer,
277 read_only,
278 selects_paths,
279 staged_all,
280 mode: if index_mode { TransactionMode::Index } else { TransactionMode::ExplicitPath },
281 };
282}
283
284fn pathspec_arguments(invocation: &CommitInvocation<'_>) -> Vec<OsString> {
295 let region_arguments: &[OsString] = &invocation.arguments[invocation.subcommand_index + 1..];
296 return invocation
297 .region
298 .pathspecs
299 .iter()
300 .map(|index| { return region_arguments[*index].clone(); })
301 .collect();
302}
303
304fn pathspec_texts(invocation: &CommitInvocation<'_>) -> Vec<String> {
315 let region_arguments: &[OsString] = &invocation.arguments[invocation.subcommand_index + 1..];
316 return invocation
317 .region
318 .pathspecs
319 .iter()
320 .map(|index| { return region_arguments[*index].to_string_lossy().into_owned(); })
321 .collect();
322}
323
324fn pathspec_file_source(invocation: &CommitInvocation<'_>) -> Option<String> {
334 let region_arguments: &[OsString] = &invocation.arguments[invocation.subcommand_index + 1..];
335 let value: OptionValue = invocation.region.pathspec_from_file?;
336 return Some(String::from_utf8_lossy(value_bytes(region_arguments, value)).into_owned());
337}
338
339struct Converged {
349 pass: PassResult,
351 changed_passes: u64,
353 changed_paths: Vec<Vec<u8>>,
355}
356
357pub fn private_checks<'a, F: RepositoryFacts>(
369 checks: &'a mut ShippedChecks<F>,
370 index: &Path,
371 object_directory: &Path,
372 candidates: CandidateRequest,
373) -> ShippedChecks<PrivateIndexFacts<&'a mut F>> {
374 let scanner_settings = std::mem::replace(&mut checks.scanner_settings, default_scanner_settings());
377 let markdown = std::mem::replace(&mut checks.markdown, markdown_state(Box::new(executable_linter(&[]))));
378 let arguments: Vec<OsString> = checks.arguments.clone();
379 let allowed_worktree_dirs: Vec<PathBuf> = checks.allowed_worktree_dirs.clone();
380 let mut rebuilt: ShippedChecks<PrivateIndexFacts<&'a mut F>> = shipped_checks(
381 PrivateIndexFacts::new(&mut checks.facts, index, object_directory),
382 arguments,
383 LifecycleContent::Requested(candidates),
384 allowed_worktree_dirs,
385 );
386 rebuilt.scanner_settings = scanner_settings;
387 rebuilt.markdown = markdown;
388 return rebuilt;
389}
390
391pub fn stages_worktree(staged_all: bool, include: bool) -> bool {
402 return staged_all || include;
403}
404
405pub fn settled_passes(convergence: &Convergence) -> u64 {
415 return match convergence {
416 Convergence::Settled { changed_passes } => *changed_passes as u64,
417 _ => 0,
418 };
419}
420
421pub fn staged_at_base(base: Option<ObjectId>, paths: &[Vec<u8>]) -> CandidateRequest {
432 return CandidateRequest::StagedAtBase {
433 base,
434 pathspecs: paths
435 .iter()
436 .map(|path| {
437 let mut pathspec: Vec<u8> = b":(top,literal)".to_vec();
438 pathspec.extend_from_slice(path.as_slice());
439 return os_from_path_bytes(pathspec.as_slice());
440 })
441 .collect(),
442 };
443}
444
445pub fn base_object(capture: &InvocationCapture) -> Result<Option<ObjectId>, TransactionStop> {
455 return match &capture.base {
456 Base::Unborn => Ok(None),
457 Base::Commit(oid) => match parse_object_id(oid.as_bytes()) {
458 Some(found) => Ok(Some(found)),
459 None => Err(failed(format!("the preparation base {oid} is not an object name"))),
460 },
461 };
462}
463
464fn converge<G: RepositoryFacts>(
474 request: &StageRequest,
475 checks: &mut ShippedChecks<PrivateIndexFacts<G>>,
476 context: &GitContext,
477 cwd: &Path,
478 workspace: &CommitWorkspace,
479 read_only: bool,
480) -> Converged {
481 if read_only {
482 checks.can_apply_patches = false;
483 let pass: PassResult = run_policy_pass(request, checks);
484 return Converged {
485 pass,
486 changed_passes: 0,
487 changed_paths: Vec::new(),
488 };
489 }
490 let outcome: CommitConvergence = converge_commit_policies(CommitConvergenceInput {
491 request,
492 checks,
493 stage: super::private_index_stage::PrivateStage {
494 context,
495 cwd,
496 index: workspace.landing.commit_index.as_path(),
497 object_directory: Some(workspace.object_directory.as_path()),
498 },
499 });
500 let changed_passes: u64 = settled_passes(&outcome.convergence);
501 return Converged {
502 pass: outcome.pass,
503 changed_passes,
504 changed_paths: outcome.changed_paths,
505 };
506}
507
508pub fn run_commit_transaction<F: RepositoryFacts>(
519 invocation: &CommitInvocation<'_>,
520 checks: &mut ShippedChecks<F>,
521 request: &StageRequest,
522) -> Result<TransactionResult, TransactionStop> {
523 let context: &GitContext = invocation.context;
524 let cwd: &Path = invocation.cwd;
525 let layout: InvocationLayout = capture_invocation_layout(context, cwd, invocation.region.amend)?;
526 let selection: Selection = decide_selection(invocation.region, layout.conclusion);
527 let mut workspace: CommitWorkspace = create_commit_workspace(&layout)?;
528 let next_sequence_before_base: i64 = read_next_capture_sequence(layout.git_dir.as_path())
531 .map_err(|error| { return failed(error.0); })?;
532 let capture: InvocationCapture = capture_invocation_base(context, cwd, layout)?;
533 let base: String = base_revision(&capture);
534 let pathspecs: Vec<String> = pathspec_texts(invocation);
535 let pathspec_arguments: Vec<OsString> = pathspec_arguments(invocation);
536 write_journal_record(
537 workspace.landing.directory.as_path(),
538 PREPARING_FILENAME,
539 encode_preparing(&PreparingRecord {
540 transaction_id: workspace.landing.transaction_id.clone(),
541 mode: selection.mode,
542 base: capture.base.clone(),
543 symbolic_head: capture.layout.symbolic_head.clone(),
544 target_ref: capture.layout.target_ref.clone(),
545 conclusion: capture.layout.conclusion,
546 repository_root: capture.layout.repository_root.display().to_string(),
547 git_dir: capture.layout.git_dir.display().to_string(),
548 common_dir: capture.layout.common_dir.display().to_string(),
549 real_index_path: capture.layout.real_index_path.display().to_string(),
550 object_directory: capture.layout.object_directory.display().to_string(),
551 ref_format: capture.layout.ref_format,
552 empty_tree_oid: capture.layout.empty_tree_oid.clone(),
553 shadow_path: workspace.landing.shadow.display().to_string(),
554 selected_pathspecs: pathspec_arguments
555 .iter()
556 .map(|pathspec| { return pathspec.as_encoded_bytes().to_vec(); })
557 .collect(),
558 invoked_at: capture.layout.invoked_at.clone(),
559 })
560 .as_str(),
561 )?;
562 super::shadow_create::create_shadow_repository(
565 context,
566 cwd,
567 &capture,
568 workspace.landing.transaction_id.as_str(),
569 workspace.landing.directory.as_path(),
570 )?;
571 let pathspec_file: Option<PathBuf> = materialize_pathspec_file(
572 workspace.landing.directory.as_path(),
573 cwd,
574 pathspec_file_source(invocation).as_deref(),
575 &mut std::io::stdin(),
576 )
577 .map_err(|error| { return failed(format!("reading the pathspec file failed: {error}")); })?;
578 let indexes: CaptureIndexes = CaptureIndexes {
579 captured_index: workspace.landing.captured_index.clone(),
580 commit_index: workspace.landing.commit_index.clone(),
581 object_directory: workspace.object_directory.clone(),
582 };
583 let index_selection: IndexSelection<'_> = IndexSelection {
584 mode: selection.mode,
585 pathspecs: pathspec_arguments.as_slice(),
586 pathspec_file: pathspec_file.as_deref(),
587 pathspec_file_nul: invocation.region.pathspec_file_nul,
588 stage_into_index: invocation.region.include,
589 stage_tracked_changes: selection.staged_all,
590 base_revision: base.as_str(),
591 };
592 capture_commit_content(
593 context,
594 cwd,
595 capture.layout.git_dir.as_path(),
596 workspace.landing.directory.as_path(),
597 &indexes,
598 selection.mode,
599 stages_worktree(selection.staged_all, invocation.region.include),
600 base.as_str(),
601 next_sequence_before_base,
602 || {
603 return initialize_commit_index(
604 context,
605 cwd,
606 capture.layout.real_index_path.as_path(),
607 &indexes,
608 &index_selection,
609 )
610 .map_err(CaptureOrderError::from);
611 },
612 )?;
613 if invocation.region.interactive || invocation.region.patch {
614 prepare_interactive_selection(
615 context,
616 cwd,
617 workspace.landing.commit_index.as_path(),
618 workspace.object_directory.as_path(),
619 invocation.region.patch,
620 &index_selection,
621 )?;
622 }
623 let unmerged: Vec<Vec<u8>> =
624 list_unmerged_index_paths(context, cwd, workspace.landing.commit_index.as_path())?;
625 if !unmerged.is_empty() {
626 return Ok(TransactionResult {
627 events: vec![PolicyEvent::EngineFailure {
628 code: EngineFailureCode::ContentUnavailable,
629 message: format!(
630 "Automatic commit fixes do not support unmerged index paths: {}",
631 unmerged
632 .iter()
633 .map(|path| { return path_message_text(path.as_slice()); })
634 .collect::<Vec<String>>()
635 .join(", ")
636 ),
637 trigger: Some(Trigger::PreForward),
638 policy: None,
639 path: None,
640 }],
641 exit_code: 2,
642 landed_oid: None,
643 });
644 }
645 let candidate_paths: Vec<Vec<u8>> = list_changed_index_paths(
646 context,
647 cwd,
648 workspace.landing.commit_index.as_path(),
649 base.as_str(),
650 workspace.object_directory.as_path(),
651 )?;
652 let mut private: ShippedChecks<PrivateIndexFacts<&mut F>> = private_checks(
653 checks,
654 workspace.landing.commit_index.as_path(),
655 workspace.object_directory.as_path(),
656 staged_at_base(base_object(&capture)?, candidate_paths.as_slice()),
657 );
658 let converged: Converged = converge(request, &mut private, context, cwd, &workspace, selection.read_only);
659 let code: i32 = pass_exit_code(converged.pass.events.as_slice(), converged.pass.end);
660 if code != 0 {
661 debug(TAG, "policies blocked the commit before preparation");
662 return Ok(TransactionResult {
663 events: converged.pass.events,
664 exit_code: code,
665 landed_oid: None,
666 });
667 }
668 return conclude_commit_transaction(ConclusionInput {
669 invocation,
670 workspace: &mut workspace,
671 capture: &capture,
672 selection: &selection,
673 request,
674 checks: &mut private,
675 pass: converged.pass,
676 changed_passes: converged.changed_passes,
677 changed_paths: converged.changed_paths,
678 committed_paths: candidate_paths,
679 pathspecs: pathspecs.as_slice(),
680 });
681}
682
683#[cfg(test)]
685#[path = "commit_transaction_tests.rs"]
686mod tests;