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::markdown_linter::executable_linter;
39use super::policy_checks::{ShippedChecks, shipped_checks};
40use super::policy_forbidden_strings::default_scanner_settings;
41use super::policy_markdown::markdown_state;
42use super::policy_content::LifecycleContent;
43use super::policy_convergence::Convergence;
44use super::policy_engine::{StageRequest, pass_exit_code};
45use super::policy_events::PolicyEvent;
46use super::policy_pass::{PassResult, run_policy_pass};
47use super::policy_trigger::Trigger;
48use super::private_storage::{sync_directory, write_private_file};
49use super::repository_facts::RepositoryFacts;
50use super::transaction_git::{GitContext, GitFailure};
51use super::transaction_journal::{Base, Conclusion, PREPARING_FILENAME, PreparingRecord, TransactionMode};
52use super::transaction_journal_encode::encode_preparing;
53use super::transaction_phase::PhaseMarkerError;
54use super::wrapper_controls::Controls;
55use std::ffi::OsString;
56use std::path::{Path, PathBuf};
57
58const TAG: &str = "runCommitTransaction";
60
61pub struct CommitInvocation<'a> {
72 pub context: &'a GitContext,
74 pub environment: &'a [(OsString, OsString)],
76 pub cwd: &'a Path,
78 pub arguments: &'a [OsString],
80 pub subcommand_index: usize,
82 pub region: &'a CommitRegion,
84 pub config: &'a CliGitConfig,
86 pub controls: &'a Controls,
88 pub inherited_lease_valid: bool,
90 pub executable: &'a Path,
92}
93
94#[derive(Debug)]
105pub enum TransactionStop {
106 NativeExit(i32),
108 Message(String),
110 Failed {
112 code: EngineFailureCode,
114 message: String,
116 },
117}
118
119impl From<GitFailure> for TransactionStop {
120 fn from(failure: GitFailure) -> Self {
122 return TransactionStop::Message(failure.0);
123 }
124}
125
126impl From<WorkspaceError> for TransactionStop {
127 fn from(failure: WorkspaceError) -> Self {
129 return failed(failure.to_string());
130 }
131}
132
133impl From<CaptureOrderError> for TransactionStop {
134 fn from(failure: CaptureOrderError) -> Self {
136 return match failure {
137 CaptureOrderError::Git(git) => TransactionStop::Message(git.0),
138 other => failed(other.to_string()),
139 };
140 }
141}
142
143impl From<PhaseMarkerError> for TransactionStop {
144 fn from(failure: PhaseMarkerError) -> Self {
146 return failed(failure.to_string());
147 }
148}
149
150impl From<LandingError> for TransactionStop {
151 fn from(failure: LandingError) -> Self {
153 return match failure {
154 LandingError::Lock(super::index_writer_coordination::CoordinationFailure::UnprovenOwner(text)) => {
155 TransactionStop::Failed {
156 code: EngineFailureCode::IndexLockUnprovenOwner,
157 message: text,
158 }
159 }
160 LandingError::Git(git) => TransactionStop::Message(git.0),
161 other => failed(other.to_string()),
162 };
163 }
164}
165
166pub fn failed(message: String) -> TransactionStop {
176 return TransactionStop::Failed {
177 code: EngineFailureCode::TransactionFailed,
178 message,
179 };
180}
181
182#[derive(Clone, Debug, Eq, PartialEq)]
192pub struct TransactionResult {
193 pub events: Vec<PolicyEvent>,
195 pub exit_code: i32,
197 pub landed_oid: Option<String>,
199}
200
201pub fn transaction_directory(registry_root: &Path, transaction_id: &str) -> PathBuf {
211 return registry_root.join(transaction_id);
212}
213
214pub fn write_journal_record(directory: &Path, filename: &str, text: &str) -> Result<(), TransactionStop> {
224 let path: PathBuf = directory.join(filename);
225 write_private_file(path.as_path(), text.as_bytes())
226 .map_err(|error| { return failed(format!("writing {} failed: {error}", path.display())); })?;
227 sync_directory(directory)
228 .map_err(|error| { return failed(format!("syncing {} failed: {error}", directory.display())); })?;
229 return Ok(());
230}
231
232#[derive(Clone, Debug, Eq, PartialEq)]
242pub struct Selection {
243 pub concludes_sequencer: bool,
245 pub read_only: bool,
247 pub selects_paths: bool,
249 pub staged_all: bool,
251 pub mode: TransactionMode,
253}
254
255pub fn decide_selection(region: &CommitRegion, conclusion: Conclusion) -> Selection {
265 let concludes_sequencer: bool = conclusion != Conclusion::None && conclusion != Conclusion::Amend;
266 let read_only: bool = region.include || region.interactive || region.patch;
267 let selects_paths: bool = !region.pathspecs.is_empty() || region.pathspec_from_file.is_some();
268 let staged_all: bool = region.all && !selects_paths && !read_only && region.only != Some(true);
269 let index_mode: bool = region.only == Some(false)
270 || concludes_sequencer
271 || read_only
272 || region.all
273 || !selects_paths;
274 return Selection {
275 concludes_sequencer,
276 read_only,
277 selects_paths,
278 staged_all,
279 mode: if index_mode { TransactionMode::Index } else { TransactionMode::ExplicitPath },
280 };
281}
282
283fn pathspec_texts(invocation: &CommitInvocation<'_>) -> Vec<String> {
293 let region_arguments: &[OsString] = &invocation.arguments[invocation.subcommand_index + 1..];
294 return invocation
295 .region
296 .pathspecs
297 .iter()
298 .map(|index| { return region_arguments[*index].to_string_lossy().into_owned(); })
299 .collect();
300}
301
302fn pathspec_file_source(invocation: &CommitInvocation<'_>) -> Option<String> {
312 let region_arguments: &[OsString] = &invocation.arguments[invocation.subcommand_index + 1..];
313 let value: OptionValue = invocation.region.pathspec_from_file?;
314 return Some(String::from_utf8_lossy(value_bytes(region_arguments, value)).into_owned());
315}
316
317struct Converged {
327 pass: PassResult,
329 changed_passes: u64,
331 changed_paths: Vec<String>,
333}
334
335pub fn private_checks<'a, F: RepositoryFacts>(
347 checks: &'a mut ShippedChecks<F>,
348 index: &Path,
349 object_directory: &Path,
350 candidates: CandidateRequest,
351) -> ShippedChecks<PrivateIndexFacts<&'a mut F>> {
352 let scanner_settings = std::mem::replace(&mut checks.scanner_settings, default_scanner_settings());
355 let markdown = std::mem::replace(&mut checks.markdown, markdown_state(Box::new(executable_linter(&[]))));
356 let arguments: Vec<OsString> = checks.arguments.clone();
357 let allowed_worktree_dirs: Vec<PathBuf> = checks.allowed_worktree_dirs.clone();
358 let mut rebuilt: ShippedChecks<PrivateIndexFacts<&'a mut F>> = shipped_checks(
359 PrivateIndexFacts::new(&mut checks.facts, index, object_directory),
360 arguments,
361 LifecycleContent::Requested(candidates),
362 allowed_worktree_dirs,
363 );
364 rebuilt.scanner_settings = scanner_settings;
365 rebuilt.markdown = markdown;
366 return rebuilt;
367}
368
369pub fn stages_worktree(staged_all: bool, include: bool) -> bool {
380 return staged_all || include;
381}
382
383pub fn settled_passes(convergence: &Convergence) -> u64 {
393 return match convergence {
394 Convergence::Settled { changed_passes } => *changed_passes as u64,
395 _ => 0,
396 };
397}
398
399pub fn staged_at_base(base: Option<ObjectId>, paths: &[String]) -> CandidateRequest {
410 return CandidateRequest::StagedAtBase {
411 base,
412 pathspecs: paths.iter().map(|path| { return OsString::from(format!(":(top,literal){path}")); }).collect(),
413 };
414}
415
416pub fn base_object(capture: &InvocationCapture) -> Result<Option<ObjectId>, TransactionStop> {
426 return match &capture.base {
427 Base::Unborn => Ok(None),
428 Base::Commit(oid) => match parse_object_id(oid.as_bytes()) {
429 Some(found) => Ok(Some(found)),
430 None => Err(failed(format!("the preparation base {oid} is not an object name"))),
431 },
432 };
433}
434
435fn converge<G: RepositoryFacts>(
445 request: &StageRequest,
446 checks: &mut ShippedChecks<PrivateIndexFacts<G>>,
447 context: &GitContext,
448 cwd: &Path,
449 workspace: &CommitWorkspace,
450 read_only: bool,
451) -> Converged {
452 if read_only {
453 checks.can_apply_patches = false;
454 let pass: PassResult = run_policy_pass(request, checks);
455 return Converged {
456 pass,
457 changed_passes: 0,
458 changed_paths: Vec::new(),
459 };
460 }
461 let outcome: CommitConvergence = converge_commit_policies(CommitConvergenceInput {
462 request,
463 checks,
464 stage: super::private_index_stage::PrivateStage {
465 context,
466 cwd,
467 index: workspace.landing.commit_index.as_path(),
468 object_directory: Some(workspace.object_directory.as_path()),
469 },
470 });
471 let changed_passes: u64 = settled_passes(&outcome.convergence);
472 return Converged {
473 pass: outcome.pass,
474 changed_passes,
475 changed_paths: outcome.changed_paths,
476 };
477}
478
479pub fn run_commit_transaction<F: RepositoryFacts>(
490 invocation: &CommitInvocation<'_>,
491 checks: &mut ShippedChecks<F>,
492 request: &StageRequest,
493) -> Result<TransactionResult, TransactionStop> {
494 let context: &GitContext = invocation.context;
495 let cwd: &Path = invocation.cwd;
496 let layout: InvocationLayout = capture_invocation_layout(context, cwd, invocation.region.amend)?;
497 let selection: Selection = decide_selection(invocation.region, layout.conclusion);
498 let mut workspace: CommitWorkspace = create_commit_workspace(&layout)?;
499 let next_sequence_before_base: i64 = read_next_capture_sequence(layout.git_dir.as_path())
502 .map_err(|error| { return failed(error.0); })?;
503 let capture: InvocationCapture = capture_invocation_base(context, cwd, layout)?;
504 let base: String = base_revision(&capture);
505 let pathspecs: Vec<String> = pathspec_texts(invocation);
506 write_journal_record(
507 workspace.landing.directory.as_path(),
508 PREPARING_FILENAME,
509 encode_preparing(&PreparingRecord {
510 transaction_id: workspace.landing.transaction_id.clone(),
511 mode: selection.mode,
512 base: capture.base.clone(),
513 symbolic_head: capture.layout.symbolic_head.clone(),
514 target_ref: capture.layout.target_ref.clone(),
515 conclusion: capture.layout.conclusion,
516 repository_root: capture.layout.repository_root.display().to_string(),
517 git_dir: capture.layout.git_dir.display().to_string(),
518 common_dir: capture.layout.common_dir.display().to_string(),
519 real_index_path: capture.layout.real_index_path.display().to_string(),
520 object_directory: capture.layout.object_directory.display().to_string(),
521 ref_format: capture.layout.ref_format,
522 empty_tree_oid: capture.layout.empty_tree_oid.clone(),
523 shadow_path: workspace.landing.shadow.display().to_string(),
524 selected_pathspecs: pathspecs.clone(),
525 invoked_at: capture.layout.invoked_at.clone(),
526 })
527 .as_str(),
528 )?;
529 super::shadow_create::create_shadow_repository(
532 context,
533 cwd,
534 &capture,
535 workspace.landing.transaction_id.as_str(),
536 workspace.landing.directory.as_path(),
537 )?;
538 let pathspec_file: Option<PathBuf> = materialize_pathspec_file(
539 workspace.landing.directory.as_path(),
540 cwd,
541 pathspec_file_source(invocation).as_deref(),
542 &mut std::io::stdin(),
543 )
544 .map_err(|error| { return failed(format!("reading the pathspec file failed: {error}")); })?;
545 let indexes: CaptureIndexes = CaptureIndexes {
546 captured_index: workspace.landing.captured_index.clone(),
547 commit_index: workspace.landing.commit_index.clone(),
548 object_directory: workspace.object_directory.clone(),
549 };
550 let index_selection: IndexSelection<'_> = IndexSelection {
551 mode: selection.mode,
552 pathspecs: pathspecs.as_slice(),
553 pathspec_file: pathspec_file.as_deref(),
554 pathspec_file_nul: invocation.region.pathspec_file_nul,
555 stage_into_index: invocation.region.include,
556 stage_tracked_changes: selection.staged_all,
557 base_revision: base.as_str(),
558 };
559 capture_commit_content(
560 context,
561 cwd,
562 capture.layout.git_dir.as_path(),
563 workspace.landing.directory.as_path(),
564 &indexes,
565 selection.mode,
566 stages_worktree(selection.staged_all, invocation.region.include),
567 base.as_str(),
568 next_sequence_before_base,
569 || {
570 return initialize_commit_index(
571 context,
572 cwd,
573 capture.layout.real_index_path.as_path(),
574 &indexes,
575 &index_selection,
576 )
577 .map_err(CaptureOrderError::from);
578 },
579 )?;
580 if invocation.region.interactive || invocation.region.patch {
581 prepare_interactive_selection(
582 context,
583 cwd,
584 workspace.landing.commit_index.as_path(),
585 workspace.object_directory.as_path(),
586 invocation.region.patch,
587 &index_selection,
588 )?;
589 }
590 let unmerged: Vec<String> =
591 list_unmerged_index_paths(context, cwd, workspace.landing.commit_index.as_path())?;
592 if !unmerged.is_empty() {
593 return Ok(TransactionResult {
594 events: vec![PolicyEvent::EngineFailure {
595 code: EngineFailureCode::ContentUnavailable,
596 message: format!(
597 "Automatic commit fixes do not support unmerged index paths: {}",
598 unmerged.join(", ")
599 ),
600 trigger: Some(Trigger::PreForward),
601 policy: None,
602 path: None,
603 }],
604 exit_code: 2,
605 landed_oid: None,
606 });
607 }
608 let candidate_paths: Vec<String> = list_changed_index_paths(
609 context,
610 cwd,
611 workspace.landing.commit_index.as_path(),
612 base.as_str(),
613 workspace.object_directory.as_path(),
614 )?;
615 let mut private: ShippedChecks<PrivateIndexFacts<&mut F>> = private_checks(
616 checks,
617 workspace.landing.commit_index.as_path(),
618 workspace.object_directory.as_path(),
619 staged_at_base(base_object(&capture)?, candidate_paths.as_slice()),
620 );
621 let converged: Converged = converge(request, &mut private, context, cwd, &workspace, selection.read_only);
622 let code: i32 = pass_exit_code(converged.pass.events.as_slice(), converged.pass.end);
623 if code != 0 {
624 debug(TAG, "policies blocked the commit before preparation");
625 return Ok(TransactionResult {
626 events: converged.pass.events,
627 exit_code: code,
628 landed_oid: None,
629 });
630 }
631 return conclude_commit_transaction(ConclusionInput {
632 invocation,
633 workspace: &mut workspace,
634 capture: &capture,
635 selection: &selection,
636 request,
637 checks: &mut private,
638 pass: converged.pass,
639 changed_passes: converged.changed_passes,
640 changed_paths: converged.changed_paths,
641 committed_paths: candidate_paths,
642 pathspecs: pathspecs.as_slice(),
643 });
644}
645
646#[cfg(test)]
648#[path = "commit_transaction_tests.rs"]
649mod tests;