use super::capture_invocation::InvocationCapture;
use super::commit_args::{resolve_private_commit_args, split_commit_invocation, without_all_flag};
use super::commit_landing_loop::{LoopInput, LoopOutcome, land_with_replay};
use super::commit_facts::PrivateIndexFacts;
use super::commit_index::write_private_tree;
use super::commit_no_change::{NoChangeInput, complete_no_change_transaction};
use super::commit_preparation::{PREPARED_FILENAME, PreparationHooks, PreparationRequest, read_prepared_commit, run_native_commit};
use super::commit_replay_options::{ReplayOptions, replay_options};
use super::commit_transaction::{CommitInvocation, Selection, TransactionResult, TransactionStop, failed, write_journal_record};
use super::commit_workspace::CommitWorkspace;
use super::commit_worktree_records::{HookChanges, describe_hook_changes, selected_worktree_records};
use super::diagnostic_log::debug;
use super::global_arguments::global_layout;
use super::hook_dispatch_entries::write_hook_dispatch;
use super::hook_dispatch_plan::{HookDispatchPlan, HookPlanInputs, SILENCED_HOOK_EVENTS, compute_hook_dispatch_plan, new_preparation_lease};
use super::policy_checks::ShippedChecks;
use super::policy_engine::StageRequest;
use super::policy_events::PolicyEvent;
use super::policy_pass::PassResult;
use super::post_landing::{LandedTransaction, PostLandingRequest, complete_landed_transaction};
use super::repository_facts::RepositoryFacts;
use super::transaction_git::GitContext;
use super::transaction_journal::{AddedPath, PreparedRecord};
use super::transaction_journal_encode::encode_prepared;
use super::transaction_phase::{Phase, reach_transaction_phase};
use std::ffi::OsString;
use std::path::Path;
const TAG: &str = "concludeCommitTransaction";
pub struct ConclusionInput<'a, F: RepositoryFacts> {
pub invocation: &'a CommitInvocation<'a>,
pub workspace: &'a mut CommitWorkspace,
pub capture: &'a InvocationCapture,
pub selection: &'a Selection,
pub request: &'a StageRequest,
pub checks: &'a mut ShippedChecks<PrivateIndexFacts<F>>,
pub pass: PassResult,
pub changed_passes: u64,
pub changed_paths: Vec<String>,
pub committed_paths: Vec<String>,
pub pathspecs: &'a [String],
}
pub fn events_with_changed_summary(events: Vec<PolicyEvent>, passes: u64, changed_paths: &[String]) -> Vec<PolicyEvent> {
if passes == 0 {
return events;
}
return events_with_summary(events, passes, changed_paths);
}
pub fn events_with_summary(mut events: Vec<PolicyEvent>, passes: u64, changed_paths: &[String]) -> Vec<PolicyEvent> {
let mut sorted: Vec<String> = changed_paths.to_vec();
sorted.sort_by(|left, right| { return left.as_bytes().cmp(right.as_bytes()); });
events.push(PolicyEvent::FixSummary {
trigger: super::policy_trigger::Trigger::PreForward,
passes,
changed_paths: sorted
.iter()
.map(|path| { return super::event_path::EventPath::from_git_bytes(path.as_bytes()); })
.collect(),
});
return events;
}
fn private_invocation(pass: &PassResult, pathspecs: &[String], selection: &Selection) -> (Vec<String>, Vec<String>) {
let text: Vec<String> = pass.arguments.iter().map(|token| { return token.to_string_lossy().into_owned(); }).collect();
let private_args: Vec<String> =
resolve_private_commit_args(text.as_slice(), pathspecs, selection.mode, selection.read_only);
let owned: Vec<OsString> = private_args.iter().map(OsString::from).collect();
let index: usize = global_layout(owned.as_slice()).prefix_len;
return split_commit_invocation(private_args.as_slice(), index);
}
pub fn conclude_commit_transaction<F: RepositoryFacts>(
input: ConclusionInput<'_, F>,
) -> Result<TransactionResult, TransactionStop> {
let ConclusionInput {
invocation,
workspace,
capture,
selection,
request,
checks,
pass,
changed_passes,
changed_paths,
committed_paths,
pathspecs,
} = input;
let context: &GitContext = invocation.context;
let cwd: &Path = invocation.cwd;
let repository_root: &Path = capture.layout.repository_root.as_path();
let timeout_ms: u64 = invocation.config.concurrency.index_lock.unproven_owner_timeout_ms;
let intended_tree: String = write_private_tree(
context,
cwd,
workspace.landing.commit_index.as_path(),
workspace.object_directory.as_path(),
)?;
let changes = checks.content.install_changes();
let selected: Vec<AddedPath> = selected_worktree_records(
context,
cwd,
repository_root,
workspace.landing.commit_index.as_path(),
changes.as_slice(),
)?;
let eligible: bool = !changes.is_empty()
&& !invocation.region.allow_empty
&& !invocation.region.amend
&& !selection.concludes_sequencer;
if let Some(result) = complete_no_change_transaction(NoChangeInput {
invocation,
workspace,
capture,
mode: selection.mode,
eligible,
intended_tree: intended_tree.as_str(),
committed_paths: committed_paths.as_slice(),
selected_worktree_paths: selected.as_slice(),
events: &pass.events,
changed_passes,
changed_paths: changed_paths.as_slice(),
index_lock_timeout_ms: timeout_ms,
})? {
return Ok(result);
}
let (global_args, commit_args): (Vec<String>, Vec<String>) = private_invocation(&pass, pathspecs, selection);
let global_os: Vec<OsString> = global_args.iter().map(OsString::from).collect();
let lease: String = new_preparation_lease(workspace.landing.directory.as_path())
.map_err(|error| { return failed(format!("{error:?}")); })?;
let plan: HookDispatchPlan = compute_hook_dispatch_plan(&HookPlanInputs {
context,
cwd,
global_arguments: global_os.as_slice(),
common_directory: capture.layout.common_dir.as_path(),
worktree_root: repository_root,
lease: lease.as_str(),
concurrent_commits: invocation.config.concurrency.hooks.concurrent_commits,
inherited_lease_valid: invocation.inherited_lease_valid,
environment: invocation.environment,
})
.map_err(|error| { return failed(format!("{error:?}")); })?;
write_hook_dispatch(workspace.hooks_directory.as_path(), &plan, invocation.executable)
.map_err(|error| { return failed(error.to_string()); })?;
let hooks: PreparationHooks = PreparationHooks {
directory: workspace.hooks_directory.clone(),
silenced_events: SILENCED_HOOK_EVENTS.iter().map(|event| { return String::from(*event); }).collect(),
};
let native_args: Vec<String> = if selection.staged_all {
without_all_flag(commit_args.as_slice())
} else {
commit_args.clone()
};
let preparation: PreparationRequest<'_> = PreparationRequest {
capture,
shadow: workspace.landing.shadow.as_path(),
hooks: &hooks,
commit_index: workspace.landing.commit_index.as_path(),
global_args: global_args.as_slice(),
commit_args: native_args.as_slice(),
};
let status: i32 = run_native_commit(context, cwd, &preparation).map_err(|error| { return TransactionStop::Message(error.to_string()); })?;
if status != 0 {
return Err(TransactionStop::NativeExit(status));
}
let prepared = read_prepared_commit(context, workspace.landing.shadow.as_path())?;
let hook_changes: HookChanges = describe_hook_changes(
context,
cwd,
workspace.object_directory.as_path(),
intended_tree.as_str(),
prepared.tree_oid.as_str(),
)?;
debug(
TAG,
format!("prepared {}; hooks changed {} paths", prepared.oid, hook_changes.paths.len()).as_str(),
);
let mut prepared_records: Vec<AddedPath> = selected.clone();
prepared_records.extend(hook_changes.worktree_records.iter().cloned());
write_journal_record(
workspace.landing.directory.as_path(),
PREPARED_FILENAME,
encode_prepared(&PreparedRecord {
shadow_path: workspace.landing.shadow.display().to_string(),
prepared_oid: prepared.oid.clone(),
signed: prepared.signed,
intended_tree_oid: intended_tree.clone(),
committed_paths: committed_paths.clone(),
added_paths: Vec::new(),
selected_worktree_paths: prepared_records.clone(),
})
.as_str(),
)?;
reach_transaction_phase(Phase::PreparationDone, None)?;
let settled_events: Vec<PolicyEvent> = events_with_changed_summary(pass.events.clone(), changed_passes, changed_paths.as_slice());
let options: ReplayOptions = replay_options(commit_args.as_slice());
let outcome: LoopOutcome = land_with_replay(LoopInput {
invocation,
workspace,
capture,
mode: selection.mode,
request,
checks,
prepared: &prepared,
settled_events: settled_events.clone(),
options: &options,
global_args: global_args.as_slice(),
read_only_selection: selection.read_only,
committed_paths: committed_paths.clone(),
added_paths: Vec::new(),
worktree_records: prepared_records.clone(),
hook_changes,
index_lock_timeout_ms: timeout_ms,
reserve_after_lost_races: invocation.config.concurrency.landing.reserve_after_lost_races,
})?;
let (oid, loop_events, final_worktree, final_added): (String, Vec<PolicyEvent>, Vec<AddedPath>, Vec<AddedPath>) =
match outcome {
LoopOutcome::Failed { events, exit_code } => {
return Ok(TransactionResult {
events,
exit_code,
landed_oid: None,
});
}
LoopOutcome::Landed {
oid,
events,
worktree_records,
added_paths,
} => (oid, events, worktree_records, added_paths),
};
let landed: LandedTransaction = LandedTransaction {
landed_oid: oid.clone(),
branch: capture.layout.target_ref.clone(),
cwd: cwd.to_path_buf(),
repository_root: repository_root.to_path_buf(),
directory: workspace.landing.directory.clone(),
shadow: workspace.landing.shadow.clone(),
git_dir: capture.layout.git_dir.clone(),
registry_root: capture.layout.registry_root.clone(),
real_index: capture.layout.real_index_path.clone(),
added_paths: final_added,
worktree_records: final_worktree,
};
let completed = complete_landed_transaction(PostLandingRequest {
context,
plan: &plan,
landed: &landed,
});
if let Err(error) = completed {
return Err(TransactionStop::Message(error.to_string()));
}
workspace.finish_transaction();
workspace.mark_disposed();
let mut events: Vec<PolicyEvent> = settled_events;
events.extend(loop_events);
return Ok(TransactionResult {
events,
exit_code: 0,
landed_oid: Some(oid),
});
}
#[cfg(test)]
#[path = "commit_conclusion_tests.rs"]
mod tests;