1use crate::commit_timing::CommitTiming;
20use crate::conflict_markers::{open_operation_line, paths_with_markers};
22use crate::transaction_journal::Conclusion;
24use crate::private_index_request::indexed_request;
25use crate::commit_transaction::{CommitInvocation, TransactionResult, TransactionStop, failed};
26use crate::diagnostic_log::warn;
27use crate::policy_checks::ShippedChecks;
28use crate::policy_engine::StageRequest;
29use crate::random_id::random_uuid;
30use crate::repository_facts::RepositoryFacts;
31use crate::service_amend::{AmendSource, amended_author, read_amend_source};
32use crate::scratch_location::SCRATCH_REFRESH_EVERY;
33use crate::service_client::{Activation, ClientConfig, FreshScratch, START_WAIT, ScratchWatch, Submitted, manager_watch_for, submit_traced, watch_scratch};
34use crate::landed_summary::{rebuilt_note, short_name, subject_of, summary_line};
35use crate::service_commit_args::{ServiceCommitArgs, opens_editor, parse_service_commit, with_fixed_message};
36use crate::service_handoff::ServiceHandoff;
37use crate::service_land::LANDING_ATTEMPTS;
38use crate::service_message::{author_overrides, build_message, identity};
39use crate::service_messages::{ClientFailure, render};
40use crate::service_native::{BuildInput, GitBuilt, build_with_git};
41use crate::service_prepare::{NativeInputs, PrepareInputs, Prepared, Ready, prepare};
42use crate::service_replay::{Captured, Retry, conflict_result, retry_of};
43use crate::service_protocol::{CommitRequest, HEARTBEAT_SILENCE};
44use crate::service_provision::{platform_without_manager, provision};
45use crate::service_pruned::{content_was_removed, prepared_content_missing, reach_service_phase};
46use crate::transaction_phase::Phase;
47use crate::service_route::{Route, RouteInputs, choose_route, remembered_own_executable, worktree_git_dir};
48use crate::transaction_git::{GitRequest, run_git, run_git_checked};
49use crate::worktree_completion::install_added_worktree_files;
50use std::ffi::OsString;
51use std::io::{Read, Write};
52use std::path::PathBuf;
53use std::time::Instant;
54
55pub struct ServiceCommit {
57 pub result: TransactionResult,
59 pub handoff: Option<ServiceHandoff>,
61}
62
63pub const REBUILD_ATTEMPTS: usize = 5;
66
67fn branch_moved(ready: &Ready) -> TransactionStop {
69 let branch: String = ready.reference.trim_start_matches("refs/heads/").to_string();
70 return TransactionStop::Message(render(&ClientFailure::BranchMoved { branch }));
71}
72
73fn arguments_after_subcommand(arguments: &[OsString], subcommand_index: usize) -> &[OsString] {
85 return arguments.get(subcommand_index + 1..).unwrap_or(&[]);
86}
87
88fn switched_branch_name(reference: &str) -> String {
102 return if reference == "HEAD" { String::from("a detached head") } else { reference.trim_start_matches("refs/heads/").to_string() };
103}
104
105fn standard_input_for(invocation: &CommitInvocation<'_>) -> Result<Option<Vec<u8>>, TransactionStop> {
107 let after: &[OsString] = arguments_after_subcommand(invocation.arguments, invocation.subcommand_index);
108 let Some(value) = invocation.region.pathspec_from_file else { return Ok(None) };
109 if crate::command_options_query::value_bytes(after, value) != b"-" {
110 return Ok(None);
111 }
112 let mut bytes: Vec<u8> = Vec::new();
113 std::io::stdin().read_to_end(&mut bytes).map_err(|error| { return failed(format!("reading the pathspec file failed: {error}")); })?;
114 return Ok(Some(bytes));
115}
116
117fn repository_not_found(invocation: &CommitInvocation<'_>) -> TransactionStop {
128 let asked = run_git(invocation.context, &GitRequest::new(invocation.cwd, &["rev-parse", "--git-dir"]));
129 let output = match asked {
130 Ok(found) => found,
131 Err(error) => return failed(format!("asking Git which repository this directory belongs to failed: {error}")),
132 };
133 if output.succeeded() {
134 return failed(String::from("this directory belongs to a repository whose Git directory could not be located"));
135 }
136 let _reported = std::io::stderr().write_all(output.stderr.as_slice());
137 return TransactionStop::NativeExit(output.code.unwrap_or(1));
138}
139
140pub fn service_commit<F: RepositoryFacts>(
151 invocation: &CommitInvocation<'_>,
152 checks: &mut ShippedChecks<F>,
153 request: &StageRequest,
154 timing: &mut CommitTiming,
155) -> Result<ServiceCommit, TransactionStop> {
156 let path_variable = |name: &str| -> Option<PathBuf> { return std::env::var_os(name).filter(|value| { return !value.is_empty(); }).map(PathBuf::from); };
157 let runtime: Option<PathBuf> = path_variable("XDG_RUNTIME_DIR");
158 let config_home: Option<PathBuf> = path_variable("XDG_CONFIG_HOME");
159 let home: Option<PathBuf> = path_variable("HOME");
160 let bus_address: Option<String> = std::env::var("DBUS_SESSION_BUS_ADDRESS").ok();
161 let Some(git_dir) = worktree_git_dir(invocation.context, invocation.cwd) else { return Err(repository_not_found(invocation)) };
163 let route: Option<Route> = choose_route(&RouteInputs {
164 runtime_dir: runtime.as_deref(),
165 bus_address: bus_address.as_deref(),
166 config_home: config_home.as_deref(),
167 home: home.as_deref(),
168 own_executable: remembered_own_executable(),
169 real_git: invocation.context.real_git.as_path(),
170 git_dir: git_dir.as_path(),
171 });
172 let Some(Route { address, provision: provisioning }) = route else {
173 return Err(TransactionStop::Message(render(&ClientFailure::NoActivation(platform_without_manager()))));
174 };
175 timing.place_at(git_dir.as_path());
178 let after: &[OsString] = arguments_after_subcommand(invocation.arguments, invocation.subcommand_index);
179 let id: String = random_uuid().map_err(|error| { return failed(format!("an identity for this commit could not be made: {error:?}")); })?;
180 timing.request = Some(id.clone());
182 let standard_input: Option<Vec<u8>> = standard_input_for(invocation)?;
183 let mut provisioned: bool = false;
184 let editor_possible: bool = opens_editor(after);
186 let config: ClientConfig = ClientConfig {
187 address,
188 activation: Activation::Managed,
189 start_wait: START_WAIT,
190 heartbeat_silence: HEARTBEAT_SILENCE,
191 start_watch: provisioning.as_ref().map(manager_watch_for),
192 keep_fresh: None, answer_ceiling: crate::wait_ceilings::ANSWER_CEILING, withdraw_ceiling: crate::wait_ceilings::WITHDRAW_CEILING, extra_wait: crate::wait_ceilings::SHORT_EXTRA_WAIT, start_ceiling: crate::wait_ceilings::START_CEILING,
193 };
194 let scratch_name: String = format!("cli-git-service-{id}");
195 let mut embedded: Option<Embedded> = None;
199 let mut decided: bool = false;
200 let mut fixed_message: Option<PathBuf> = None;
202 let mut last_message: Option<String> = None;
204 let mut retry: Option<Captured> = None;
207 let mut first_reference: Option<String> = None;
209 let mut scratch_watch: Option<ScratchWatch> = None;
213 let mut unlanded: Vec<String> = Vec::new();
216 for attempt in 0..REBUILD_ATTEMPTS {
217 let inputs: PrepareInputs<'_> = PrepareInputs {
218 scratch_name: scratch_name.as_str(),
219 allow_empty: invocation.region.allow_empty,
220 standard_input: standard_input.as_deref(),
221 first: attempt == 0,
222 replay: retry.as_ref().map(|captured| { return Retry { captured, attempt: u64::try_from(attempt).unwrap_or(u64::MAX) }; }),
223 };
224 let preparing: Instant = Instant::now();
225 let prepared: Result<Prepared, TransactionStop> = prepare(invocation, checks, request, &inputs);
226 timing.prepare += preparing.elapsed();
227 timing.attempts += 1;
228 let ready: Box<Ready> = match prepared? {
229 Prepared::Conflict(conflict) => {
230 let saved: PathBuf = conflict.git_dir.join(format!("cli-git-saved-message-{}", id.get(..8).unwrap_or(id.as_str())));
232 let kept: Option<&PathBuf> = last_message.as_ref().and_then(|text| {
233 return match std::fs::write(&saved, text.as_str()) {
234 Ok(()) => Some(&saved),
235 Err(error) => {
236 warn("serviceCommit", format!("saving the message failed: {error}").as_str());
237 None
238 }
239 };
240 });
241 let result: TransactionResult = conflict_result(&conflict, kept.map(PathBuf::as_path));
242 return Ok(ServiceCommit { result, handoff: None });
243 }
244 Prepared::Ended(result) => {
245 if content_was_removed(result.events.as_slice()) {
248 return Err(TransactionStop::Message(render(&ClientFailure::ContentPruned)));
249 }
250 return Ok(ServiceCommit { result, handoff: None });
251 }
252 Prepared::Ready(ready) => ready,
253 };
254 let building: Instant = Instant::now();
257 if timing.reference.is_none() {
258 timing.reference = Some(ready.reference.clone());
259 }
260 let first_branch: &str = first_reference.get_or_insert_with(|| { return ready.reference.clone(); });
261 if first_branch != ready.reference {
262 let failure: ClientFailure = ClientFailure::BranchSwitched {
263 branch: switched_branch_name(first_branch),
264 now: switched_branch_name(ready.reference.as_str()),
265 file: fixed_message.as_ref().map(|file| { return file.display().to_string(); }),
266 };
267 return Err(TransactionStop::Message(render(&failure)));
268 }
269 if scratch_watch.is_none() {
270 scratch_watch = watch_scratch(&config, ready.inputs.capture.layout.git_dir.as_path(), ready.scratch.0.as_path());
271 }
272 if !decided {
273 decided = true;
274 if !ready.needs_native
275 && let Some(parsed) = parse_service_commit(after)
276 {
277 embedded = embedded_commit(invocation, parsed)?;
278 }
279 }
280 let native_inputs: &NativeInputs = &ready.inputs;
281 let mut tree: Option<String> = None;
286 if !ready.needs_native
287 && let Some(source) = embedded.as_ref().and_then(|made| { return made.source.as_ref(); })
288 {
289 if source.base != ready.base {
290 return Err(branch_moved(&ready));
291 }
292 let written = run_git_checked(
293 invocation.context,
294 &indexed_request(
295 invocation.cwd,
296 &["write-tree"],
297 Some(native_inputs.commit_index.as_path()),
298 Some(native_inputs.capture.layout.object_directory.as_path()),
299 ),
300 )?;
301 let made: String = String::from_utf8_lossy(written.stdout.as_slice()).trim().to_string();
302 let against: &str = source.parent_tree.as_deref().unwrap_or(native_inputs.capture.layout.empty_tree_oid.as_str());
303 if source.parents.len() < 2 && !invocation.region.allow_empty && made == against {
304 embedded = None;
305 } else {
306 tree = Some(made);
307 }
308 }
309 let native: bool = ready.needs_native || embedded.is_none();
310 let mut typed: Option<String> = None;
311 let (sent, first_parent): (CommitRequest, String) = match embedded.as_ref().filter(|_| { return !native; }) {
312 None => {
313 let layout_prefix: usize = crate::global_arguments::global_layout(native_inputs.arguments.as_slice()).prefix_len;
314 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); });
315 let built: GitBuilt = build_with_git(&BuildInput {
316 context: invocation.context,
317 cwd: invocation.cwd,
318 capture: &native_inputs.capture,
319 scratch: ready.scratch.0.as_path(),
320 commit_index: native_inputs.commit_index.as_path(),
321 arguments: rebuilt_arguments.as_deref().unwrap_or(native_inputs.arguments.as_slice()),
322 pathspecs: native_inputs.pathspecs.as_slice(),
323 mode: native_inputs.mode,
324 staged_all: native_inputs.staged_all,
325 selected_privately: native_inputs.selected_privately,
326 })?;
327 let parent: String = built.parents.first().cloned().unwrap_or_default();
328 typed = Some(built.message.clone());
329 let sent: CommitRequest = CommitRequest {
330 id: id.clone(),
331 repository: ready.repository.clone(),
332 common_dir: ready.common_dir.clone(),
333 git_dir: native_inputs.capture.layout.git_dir.clone(),
334 reference: ready.reference.clone(),
335 base: ready.base.clone(),
336 changes: Vec::new(),
338 message: String::new(),
340 author: None,
341 committer: String::new(),
342 amend: invocation.region.amend,
343 tree: None,
344 commit: Some(built.commit),
345 cleanup: ready.cleanup.clone(),
346 index: native_inputs.capture.layout.real_index_path.clone(),
347 captured: ready.captured_at,
348 };
349 (sent, parent)
350 }
351 Some(made) => {
352 let sent: CommitRequest = CommitRequest {
353 id: id.clone(),
354 repository: ready.repository.clone(),
355 common_dir: ready.common_dir.clone(),
356 git_dir: native_inputs.capture.layout.git_dir.clone(),
357 reference: ready.reference.clone(),
358 base: ready.base.clone(),
359 changes: ready.changes.clone().unwrap_or_default(),
360 message: made.message.clone(),
361 author: Some(made.author.clone()),
362 committer: made.committer.clone(),
363 amend: tree.is_some(),
364 tree: tree.clone(),
365 commit: None,
366 cleanup: None,
367 index: native_inputs.capture.layout.real_index_path.clone(),
368 captured: ready.captured_at,
369 };
370 let parent: String = match made.source.as_ref() {
372 Some(replaced) => replaced.parents.first().cloned().unwrap_or_default(),
373 None => ready.base.clone(),
374 };
375 (sent, parent)
376 }
377 };
378 last_message = typed.clone().or_else(|| { return embedded.as_ref().filter(|_| { return !native; }).map(|made| { return made.message.clone(); }); });
379 timing.build += building.elapsed();
381 reach_service_phase(Phase::ServicePrepared)?;
382 if let Some(host) = provisioning.as_ref().filter(|_| { return !provisioned; }) {
385 let provisioning_began: Instant = Instant::now();
386 let provided = provision(host);
387 timing.provision += provisioning_began.elapsed();
388 provided.map_err(|failure| { return TransactionStop::Message(render(&failure)); })?;
389 provisioned = true;
390 }
391 let waiting_config: ClientConfig = ClientConfig {
394 keep_fresh: Some(FreshScratch { path: ready.scratch.0.clone(), every: SCRATCH_REFRESH_EVERY }),
395 ..config.clone()
396 };
397 match submit_traced(&waiting_config, &sent, &mut std::io::stderr(), &mut timing.submit) {
398 Ok(Submitted::Landed { commit, .. }) => {
399 let after_landing: Instant = Instant::now();
400 if let Some(file) = fixed_message.as_ref() {
401 let _removed = std::fs::remove_file(file);
403 }
404 if let Some(made) = embedded.as_ref().filter(|_| { return !native; }).filter(|made| { return !made.quiet; }) {
408 let name: String = short_name(
409 invocation.context,
410 invocation.cwd,
411 native_inputs.capture.layout.object_directory.as_path(),
412 native_inputs.capture.layout.config_snapshot.as_ref(),
413 commit.as_str(),
414 );
415 print_summary(summary_line(ready.reference.as_str(), first_parent.as_str(), name.as_str(), subject_of(made.message.as_str()).as_str()).as_str());
416 }
417 if native && !unlanded.is_empty() {
419 let abbreviated = |full: &str| -> String {
420 return short_name(
421 invocation.context,
422 invocation.cwd,
423 native_inputs.capture.layout.object_directory.as_path(),
424 native_inputs.capture.layout.config_snapshot.as_ref(),
425 full,
426 );
427 };
428 let lost: Vec<String> = unlanded.iter().map(|full| { return abbreviated(full.as_str()); }).collect();
429 print_note(rebuilt_note(lost.as_slice(), abbreviated(commit.as_str()).as_str()).as_str());
430 }
431 for path in paths_with_markers(invocation.context, ready.repository.as_path(), commit.as_str(), ready.committed_conflicted.as_slice()) {
435 let name: String = String::from_utf8_lossy(path.as_slice()).into_owned();
436 let _warned = writeln!(
438 std::io::stderr(),
439 "cli-git: warning: {name} still has conflict markers, and they were committed. To fix it, remove the \
440 markers from {name} and make a follow-up commit: git commit -m \"<message>\" -- {name}"
441 );
442 }
443 if let Some(kind) = ready.leaves_open {
445 let _noted = write!(std::io::stderr(), "{}", open_operation_line(kind == Conclusion::Merge));
446 }
447 install_added_worktree_files(
449 invocation.context,
450 invocation.cwd,
451 ready.repository.as_path(),
452 ready.worktree_records.as_slice(),
453 Some(native_inputs.capture.layout.object_directory.as_path()),
454 )
455 .map_err(|error| { return TransactionStop::Message(error.0); })?;
456 let handoff: ServiceHandoff = ServiceHandoff {
457 config: config.clone(),
458 repository: ready.repository.clone(),
459 git_dir: native_inputs.capture.layout.git_dir.clone(),
460 reference: ready.reference.clone(),
461 parent: first_parent,
462 };
463 let result: TransactionResult = TransactionResult { events: ready.events, exit_code: 0, landed_oid: Some(commit), uses_lfs: ready.uses_lfs };
464 timing.after_landing += after_landing.elapsed();
465 return Ok(ServiceCommit { result, handoff: Some(handoff) });
466 }
467 Ok(Submitted::PrepareAgain { landed, .. }) => {
468 if native || sent.amend {
469 return Err(branch_moved(&ready));
471 }
472 if attempt + 1 == LANDING_ATTEMPTS {
473 let branch: String = ready.reference.trim_start_matches("refs/heads/").to_string();
474 return Err(TransactionStop::Message(render(&ClientFailure::BranchKeepsChanging { branch, times: LANDING_ATTEMPTS })));
475 }
476 retry = Some(retry_of(invocation.context, invocation.cwd, &ready, landed)?);
478 }
479 Ok(Submitted::Moved) => {
480 let branch: String = ready.reference.trim_start_matches("refs/heads/").to_string();
481 if native && !sent.amend && !editor_possible && !ready.interactive && !ready.fixed_parents {
484 if attempt + 1 == REBUILD_ATTEMPTS {
485 return Err(TransactionStop::Message(render(&ClientFailure::BranchKeepsChanging { branch, times: REBUILD_ATTEMPTS })));
486 }
487 retry = Some(retry_of(invocation.context, invocation.cwd, &ready, Vec::new())?);
489 unlanded.extend(sent.commit.clone());
490 continue;
491 }
492 if native && editor_possible && let Some(text) = typed.as_deref() {
493 let file: PathBuf = native_inputs.capture.layout.git_dir.join(format!("cli-git-saved-message-{}", id.get(..8).unwrap_or(id.as_str())));
495 match std::fs::write(&file, text) {
496 Ok(()) => {
497 let prefix: usize = crate::global_arguments::global_layout(native_inputs.arguments.as_slice()).prefix_len;
500 let repeatable: bool = !sent.amend
501 && !ready.interactive
502 && !ready.fixed_parents
503 && with_fixed_message(native_inputs.arguments.as_slice(), prefix, file.as_path()).is_some();
504 if repeatable && attempt + 1 < REBUILD_ATTEMPTS {
505 fixed_message = Some(file);
506 retry = Some(retry_of(invocation.context, invocation.cwd, &ready, Vec::new())?);
507 unlanded.extend(sent.commit.clone());
508 continue;
509 }
510 let failure: ClientFailure = ClientFailure::BranchMovedMessageSaved { branch, file: file.display().to_string(), amend: sent.amend };
511 return Err(TransactionStop::Message(render(&failure)));
512 }
513 Err(error) => warn("serviceCommit", format!("saving the typed message failed: {error}").as_str()),
514 }
515 }
516 return Err(branch_moved(&ready));
517 }
518 Err(ClientFailure::Unavailable)
519 if prepared_content_missing(
520 native_inputs.capture.layout.common_dir.as_path(),
521 native_inputs.capture.layout.object_directory.as_path(),
522 native_inputs.capture.layout.object_format.as_str(),
523 &sent,
524 ) =>
525 {
526 return Err(TransactionStop::Message(render(&ClientFailure::ContentPruned)));
529 }
530 Err(failure) => return Err(TransactionStop::Message(render(&failure))),
531 }
532 }
533 return Err(TransactionStop::Message(render(&ClientFailure::BranchKeepsChanging { branch: String::from("the branch"), times: REBUILD_ATTEMPTS })));
534}
535
536fn print_summary(line: &str) {
547 let mut out = std::io::stdout();
548 let written = writeln!(out, "{line}").and_then(|()| { return out.flush(); });
549 let Err(error) = written else { return };
550 if error.kind() == std::io::ErrorKind::BrokenPipe {
551 unsafe {
553 libc::signal(libc::SIGPIPE, libc::SIG_DFL);
554 libc::raise(libc::SIGPIPE);
555 }
556 }
557 let _reported = writeln!(std::io::stderr(), "fatal: write error: {error}");
558 std::process::exit(128);
559}
560
561fn print_note(line: &str) {
572 if let Err(error) = writeln!(std::io::stderr(), "{line}") {
573 warn("serviceCommit", format!("writing the note about the rebuilt commit failed: {error}").as_str());
574 }
575}
576
577struct Embedded {
579 message: String,
580 author: String,
581 committer: String,
582 source: Option<AmendSource>,
583 quiet: bool,
584}
585
586fn embedded_commit(invocation: &CommitInvocation<'_>, parsed: ServiceCommitArgs) -> Result<Option<Embedded>, TransactionStop> {
597 let Some(committer) = identity(invocation.context, invocation.cwd, "GIT_COMMITTER_IDENT", &[])? else { return Ok(None) };
598 let source: Option<AmendSource> = if parsed.amend {
599 let Some(read) = read_amend_source(invocation.context, invocation.cwd)? else { return Ok(None) };
600 Some(read)
601 } else {
602 None
603 };
604 let previous: Option<&str> = source.as_ref().map(|found| { return found.message.as_str(); });
605 let Some(built) = build_message(invocation, &parsed, committer.as_str(), previous)? else { return Ok(None) };
606 let author: String = match built.reused_author {
607 Some(reused) => reused,
608 None => match source.as_ref() {
609 Some(replaced) => {
610 let Some(made) = amended_author(invocation.context, invocation.cwd, replaced, &parsed)? else { return Ok(None) };
611 made
612 }
613 None => {
614 let Some(made) = identity(invocation.context, invocation.cwd, "GIT_AUTHOR_IDENT", author_overrides(&parsed).as_slice())? else {
615 return Ok(None);
616 };
617 made
618 }
619 },
620 };
621 return Ok(Some(Embedded { message: built.message, author, committer, source, quiet: parsed.quiet }));
622}
623
624#[cfg(test)]
626#[path = "service_commit_tests.rs"]
627mod tests;