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::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
62const TAG: &str = "runCommitTransaction";
64
65pub struct CommitInvocation<'a> {
76 pub context: &'a GitContext,
78 pub environment: &'a [(OsString, OsString)],
80 pub cwd: &'a Path,
82 pub arguments: &'a [OsString],
84 pub subcommand_index: usize,
86 pub region: &'a CommitRegion,
88 pub config: &'a CliGitConfig,
90 pub controls: &'a Controls,
92 pub inherited_lease_valid: bool,
94 pub executable: &'a Path,
96}
97
98#[derive(Debug)]
109pub enum TransactionStop {
110 NativeExit(i32),
112 Message(String),
114 Failed {
116 code: EngineFailureCode,
118 message: String,
120 },
121}
122
123impl From<GitFailure> for TransactionStop {
124 fn from(failure: GitFailure) -> Self {
126 return TransactionStop::Message(failure.0);
127 }
128}
129
130impl From<WorkspaceError> for TransactionStop {
131 fn from(failure: WorkspaceError) -> Self {
133 return failed(failure.to_string());
134 }
135}
136
137impl From<CaptureOrderError> for TransactionStop {
138 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 fn from(failure: PhaseMarkerError) -> Self {
150 return failed(failure.to_string());
151 }
152}
153
154impl From<LandingError> for TransactionStop {
155 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
170pub fn failed(message: String) -> TransactionStop {
180 return TransactionStop::Failed {
181 code: EngineFailureCode::TransactionFailed,
182 message,
183 };
184}
185
186#[derive(Clone, Debug, Eq, PartialEq)]
196pub struct TransactionResult {
197 pub events: Vec<PolicyEvent>,
199 pub exit_code: i32,
201 pub landed_oid: Option<String>,
203}
204
205pub fn transaction_directory(registry_root: &Path, transaction_id: &str) -> PathBuf {
215 return registry_root.join(transaction_id);
216}
217
218pub 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#[derive(Clone, Debug, Eq, PartialEq)]
246pub struct Selection {
247 pub concludes_sequencer: bool,
249 pub read_only: bool,
251 pub selects_paths: bool,
253 pub staged_all: bool,
255 pub mode: TransactionMode,
257}
258
259pub 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
287fn 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
307fn 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
327fn 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
342struct Converged {
352 pass: PassResult,
354 changed_passes: u64,
356 changed_paths: Vec<Vec<u8>>,
358}
359
360pub 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 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
394pub fn stages_worktree(staged_all: bool, include: bool) -> bool {
405 return staged_all || include;
406}
407
408pub fn settled_passes(convergence: &Convergence) -> u64 {
418 return match convergence {
419 Convergence::Settled { changed_passes } => *changed_passes as u64,
420 _ => 0,
421 };
422}
423
424pub 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
448pub 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
467fn 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
511pub 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 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 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 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#[cfg(test)]
711#[path = "commit_transaction_tests.rs"]
712mod tests;