use super::capture_order::indexed_request;
use super::commit_transaction::{CommitInvocation, TransactionResult, TransactionStop, failed};
use super::diagnostic_log::warn;
use super::policy_checks::ShippedChecks;
use super::policy_engine::StageRequest;
use super::random_id::random_uuid;
use super::repository_facts::RepositoryFacts;
use super::service_amend::{AmendSource, amended_author, read_amend_source};
use super::service_client::{Activation, COMMIT_WAIT, ClientConfig, START_WAIT, Submitted, submit};
use super::service_commit_args::{ServiceCommitArgs, opens_editor, parse_service_commit, with_fixed_message};
use super::service_handoff::ServiceHandoff;
use super::service_land::LANDING_ATTEMPTS;
use super::service_message::{author_overrides, build_message, identity};
use super::service_messages::{ClientFailure, render};
use super::service_native::{BuildInput, GitBuilt, build_with_git};
use super::service_prepare::{NativeInputs, PrepareInputs, Prepared, Ready, prepare};
use super::service_protocol::CommitRequest;
use super::service_provision::provision;
use super::service_pruned::{PRUNE_RETRIES, content_was_removed, prepared_content_missing, reach_service_phase};
use super::transaction_phase::Phase;
use super::service_route::{Route, RouteInputs, choose_route, remembered_own_executable};
use super::transaction_git::run_git_checked;
use super::worktree_completion::install_added_worktree_files;
use std::ffi::OsString;
use std::io::{Read, Write};
use std::path::PathBuf;
pub struct ServiceCommit {
pub result: TransactionResult,
pub handoff: Option<ServiceHandoff>,
}
pub const REBUILD_ATTEMPTS: usize = 5;
fn branch_moved(ready: &Ready) -> TransactionStop {
let branch: String = ready.reference.trim_start_matches("refs/heads/").to_string();
return TransactionStop::Message(render(&ClientFailure::BranchMoved { branch }));
}
fn standard_input_for(invocation: &CommitInvocation<'_>) -> Result<Option<Vec<u8>>, TransactionStop> {
let after: &[OsString] = invocation.arguments.get(invocation.subcommand_index + 1..).unwrap_or(&[]);
let Some(value) = invocation.region.pathspec_from_file else { return Ok(None) };
if super::command_options_query::value_bytes(after, value) != b"-" {
return Ok(None);
}
let mut bytes: Vec<u8> = Vec::new();
std::io::stdin().read_to_end(&mut bytes).map_err(|error| { return failed(format!("reading the pathspec file failed: {error}")); })?;
return Ok(Some(bytes));
}
pub fn try_service_commit<F: RepositoryFacts>(
invocation: &CommitInvocation<'_>,
checks: &mut ShippedChecks<F>,
request: &StageRequest,
) -> Result<Option<ServiceCommit>, TransactionStop> {
let path_variable = |name: &str| -> Option<PathBuf> { return std::env::var_os(name).filter(|value| { return !value.is_empty(); }).map(PathBuf::from); };
let runtime: Option<PathBuf> = path_variable("XDG_RUNTIME_DIR");
let config_home: Option<PathBuf> = path_variable("XDG_CONFIG_HOME");
let home: Option<PathBuf> = path_variable("HOME");
let bus_address: Option<String> = std::env::var("DBUS_SESSION_BUS_ADDRESS").ok();
let route: Route = choose_route(&RouteInputs {
runtime_dir: runtime.as_deref(),
bus_address: bus_address.as_deref(),
config_home: config_home.as_deref(),
home: home.as_deref(),
own_executable: remembered_own_executable(),
real_git: invocation.context.real_git.as_path(),
});
let Route::Service { address, provision: provisioning } = route else { return Ok(None) };
let after: &[OsString] = invocation.arguments.get(invocation.subcommand_index + 1..).unwrap_or(&[]);
let id: String = random_uuid().map_err(|error| { return failed(format!("an identity for this commit could not be made: {error:?}")); })?;
let standard_input: Option<Vec<u8>> = standard_input_for(invocation)?;
let mut provisioned: bool = false;
let editor_possible: bool = opens_editor(after);
let config: ClientConfig = ClientConfig { address, activation: Activation::Managed, start_wait: START_WAIT, commit_wait: COMMIT_WAIT };
let scratch_name: String = format!("cli-git-service-{id}");
let mut embedded: Option<Embedded> = None;
let mut decided: bool = false;
let mut fixed_message: Option<PathBuf> = None;
let mut removals: usize = 0;
for attempt in 0..REBUILD_ATTEMPTS {
let inputs: PrepareInputs<'_> = PrepareInputs {
scratch_name: scratch_name.as_str(),
allow_empty: invocation.region.allow_empty,
standard_input: standard_input.as_deref(),
first: attempt == 0,
};
let ready: Box<Ready> = match prepare(invocation, checks, request, &inputs)? {
Prepared::Ended(result) => {
if content_was_removed(result.events.as_slice()) {
removals += 1;
if removals > PRUNE_RETRIES {
return Err(TransactionStop::Message(render(&ClientFailure::ContentPruned)));
}
continue;
}
return Ok(Some(ServiceCommit { result, handoff: None }));
}
Prepared::Ready(ready) => ready,
};
if !decided {
decided = true;
if !ready.needs_native
&& let Some(parsed) = parse_service_commit(after)
{
embedded = embedded_commit(invocation, parsed)?;
}
}
let native_inputs: &NativeInputs = &ready.inputs;
let mut tree: Option<String> = None;
if !ready.needs_native
&& let Some(source) = embedded.as_ref().and_then(|made| { return made.source.as_ref(); })
{
if source.base != ready.base {
return Err(branch_moved(&ready));
}
let written = run_git_checked(
invocation.context,
&indexed_request(
invocation.cwd,
&["write-tree"],
Some(native_inputs.commit_index.as_path()),
Some(native_inputs.capture.layout.object_directory.as_path()),
),
)?;
let made: String = String::from_utf8_lossy(written.stdout.as_slice()).trim().to_string();
let against: &str = source.parent_tree.as_deref().unwrap_or(native_inputs.capture.layout.empty_tree_oid.as_str());
if source.parents.len() < 2 && !invocation.region.allow_empty && made == against {
embedded = None;
} else {
tree = Some(made);
}
}
let native: bool = ready.needs_native || embedded.is_none();
let mut typed: Option<String> = None;
let (sent, first_parent): (CommitRequest, String) = match embedded.as_ref().filter(|_| { return !native; }) {
None => {
let layout_prefix: usize = super::global_arguments::global_layout(native_inputs.arguments.as_slice()).prefix_len;
let rebuilt_arguments: Option<Vec<OsString>> = fixed_message.as_deref().and_then(|file| { return with_fixed_message(native_inputs.arguments.as_slice(), layout_prefix, file); });
let built: GitBuilt = build_with_git(&BuildInput {
context: invocation.context,
cwd: invocation.cwd,
capture: &native_inputs.capture,
scratch: ready.scratch.0.as_path(),
commit_index: native_inputs.commit_index.as_path(),
arguments: rebuilt_arguments.as_deref().unwrap_or(native_inputs.arguments.as_slice()),
pathspecs: native_inputs.pathspecs.as_slice(),
mode: native_inputs.mode,
staged_all: native_inputs.staged_all,
selected_privately: native_inputs.selected_privately,
quiet: attempt > 0,
})?;
let parent: String = built.parents.first().cloned().unwrap_or_default();
typed = Some(built.message.clone());
let sent: CommitRequest = CommitRequest {
id: id.clone(),
repository: ready.repository.clone(),
common_dir: ready.common_dir.clone(),
reference: ready.reference.clone(),
base: ready.base.clone(),
changes: Vec::new(),
message: String::new(),
author: None,
committer: String::new(),
amend: invocation.region.amend,
tree: None,
commit: Some(built.commit),
cleanup: ready.cleanup.clone(),
index: native_inputs.capture.layout.real_index_path.clone(),
};
(sent, parent)
}
Some(made) => {
let sent: CommitRequest = CommitRequest {
id: id.clone(),
repository: ready.repository.clone(),
common_dir: ready.common_dir.clone(),
reference: ready.reference.clone(),
base: ready.base.clone(),
changes: ready.changes.clone().unwrap_or_default(),
message: made.message.clone(),
author: Some(made.author.clone()),
committer: made.committer.clone(),
amend: tree.is_some(),
tree: tree.clone(),
commit: None,
cleanup: None,
index: native_inputs.capture.layout.real_index_path.clone(),
};
let parent: String = match made.source.as_ref() {
Some(replaced) => replaced.parents.first().cloned().unwrap_or_default(),
None => ready.base.clone(),
};
(sent, parent)
}
};
reach_service_phase(Phase::ServicePrepared)?;
if let Some(host) = provisioning.as_ref().filter(|_| { return !provisioned; }) {
provision(host).map_err(|failure| { return TransactionStop::Message(render(&failure)); })?;
provisioned = true;
}
match submit(&config, &sent, &mut std::io::stderr()) {
Ok(Submitted::Landed { commit, .. }) => {
if let Some(file) = fixed_message.as_ref() {
let _removed = std::fs::remove_file(file);
}
if let Some(made) = embedded.as_ref().filter(|made| { return !native && !made.quiet; }) {
let subject: &str = made.message.lines().next().unwrap_or("");
let short: &str = commit.get(..7).unwrap_or(commit.as_str());
let branch: &str = ready.reference.trim_start_matches("refs/heads/");
print_summary(format!("[{branch} {short}] {subject}").as_str());
}
if native && attempt > 0 {
let subject: &str = typed.as_deref().and_then(|text| { return text.lines().next(); }).unwrap_or("");
let short: &str = commit.get(..7).unwrap_or(commit.as_str());
let branch: &str = ready.reference.trim_start_matches("refs/heads/");
print_summary(format!("[{branch} {short}] {subject}").as_str());
}
install_added_worktree_files(
invocation.context,
invocation.cwd,
ready.repository.as_path(),
ready.worktree_records.as_slice(),
Some(native_inputs.capture.layout.object_directory.as_path()),
)
.map_err(|error| { return TransactionStop::Message(error.0); })?;
let handoff: ServiceHandoff = ServiceHandoff {
config: config.clone(),
repository: ready.repository.clone(),
common_dir: ready.common_dir.clone(),
reference: ready.reference.clone(),
parent: first_parent,
};
let result: TransactionResult = TransactionResult { events: ready.events, exit_code: 0, landed_oid: Some(commit), uses_lfs: ready.uses_lfs };
return Ok(Some(ServiceCommit { result, handoff: Some(handoff) }));
}
Ok(Submitted::PrepareAgain { .. }) => {
if native || sent.amend {
return Err(branch_moved(&ready));
}
if attempt + 1 == LANDING_ATTEMPTS {
let branch: String = ready.reference.trim_start_matches("refs/heads/").to_string();
return Err(TransactionStop::Message(render(&ClientFailure::BranchKeepsChanging { branch, times: LANDING_ATTEMPTS })));
}
}
Ok(Submitted::Moved) => {
let branch: String = ready.reference.trim_start_matches("refs/heads/").to_string();
if native && !sent.amend && !editor_possible && !ready.interactive && !ready.fixed_parents {
if attempt + 1 == REBUILD_ATTEMPTS {
return Err(TransactionStop::Message(render(&ClientFailure::BranchKeepsChanging { branch, times: REBUILD_ATTEMPTS })));
}
continue;
}
if native && editor_possible && let Some(text) = typed.as_deref() {
let file: PathBuf = native_inputs.capture.layout.git_dir.join(format!("cli-git-saved-message-{}", id.get(..8).unwrap_or(id.as_str())));
match std::fs::write(&file, text) {
Ok(()) => {
let prefix: usize = super::global_arguments::global_layout(native_inputs.arguments.as_slice()).prefix_len;
let repeatable: bool = !sent.amend
&& !ready.interactive
&& !ready.fixed_parents
&& with_fixed_message(native_inputs.arguments.as_slice(), prefix, file.as_path()).is_some();
if repeatable && attempt + 1 < REBUILD_ATTEMPTS {
fixed_message = Some(file);
continue;
}
let failure: ClientFailure = ClientFailure::BranchMovedMessageSaved { branch, file: file.display().to_string(), amend: sent.amend };
return Err(TransactionStop::Message(render(&failure)));
}
Err(error) => warn("serviceCommit", format!("saving the typed message failed: {error}").as_str()),
}
}
return Err(branch_moved(&ready));
}
Err(ClientFailure::Unavailable)
if prepared_content_missing(
native_inputs.capture.layout.common_dir.as_path(),
native_inputs.capture.layout.object_directory.as_path(),
native_inputs.capture.layout.object_format.as_str(),
&sent,
) =>
{
removals += 1;
if removals > PRUNE_RETRIES {
return Err(TransactionStop::Message(render(&ClientFailure::ContentPruned)));
}
}
Err(failure) => return Err(TransactionStop::Message(render(&failure))),
}
}
return Err(TransactionStop::Message(render(&ClientFailure::BranchKeepsChanging { branch: String::from("the branch"), times: REBUILD_ATTEMPTS })));
}
fn print_summary(line: &str) {
let mut out = std::io::stdout();
let written = writeln!(out, "{line}").and_then(|()| { return out.flush(); });
let Err(error) = written else { return };
if error.kind() == std::io::ErrorKind::BrokenPipe {
unsafe {
libc::signal(libc::SIGPIPE, libc::SIG_DFL);
libc::raise(libc::SIGPIPE);
}
}
let _reported = writeln!(std::io::stderr(), "fatal: write error: {error}");
std::process::exit(128);
}
struct Embedded {
message: String,
author: String,
committer: String,
source: Option<AmendSource>,
quiet: bool,
}
fn embedded_commit(invocation: &CommitInvocation<'_>, parsed: ServiceCommitArgs) -> Result<Option<Embedded>, TransactionStop> {
let Some(committer) = identity(invocation.context, invocation.cwd, "GIT_COMMITTER_IDENT", &[])? else { return Ok(None) };
let source: Option<AmendSource> = if parsed.amend {
let Some(read) = read_amend_source(invocation.context, invocation.cwd)? else { return Ok(None) };
Some(read)
} else {
None
};
let previous: Option<&str> = source.as_ref().map(|found| { return found.message.as_str(); });
let Some(built) = build_message(invocation, &parsed, committer.as_str(), previous)? else { return Ok(None) };
let author: String = match built.reused_author {
Some(reused) => reused,
None => match source.as_ref() {
Some(replaced) => {
let Some(made) = amended_author(invocation.context, invocation.cwd, replaced, &parsed)? else { return Ok(None) };
made
}
None => {
let Some(made) = identity(invocation.context, invocation.cwd, "GIT_AUTHOR_IDENT", author_overrides(&parsed).as_slice())? else {
return Ok(None);
};
made
}
},
};
return Ok(Some(Embedded { message: built.message, author, committer, source, quiet: parsed.quiet }));
}
#[cfg(test)]
#[path = "service_commit_tests.rs"]
mod tests;