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_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::traditional_hook_warning::warn_traditional_hooks;
57use super::transaction_journal_encode::encode_preparing;
58use super::transaction_phase::PhaseMarkerError;
59use super::wrapper_controls::Controls;
60use std::ffi::OsString;
61use std::path::{Path, PathBuf};
62
63const TAG: &str = "runCommitTransaction";
65
66pub struct CommitInvocation<'a> {
77 pub context: &'a GitContext,
79 pub environment: &'a [(OsString, OsString)],
81 pub cwd: &'a Path,
83 pub arguments: &'a [OsString],
85 pub subcommand_index: usize,
87 pub region: &'a CommitRegion,
89 pub config: &'a CliGitConfig,
91 pub controls: &'a Controls,
93 pub inherited_lease_valid: bool,
95 pub executable: &'a Path,
97}
98
99#[derive(Debug)]
110pub enum TransactionStop {
111 NativeExit(i32),
113 Message(String),
115 Failed {
117 code: EngineFailureCode,
119 message: String,
121 },
122}
123
124impl From<GitFailure> for TransactionStop {
125 fn from(failure: GitFailure) -> Self {
127 return TransactionStop::Message(failure.0);
128 }
129}
130
131impl From<WorkspaceError> for TransactionStop {
132 fn from(failure: WorkspaceError) -> Self {
134 return failed(failure.to_string());
135 }
136}
137
138impl From<CaptureOrderError> for TransactionStop {
139 fn from(failure: CaptureOrderError) -> Self {
141 return match failure {
142 CaptureOrderError::Git(git) => TransactionStop::Message(git.0),
143 other => failed(other.to_string()),
144 };
145 }
146}
147
148impl From<PhaseMarkerError> for TransactionStop {
149 fn from(failure: PhaseMarkerError) -> Self {
151 return failed(failure.to_string());
152 }
153}
154
155impl From<LandingError> for TransactionStop {
156 fn from(failure: LandingError) -> Self {
158 return match failure {
159 LandingError::Lock(super::index_writer_coordination::CoordinationFailure::UnprovenOwner(text)) => {
160 TransactionStop::Failed {
161 code: EngineFailureCode::IndexLockUnprovenOwner,
162 message: text,
163 }
164 }
165 LandingError::Git(git) => TransactionStop::Message(git.0),
166 other => failed(other.to_string()),
167 };
168 }
169}
170
171pub fn failed(message: String) -> TransactionStop {
181 return TransactionStop::Failed {
182 code: EngineFailureCode::TransactionFailed,
183 message,
184 };
185}
186
187#[derive(Clone, Debug, Eq, PartialEq)]
197pub struct TransactionResult {
198 pub events: Vec<PolicyEvent>,
200 pub exit_code: i32,
202 pub landed_oid: Option<String>,
204 pub uses_lfs: bool,
207}
208
209pub fn transaction_directory(registry_root: &Path, transaction_id: &str) -> PathBuf {
219 return registry_root.join(transaction_id);
220}
221
222pub fn write_journal_record(directory: &Path, filename: &str, text: &str) -> Result<(), TransactionStop> {
232 let path: PathBuf = directory.join(filename);
233 write_private_file(path.as_path(), text.as_bytes())
234 .map_err(|error| { return failed(format!("writing {} failed: {error}", path.display())); })?;
235 sync_directory(directory)
236 .map_err(|error| { return failed(format!("syncing {} failed: {error}", directory.display())); })?;
237 return Ok(());
238}
239
240#[derive(Clone, Debug, Eq, PartialEq)]
250pub struct Selection {
251 pub concludes_sequencer: bool,
253 pub read_only: bool,
255 pub selects_paths: bool,
257 pub staged_all: bool,
259 pub mode: TransactionMode,
261}
262
263pub fn decide_selection(region: &CommitRegion, conclusion: Conclusion) -> Selection {
273 let concludes_sequencer: bool = conclusion != Conclusion::None && conclusion != Conclusion::Amend;
274 let read_only: bool = region.include || region.interactive || region.patch;
275 let selects_paths: bool = !region.pathspecs.is_empty() || region.pathspec_from_file.is_some();
276 let staged_all: bool = region.all && !selects_paths && !read_only && region.only != Some(true);
277 let index_mode: bool = region.only == Some(false)
278 || concludes_sequencer
279 || read_only
280 || region.all
281 || !selects_paths;
282 return Selection {
283 concludes_sequencer,
284 read_only,
285 selects_paths,
286 staged_all,
287 mode: if index_mode { TransactionMode::Index } else { TransactionMode::ExplicitPath },
288 };
289}
290
291fn pathspec_arguments(invocation: &CommitInvocation<'_>) -> Vec<OsString> {
302 let region_arguments: &[OsString] = &invocation.arguments[invocation.subcommand_index + 1..];
303 return invocation
304 .region
305 .pathspecs
306 .iter()
307 .map(|index| { return region_arguments[*index].clone(); })
308 .collect();
309}
310
311fn pathspec_texts(invocation: &CommitInvocation<'_>) -> Vec<String> {
322 let region_arguments: &[OsString] = &invocation.arguments[invocation.subcommand_index + 1..];
323 return invocation
324 .region
325 .pathspecs
326 .iter()
327 .map(|index| { return region_arguments[*index].to_string_lossy().into_owned(); })
328 .collect();
329}
330
331fn pathspec_file_source(invocation: &CommitInvocation<'_>) -> Option<String> {
341 let region_arguments: &[OsString] = &invocation.arguments[invocation.subcommand_index + 1..];
342 let value: OptionValue = invocation.region.pathspec_from_file?;
343 return Some(String::from_utf8_lossy(value_bytes(region_arguments, value)).into_owned());
344}
345
346struct Converged {
356 pass: PassResult,
358 changed_passes: u64,
360 changed_paths: Vec<Vec<u8>>,
362}
363
364pub fn private_checks<'a, F: RepositoryFacts>(
376 checks: &'a mut ShippedChecks<F>,
377 index: &Path,
378 object_directory: &Path,
379 candidates: CandidateRequest,
380) -> ShippedChecks<PrivateIndexFacts<&'a mut F>> {
381 let scanner_settings = std::mem::replace(&mut checks.scanner_settings, default_scanner_settings());
384 let markdown = std::mem::replace(&mut checks.markdown, markdown_state(Box::new(executable_linter(&[]))));
385 let arguments: Vec<OsString> = checks.arguments.clone();
386 let allowed_worktree_dirs: Vec<PathBuf> = checks.allowed_worktree_dirs.clone();
387 let mut rebuilt: ShippedChecks<PrivateIndexFacts<&'a mut F>> = shipped_checks(
388 PrivateIndexFacts::new(&mut checks.facts, index, object_directory),
389 arguments,
390 LifecycleContent::Requested(candidates),
391 allowed_worktree_dirs,
392 );
393 rebuilt.scanner_settings = scanner_settings;
394 rebuilt.markdown = markdown;
395 return rebuilt;
396}
397
398pub fn stages_worktree(staged_all: bool, include: bool) -> bool {
409 return staged_all || include;
410}
411
412pub fn settled_passes(convergence: &Convergence) -> u64 {
422 return match convergence {
423 Convergence::Settled { changed_passes } => *changed_passes as u64,
424 _ => 0,
425 };
426}
427
428pub fn staged_at_base(base: Option<ObjectId>, paths: &[Vec<u8>]) -> CandidateRequest {
439 return CandidateRequest::StagedAtBase {
440 base,
441 pathspecs: paths
442 .iter()
443 .map(|path| {
444 let mut pathspec: Vec<u8> = b":(top,literal)".to_vec();
445 pathspec.extend_from_slice(path.as_slice());
446 return os_from_path_bytes(pathspec.as_slice());
447 })
448 .collect(),
449 };
450}
451
452pub fn base_object(capture: &InvocationCapture) -> Result<Option<ObjectId>, TransactionStop> {
462 return match &capture.base {
463 Base::Unborn => Ok(None),
464 Base::Commit(oid) => match parse_object_id(oid.as_bytes()) {
465 Some(found) => Ok(Some(found)),
466 None => Err(failed(format!("the preparation base {oid} is not an object name"))),
467 },
468 };
469}
470
471fn converge<G: RepositoryFacts>(
481 request: &StageRequest,
482 checks: &mut ShippedChecks<PrivateIndexFacts<G>>,
483 context: &GitContext,
484 cwd: &Path,
485 workspace: &CommitWorkspace,
486 read_only: bool,
487) -> Converged {
488 if read_only {
489 checks.can_apply_patches = false;
490 let pass: PassResult = run_policy_pass(request, checks);
491 return Converged {
492 pass,
493 changed_passes: 0,
494 changed_paths: Vec::new(),
495 };
496 }
497 let outcome: CommitConvergence = converge_commit_policies(CommitConvergenceInput {
498 request,
499 checks,
500 stage: super::private_index_stage::PrivateStage {
501 context,
502 cwd,
503 index: workspace.landing.commit_index.as_path(),
504 object_directory: Some(workspace.object_directory.as_path()),
505 },
506 });
507 let changed_passes: u64 = settled_passes(&outcome.convergence);
508 return Converged {
509 pass: outcome.pass,
510 changed_passes,
511 changed_paths: outcome.changed_paths,
512 };
513}
514
515pub fn run_commit_transaction<F: RepositoryFacts>(
526 invocation: &CommitInvocation<'_>,
527 checks: &mut ShippedChecks<F>,
528 request: &StageRequest,
529) -> Result<TransactionResult, TransactionStop> {
530 let context: &GitContext = invocation.context;
531 let cwd: &Path = invocation.cwd;
532 let layout: InvocationLayout = capture_invocation_layout(context, cwd, invocation.region.amend)?;
533 warn_traditional_hooks(layout.config_snapshot.as_ref(), &mut std::io::stderr());
535 let selection: Selection = decide_selection(invocation.region, layout.conclusion);
536 let mut workspace: CommitWorkspace = create_commit_workspace(&layout)?;
537 let next_sequence_before_base: i64 = read_next_capture_sequence(layout.git_dir.as_path())
540 .map_err(|error| { return failed(error.0); })?;
541 let capture: InvocationCapture = capture_invocation_base(context, cwd, layout)?;
542 let base: String = base_revision(&capture);
543 let pathspecs: Vec<String> = pathspec_texts(invocation);
544 let pathspec_arguments: Vec<OsString> = pathspec_arguments(invocation);
545 if selection.mode == TransactionMode::ExplicitPath {
546 let present: Vec<Vec<u8>> = staged_deletions_present_on_disk(
548 context,
549 cwd,
550 capture.layout.repository_root.as_path(),
551 capture.layout.real_index_path.as_path(),
552 base.as_str(),
553 pathspec_arguments.as_slice(),
554 )?;
555 if !present.is_empty() {
556 debug(TAG, "an explicit-path commit selects a staged deletion whose path is on disk");
557 return Ok(TransactionResult {
558 events: vec![PolicyEvent::CoreFinding {
559 core_id: STAGED_DELETION_CORE_ID,
560 code: STAGED_DELETION_CODE,
561 message: staged_deletion_message(present.as_slice()),
562 }],
563 exit_code: 1,
564 landed_oid: None,
565 uses_lfs: false,
566 });
567 }
568 }
569 write_journal_record(
570 workspace.landing.directory.as_path(),
571 PREPARING_FILENAME,
572 encode_preparing(&PreparingRecord {
573 transaction_id: workspace.landing.transaction_id.clone(),
574 mode: selection.mode,
575 base: capture.base.clone(),
576 symbolic_head: capture.layout.symbolic_head.clone(),
577 target_ref: capture.layout.target_ref.clone(),
578 conclusion: capture.layout.conclusion,
579 repository_root: capture.layout.repository_root.display().to_string(),
580 git_dir: capture.layout.git_dir.display().to_string(),
581 common_dir: capture.layout.common_dir.display().to_string(),
582 real_index_path: capture.layout.real_index_path.display().to_string(),
583 object_directory: capture.layout.object_directory.display().to_string(),
584 ref_format: capture.layout.ref_format,
585 empty_tree_oid: capture.layout.empty_tree_oid.clone(),
586 shadow_path: workspace.landing.shadow.display().to_string(),
587 selected_pathspecs: pathspec_arguments
588 .iter()
589 .map(|pathspec| { return pathspec.as_encoded_bytes().to_vec(); })
590 .collect(),
591 invoked_at: capture.layout.invoked_at.clone(),
592 })
593 .as_str(),
594 )?;
595 super::shadow_create::create_shadow_repository(
598 context,
599 cwd,
600 &capture,
601 workspace.landing.transaction_id.as_str(),
602 workspace.landing.directory.as_path(),
603 )?;
604 let pathspec_file: Option<PathBuf> = materialize_pathspec_file(
605 workspace.landing.directory.as_path(),
606 cwd,
607 pathspec_file_source(invocation).as_deref(),
608 &mut std::io::stdin(),
609 )
610 .map_err(|error| { return failed(format!("reading the pathspec file failed: {error}")); })?;
611 let indexes: CaptureIndexes = CaptureIndexes {
612 captured_index: workspace.landing.captured_index.clone(),
613 commit_index: workspace.landing.commit_index.clone(),
614 object_directory: workspace.object_directory.clone(),
615 };
616 let index_selection: IndexSelection<'_> = IndexSelection {
617 mode: selection.mode,
618 pathspecs: pathspec_arguments.as_slice(),
619 pathspec_file: pathspec_file.as_deref(),
620 pathspec_file_nul: invocation.region.pathspec_file_nul,
621 stage_into_index: invocation.region.include,
622 stage_tracked_changes: selection.staged_all,
623 base_revision: base.as_str(),
624 };
625 capture_commit_content(
626 context,
627 cwd,
628 capture.layout.git_dir.as_path(),
629 workspace.landing.directory.as_path(),
630 &indexes,
631 selection.mode,
632 stages_worktree(selection.staged_all, invocation.region.include),
633 base.as_str(),
634 next_sequence_before_base,
635 || {
636 return initialize_commit_index(
637 context,
638 cwd,
639 capture.layout.real_index_path.as_path(),
640 &indexes,
641 &index_selection,
642 )
643 .map_err(CaptureOrderError::from);
644 },
645 )?;
646 if invocation.region.interactive || invocation.region.patch {
647 prepare_interactive_selection(
648 context,
649 cwd,
650 workspace.landing.commit_index.as_path(),
651 workspace.object_directory.as_path(),
652 invocation.region.patch,
653 &index_selection,
654 )?;
655 }
656 let unmerged: Vec<Vec<u8>> =
657 list_unmerged_index_paths(context, cwd, workspace.landing.commit_index.as_path())?;
658 if !unmerged.is_empty() {
659 return Ok(TransactionResult {
660 events: vec![PolicyEvent::EngineFailure {
661 code: EngineFailureCode::ContentUnavailable,
662 message: format!(
663 "Automatic commit fixes do not support unmerged index paths: {}",
664 unmerged
665 .iter()
666 .map(|path| { return path_message_text(path.as_slice()); })
667 .collect::<Vec<String>>()
668 .join(", ")
669 ),
670 trigger: Some(Trigger::PreForward),
671 policy: None,
672 path: None,
673 }],
674 exit_code: 2,
675 landed_oid: None,
676 uses_lfs: false,
677 });
678 }
679 let candidate_paths: Vec<Vec<u8>> = list_changed_index_paths(
680 context,
681 cwd,
682 workspace.landing.commit_index.as_path(),
683 base.as_str(),
684 workspace.object_directory.as_path(),
685 )?;
686 let mut private: ShippedChecks<PrivateIndexFacts<&mut F>> = private_checks(
687 checks,
688 workspace.landing.commit_index.as_path(),
689 workspace.object_directory.as_path(),
690 staged_at_base(base_object(&capture)?, candidate_paths.as_slice()),
691 );
692 let converged: Converged = converge(request, &mut private, context, cwd, &workspace, selection.read_only);
693 let code: i32 = pass_exit_code(converged.pass.events.as_slice(), converged.pass.end);
694 if code != 0 {
695 debug(TAG, "policies blocked the commit before preparation");
696 return Ok(TransactionResult {
697 events: converged.pass.events,
698 exit_code: code,
699 landed_oid: None,
700 uses_lfs: false,
701 });
702 }
703 return conclude_commit_transaction(ConclusionInput {
704 invocation,
705 workspace: &mut workspace,
706 capture: &capture,
707 selection: &selection,
708 request,
709 checks: &mut private,
710 pass: converged.pass,
711 changed_passes: converged.changed_passes,
712 changed_paths: converged.changed_paths,
713 committed_paths: candidate_paths,
714 pathspecs: pathspecs.as_slice(),
715 });
716}
717
718#[cfg(test)]
720#[path = "commit_transaction_tests.rs"]
721mod tests;