1use super::commit_timing::CommitTiming;
20use super::private_index_request::indexed_request;
21use super::commit_transaction::{CommitInvocation, TransactionResult, TransactionStop, failed};
22use super::diagnostic_log::warn;
23use super::policy_checks::ShippedChecks;
24use super::policy_engine::StageRequest;
25use super::random_id::random_uuid;
26use super::repository_facts::RepositoryFacts;
27use super::service_amend::{AmendSource, amended_author, read_amend_source};
28use super::service_client::{Activation, COMMIT_WAIT, ClientConfig, START_WAIT, ScratchWatch, Submitted, submit_traced, watch_scratch};
29use super::landed_summary::{change_summary, short_name, subject_of, summary_line};
30use super::service_commit_args::{ServiceCommitArgs, asks_for_quiet, opens_editor, parse_service_commit, with_fixed_message};
31use super::service_handoff::ServiceHandoff;
32use super::service_land::LANDING_ATTEMPTS;
33use super::service_message::{author_overrides, build_message, identity};
34use super::service_messages::{ClientFailure, render};
35use super::service_native::{BuildInput, GitBuilt, build_with_git};
36use super::service_prepare::{NativeInputs, PrepareInputs, Prepared, Ready, prepare};
37use super::service_replay::{Captured, Retry, conflict_result, retry_of};
38use super::service_protocol::CommitRequest;
39use super::service_provision::{platform_without_manager, provision};
40use super::service_pruned::{content_was_removed, prepared_content_missing, reach_service_phase};
41use super::transaction_phase::Phase;
42use super::service_route::{Route, RouteInputs, choose_route, remembered_own_executable, worktree_git_dir};
43use super::transaction_git::{GitRequest, run_git, run_git_checked};
44use super::worktree_completion::install_added_worktree_files;
45use std::ffi::OsString;
46use std::io::{Read, Write};
47use std::path::PathBuf;
48use std::time::Instant;
49
50pub struct ServiceCommit {
52 pub result: TransactionResult,
54 pub handoff: Option<ServiceHandoff>,
56}
57
58pub const REBUILD_ATTEMPTS: usize = 5;
61
62fn branch_moved(ready: &Ready) -> TransactionStop {
64 let branch: String = ready.reference.trim_start_matches("refs/heads/").to_string();
65 return TransactionStop::Message(render(&ClientFailure::BranchMoved { branch }));
66}
67
68fn arguments_after_subcommand(arguments: &[OsString], subcommand_index: usize) -> &[OsString] {
80 return arguments.get(subcommand_index + 1..).unwrap_or(&[]);
81}
82
83fn switched_branch_name(reference: &str) -> String {
97 return if reference == "HEAD" { String::from("a detached head") } else { reference.trim_start_matches("refs/heads/").to_string() };
98}
99
100fn standard_input_for(invocation: &CommitInvocation<'_>) -> Result<Option<Vec<u8>>, TransactionStop> {
102 let after: &[OsString] = arguments_after_subcommand(invocation.arguments, invocation.subcommand_index);
103 let Some(value) = invocation.region.pathspec_from_file else { return Ok(None) };
104 if super::command_options_query::value_bytes(after, value) != b"-" {
105 return Ok(None);
106 }
107 let mut bytes: Vec<u8> = Vec::new();
108 std::io::stdin().read_to_end(&mut bytes).map_err(|error| { return failed(format!("reading the pathspec file failed: {error}")); })?;
109 return Ok(Some(bytes));
110}
111
112fn repository_not_found(invocation: &CommitInvocation<'_>) -> TransactionStop {
123 let asked = run_git(invocation.context, &GitRequest::new(invocation.cwd, &["rev-parse", "--git-dir"]));
124 let output = match asked {
125 Ok(found) => found,
126 Err(error) => return failed(format!("asking Git which repository this directory belongs to failed: {error}")),
127 };
128 if output.succeeded() {
129 return failed(String::from("this directory belongs to a repository whose Git directory could not be located"));
130 }
131 let _reported = std::io::stderr().write_all(output.stderr.as_slice());
132 return TransactionStop::NativeExit(output.code.unwrap_or(1));
133}
134
135pub fn service_commit<F: RepositoryFacts>(
146 invocation: &CommitInvocation<'_>,
147 checks: &mut ShippedChecks<F>,
148 request: &StageRequest,
149 timing: &mut CommitTiming,
150) -> Result<ServiceCommit, TransactionStop> {
151 let path_variable = |name: &str| -> Option<PathBuf> { return std::env::var_os(name).filter(|value| { return !value.is_empty(); }).map(PathBuf::from); };
152 let runtime: Option<PathBuf> = path_variable("XDG_RUNTIME_DIR");
153 let config_home: Option<PathBuf> = path_variable("XDG_CONFIG_HOME");
154 let home: Option<PathBuf> = path_variable("HOME");
155 let bus_address: Option<String> = std::env::var("DBUS_SESSION_BUS_ADDRESS").ok();
156 let Some(git_dir) = worktree_git_dir(invocation.context, invocation.cwd) else { return Err(repository_not_found(invocation)) };
158 let route: Option<Route> = choose_route(&RouteInputs {
159 runtime_dir: runtime.as_deref(),
160 bus_address: bus_address.as_deref(),
161 config_home: config_home.as_deref(),
162 home: home.as_deref(),
163 own_executable: remembered_own_executable(),
164 real_git: invocation.context.real_git.as_path(),
165 git_dir: git_dir.as_path(),
166 });
167 let Some(Route { address, provision: provisioning }) = route else {
168 return Err(TransactionStop::Message(render(&ClientFailure::NoActivation(platform_without_manager()))));
169 };
170 timing.place_at(git_dir.as_path());
173 let after: &[OsString] = arguments_after_subcommand(invocation.arguments, invocation.subcommand_index);
174 let id: String = random_uuid().map_err(|error| { return failed(format!("an identity for this commit could not be made: {error:?}")); })?;
175 timing.request = Some(id.clone());
177 let standard_input: Option<Vec<u8>> = standard_input_for(invocation)?;
178 let mut provisioned: bool = false;
179 let editor_possible: bool = opens_editor(after);
181 let config: ClientConfig = ClientConfig { address, activation: Activation::Managed, start_wait: START_WAIT, commit_wait: COMMIT_WAIT };
182 let scratch_name: String = format!("cli-git-service-{id}");
183 let mut embedded: Option<Embedded> = None;
187 let mut decided: bool = false;
188 let mut fixed_message: Option<PathBuf> = None;
190 let mut last_message: Option<String> = None;
192 let mut retry: Option<Captured> = None;
195 let mut first_reference: Option<String> = None;
197 let mut scratch_watch: Option<ScratchWatch> = None;
201 for attempt in 0..REBUILD_ATTEMPTS {
202 let inputs: PrepareInputs<'_> = PrepareInputs {
203 scratch_name: scratch_name.as_str(),
204 allow_empty: invocation.region.allow_empty,
205 standard_input: standard_input.as_deref(),
206 first: attempt == 0,
207 replay: retry.as_ref().map(|captured| { return Retry { captured, attempt: u64::try_from(attempt).unwrap_or(u64::MAX) }; }),
208 };
209 let preparing: Instant = Instant::now();
210 let prepared: Result<Prepared, TransactionStop> = prepare(invocation, checks, request, &inputs);
211 timing.prepare += preparing.elapsed();
212 timing.attempts += 1;
213 let ready: Box<Ready> = match prepared? {
214 Prepared::Conflict(conflict) => {
215 let saved: PathBuf = conflict.git_dir.join(format!("cli-git-saved-message-{}", id.get(..8).unwrap_or(id.as_str())));
217 let kept: Option<&PathBuf> = last_message.as_ref().and_then(|text| {
218 return match std::fs::write(&saved, text.as_str()) {
219 Ok(()) => Some(&saved),
220 Err(error) => {
221 warn("serviceCommit", format!("saving the message failed: {error}").as_str());
222 None
223 }
224 };
225 });
226 let result: TransactionResult = conflict_result(&conflict, kept.map(PathBuf::as_path));
227 return Ok(ServiceCommit { result, handoff: None });
228 }
229 Prepared::Ended(result) => {
230 if content_was_removed(result.events.as_slice()) {
233 return Err(TransactionStop::Message(render(&ClientFailure::ContentPruned)));
234 }
235 return Ok(ServiceCommit { result, handoff: None });
236 }
237 Prepared::Ready(ready) => ready,
238 };
239 let building: Instant = Instant::now();
242 if timing.reference.is_none() {
243 timing.reference = Some(ready.reference.clone());
244 }
245 let first_branch: &str = first_reference.get_or_insert_with(|| { return ready.reference.clone(); });
246 if first_branch != ready.reference {
247 let failure: ClientFailure = ClientFailure::BranchSwitched {
248 branch: switched_branch_name(first_branch),
249 now: switched_branch_name(ready.reference.as_str()),
250 file: fixed_message.as_ref().map(|file| { return file.display().to_string(); }),
251 };
252 return Err(TransactionStop::Message(render(&failure)));
253 }
254 if scratch_watch.is_none() {
255 scratch_watch = watch_scratch(&config, ready.inputs.capture.layout.git_dir.as_path(), ready.scratch.0.as_path());
256 }
257 if !decided {
258 decided = true;
259 if !ready.needs_native
260 && let Some(parsed) = parse_service_commit(after)
261 {
262 embedded = embedded_commit(invocation, parsed)?;
263 }
264 }
265 let native_inputs: &NativeInputs = &ready.inputs;
266 let mut tree: Option<String> = None;
271 if !ready.needs_native
272 && let Some(source) = embedded.as_ref().and_then(|made| { return made.source.as_ref(); })
273 {
274 if source.base != ready.base {
275 return Err(branch_moved(&ready));
276 }
277 let written = run_git_checked(
278 invocation.context,
279 &indexed_request(
280 invocation.cwd,
281 &["write-tree"],
282 Some(native_inputs.commit_index.as_path()),
283 Some(native_inputs.capture.layout.object_directory.as_path()),
284 ),
285 )?;
286 let made: String = String::from_utf8_lossy(written.stdout.as_slice()).trim().to_string();
287 let against: &str = source.parent_tree.as_deref().unwrap_or(native_inputs.capture.layout.empty_tree_oid.as_str());
288 if source.parents.len() < 2 && !invocation.region.allow_empty && made == against {
289 embedded = None;
290 } else {
291 tree = Some(made);
292 }
293 }
294 let native: bool = ready.needs_native || embedded.is_none();
295 let mut typed: Option<String> = None;
296 let (sent, first_parent): (CommitRequest, String) = match embedded.as_ref().filter(|_| { return !native; }) {
297 None => {
298 let layout_prefix: usize = super::global_arguments::global_layout(native_inputs.arguments.as_slice()).prefix_len;
299 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); });
300 let built: GitBuilt = build_with_git(&BuildInput {
301 context: invocation.context,
302 cwd: invocation.cwd,
303 capture: &native_inputs.capture,
304 scratch: ready.scratch.0.as_path(),
305 commit_index: native_inputs.commit_index.as_path(),
306 arguments: rebuilt_arguments.as_deref().unwrap_or(native_inputs.arguments.as_slice()),
307 pathspecs: native_inputs.pathspecs.as_slice(),
308 mode: native_inputs.mode,
309 staged_all: native_inputs.staged_all,
310 selected_privately: native_inputs.selected_privately,
311 quiet: true,
313 })?;
314 let parent: String = built.parents.first().cloned().unwrap_or_default();
315 typed = Some(built.message.clone());
316 let sent: CommitRequest = CommitRequest {
317 id: id.clone(),
318 repository: ready.repository.clone(),
319 common_dir: ready.common_dir.clone(),
320 git_dir: native_inputs.capture.layout.git_dir.clone(),
321 reference: ready.reference.clone(),
322 base: ready.base.clone(),
323 changes: Vec::new(),
325 message: String::new(),
327 author: None,
328 committer: String::new(),
329 amend: invocation.region.amend,
330 tree: None,
331 commit: Some(built.commit),
332 cleanup: ready.cleanup.clone(),
333 index: native_inputs.capture.layout.real_index_path.clone(),
334 captured: ready.captured_at,
335 };
336 (sent, parent)
337 }
338 Some(made) => {
339 let sent: CommitRequest = CommitRequest {
340 id: id.clone(),
341 repository: ready.repository.clone(),
342 common_dir: ready.common_dir.clone(),
343 git_dir: native_inputs.capture.layout.git_dir.clone(),
344 reference: ready.reference.clone(),
345 base: ready.base.clone(),
346 changes: ready.changes.clone().unwrap_or_default(),
347 message: made.message.clone(),
348 author: Some(made.author.clone()),
349 committer: made.committer.clone(),
350 amend: tree.is_some(),
351 tree: tree.clone(),
352 commit: None,
353 cleanup: None,
354 index: native_inputs.capture.layout.real_index_path.clone(),
355 captured: ready.captured_at,
356 };
357 let parent: String = match made.source.as_ref() {
359 Some(replaced) => replaced.parents.first().cloned().unwrap_or_default(),
360 None => ready.base.clone(),
361 };
362 (sent, parent)
363 }
364 };
365 last_message = typed.clone().or_else(|| { return embedded.as_ref().filter(|_| { return !native; }).map(|made| { return made.message.clone(); }); });
366 timing.build += building.elapsed();
368 reach_service_phase(Phase::ServicePrepared)?;
369 if let Some(host) = provisioning.as_ref().filter(|_| { return !provisioned; }) {
372 let provisioning_began: Instant = Instant::now();
373 let provided = provision(host);
374 timing.provision += provisioning_began.elapsed();
375 provided.map_err(|failure| { return TransactionStop::Message(render(&failure)); })?;
376 provisioned = true;
377 }
378 match submit_traced(&config, &sent, &mut std::io::stderr(), &mut timing.submit) {
379 Ok(Submitted::Landed { commit, .. }) => {
380 let after_landing: Instant = Instant::now();
381 if let Some(file) = fixed_message.as_ref() {
382 let _removed = std::fs::remove_file(file);
384 }
385 let embedded_made: Option<&Embedded> = embedded.as_ref().filter(|_| { return !native; });
388 let quiet: bool = embedded_made.map_or_else(|| { return asks_for_quiet(after); }, |made| { return made.quiet; });
389 if !quiet {
390 let message: &str = embedded_made.map(|made| { return made.message.as_str(); }).or(typed.as_deref()).unwrap_or("");
391 let name: String = short_name(
392 invocation.context,
393 invocation.cwd,
394 native_inputs.capture.layout.object_directory.as_path(),
395 native_inputs.capture.layout.config_snapshot.as_ref(),
396 commit.as_str(),
397 );
398 print_summary(summary_line(ready.reference.as_str(), first_parent.as_str(), name.as_str(), subject_of(message).as_str()).as_str());
399 if embedded_made.is_none() {
400 let changes: String = change_summary(invocation.context, invocation.cwd, first_parent.as_str(), commit.as_str());
402 if !changes.is_empty() {
403 print_summary(changes.as_str());
404 }
405 }
406 }
407 install_added_worktree_files(
409 invocation.context,
410 invocation.cwd,
411 ready.repository.as_path(),
412 ready.worktree_records.as_slice(),
413 Some(native_inputs.capture.layout.object_directory.as_path()),
414 )
415 .map_err(|error| { return TransactionStop::Message(error.0); })?;
416 let handoff: ServiceHandoff = ServiceHandoff {
417 config: config.clone(),
418 repository: ready.repository.clone(),
419 git_dir: native_inputs.capture.layout.git_dir.clone(),
420 reference: ready.reference.clone(),
421 parent: first_parent,
422 };
423 let result: TransactionResult = TransactionResult { events: ready.events, exit_code: 0, landed_oid: Some(commit), uses_lfs: ready.uses_lfs };
424 timing.after_landing += after_landing.elapsed();
425 return Ok(ServiceCommit { result, handoff: Some(handoff) });
426 }
427 Ok(Submitted::PrepareAgain { landed, .. }) => {
428 if native || sent.amend {
429 return Err(branch_moved(&ready));
431 }
432 if attempt + 1 == LANDING_ATTEMPTS {
433 let branch: String = ready.reference.trim_start_matches("refs/heads/").to_string();
434 return Err(TransactionStop::Message(render(&ClientFailure::BranchKeepsChanging { branch, times: LANDING_ATTEMPTS })));
435 }
436 retry = Some(retry_of(invocation.context, invocation.cwd, &ready, landed)?);
438 }
439 Ok(Submitted::Moved) => {
440 let branch: String = ready.reference.trim_start_matches("refs/heads/").to_string();
441 if native && !sent.amend && !editor_possible && !ready.interactive && !ready.fixed_parents {
444 if attempt + 1 == REBUILD_ATTEMPTS {
445 return Err(TransactionStop::Message(render(&ClientFailure::BranchKeepsChanging { branch, times: REBUILD_ATTEMPTS })));
446 }
447 retry = Some(retry_of(invocation.context, invocation.cwd, &ready, Vec::new())?);
449 continue;
450 }
451 if native && editor_possible && let Some(text) = typed.as_deref() {
452 let file: PathBuf = native_inputs.capture.layout.git_dir.join(format!("cli-git-saved-message-{}", id.get(..8).unwrap_or(id.as_str())));
454 match std::fs::write(&file, text) {
455 Ok(()) => {
456 let prefix: usize = super::global_arguments::global_layout(native_inputs.arguments.as_slice()).prefix_len;
459 let repeatable: bool = !sent.amend
460 && !ready.interactive
461 && !ready.fixed_parents
462 && with_fixed_message(native_inputs.arguments.as_slice(), prefix, file.as_path()).is_some();
463 if repeatable && attempt + 1 < REBUILD_ATTEMPTS {
464 fixed_message = Some(file);
465 retry = Some(retry_of(invocation.context, invocation.cwd, &ready, Vec::new())?);
466 continue;
467 }
468 let failure: ClientFailure = ClientFailure::BranchMovedMessageSaved { branch, file: file.display().to_string(), amend: sent.amend };
469 return Err(TransactionStop::Message(render(&failure)));
470 }
471 Err(error) => warn("serviceCommit", format!("saving the typed message failed: {error}").as_str()),
472 }
473 }
474 return Err(branch_moved(&ready));
475 }
476 Err(ClientFailure::Unavailable)
477 if prepared_content_missing(
478 native_inputs.capture.layout.common_dir.as_path(),
479 native_inputs.capture.layout.object_directory.as_path(),
480 native_inputs.capture.layout.object_format.as_str(),
481 &sent,
482 ) =>
483 {
484 return Err(TransactionStop::Message(render(&ClientFailure::ContentPruned)));
487 }
488 Err(failure) => return Err(TransactionStop::Message(render(&failure))),
489 }
490 }
491 return Err(TransactionStop::Message(render(&ClientFailure::BranchKeepsChanging { branch: String::from("the branch"), times: REBUILD_ATTEMPTS })));
492}
493
494fn print_summary(line: &str) {
505 let mut out = std::io::stdout();
506 let written = writeln!(out, "{line}").and_then(|()| { return out.flush(); });
507 let Err(error) = written else { return };
508 if error.kind() == std::io::ErrorKind::BrokenPipe {
509 unsafe {
511 libc::signal(libc::SIGPIPE, libc::SIG_DFL);
512 libc::raise(libc::SIGPIPE);
513 }
514 }
515 let _reported = writeln!(std::io::stderr(), "fatal: write error: {error}");
516 std::process::exit(128);
517}
518
519struct Embedded {
521 message: String,
522 author: String,
523 committer: String,
524 source: Option<AmendSource>,
525 quiet: bool,
526}
527
528fn embedded_commit(invocation: &CommitInvocation<'_>, parsed: ServiceCommitArgs) -> Result<Option<Embedded>, TransactionStop> {
539 let Some(committer) = identity(invocation.context, invocation.cwd, "GIT_COMMITTER_IDENT", &[])? else { return Ok(None) };
540 let source: Option<AmendSource> = if parsed.amend {
541 let Some(read) = read_amend_source(invocation.context, invocation.cwd)? else { return Ok(None) };
542 Some(read)
543 } else {
544 None
545 };
546 let previous: Option<&str> = source.as_ref().map(|found| { return found.message.as_str(); });
547 let Some(built) = build_message(invocation, &parsed, committer.as_str(), previous)? else { return Ok(None) };
548 let author: String = match built.reused_author {
549 Some(reused) => reused,
550 None => match source.as_ref() {
551 Some(replaced) => {
552 let Some(made) = amended_author(invocation.context, invocation.cwd, replaced, &parsed)? else { return Ok(None) };
553 made
554 }
555 None => {
556 let Some(made) = identity(invocation.context, invocation.cwd, "GIT_AUTHOR_IDENT", author_overrides(&parsed).as_slice())? else {
557 return Ok(None);
558 };
559 made
560 }
561 },
562 };
563 return Ok(Some(Embedded { message: built.message, author, committer, source, quiet: parsed.quiet }));
564}
565
566#[cfg(test)]
568#[path = "service_commit_tests.rs"]
569mod tests;