1use super::private_index_request::indexed_request;
20use super::commit_transaction::{CommitInvocation, TransactionResult, TransactionStop, failed};
21use super::diagnostic_log::warn;
22use super::policy_checks::ShippedChecks;
23use super::policy_engine::StageRequest;
24use super::random_id::random_uuid;
25use super::repository_facts::RepositoryFacts;
26use super::service_amend::{AmendSource, amended_author, read_amend_source};
27use super::service_client::{Activation, COMMIT_WAIT, ClientConfig, START_WAIT, ScratchWatch, Submitted, submit, watch_scratch};
28use super::landed_summary::{change_summary, short_name, subject_of, summary_line};
29use super::service_commit_args::{ServiceCommitArgs, asks_for_quiet, opens_editor, parse_service_commit, with_fixed_message};
30use super::service_handoff::ServiceHandoff;
31use super::service_land::LANDING_ATTEMPTS;
32use super::service_message::{author_overrides, build_message, identity};
33use super::service_messages::{ClientFailure, render};
34use super::service_native::{BuildInput, GitBuilt, build_with_git};
35use super::service_prepare::{NativeInputs, PrepareInputs, Prepared, Ready, prepare};
36use super::service_replay::{Captured, Retry, conflict_result, retry_of};
37use super::service_protocol::CommitRequest;
38use super::service_provision::{platform_without_manager, provision};
39use super::service_pruned::{content_was_removed, prepared_content_missing, reach_service_phase};
40use super::transaction_phase::Phase;
41use super::service_route::{Route, RouteInputs, choose_route, remembered_own_executable, worktree_git_dir};
42use super::transaction_git::{GitRequest, run_git, run_git_checked};
43use super::worktree_completion::install_added_worktree_files;
44use std::ffi::OsString;
45use std::io::{Read, Write};
46use std::path::PathBuf;
47
48pub struct ServiceCommit {
50 pub result: TransactionResult,
52 pub handoff: Option<ServiceHandoff>,
54}
55
56pub const REBUILD_ATTEMPTS: usize = 5;
59
60fn branch_moved(ready: &Ready) -> TransactionStop {
62 let branch: String = ready.reference.trim_start_matches("refs/heads/").to_string();
63 return TransactionStop::Message(render(&ClientFailure::BranchMoved { branch }));
64}
65
66fn standard_input_for(invocation: &CommitInvocation<'_>) -> Result<Option<Vec<u8>>, TransactionStop> {
68 let after: &[OsString] = invocation.arguments.get(invocation.subcommand_index + 1..).unwrap_or(&[]);
69 let Some(value) = invocation.region.pathspec_from_file else { return Ok(None) };
70 if super::command_options_query::value_bytes(after, value) != b"-" {
71 return Ok(None);
72 }
73 let mut bytes: Vec<u8> = Vec::new();
74 std::io::stdin().read_to_end(&mut bytes).map_err(|error| { return failed(format!("reading the pathspec file failed: {error}")); })?;
75 return Ok(Some(bytes));
76}
77
78fn repository_not_found(invocation: &CommitInvocation<'_>) -> TransactionStop {
89 let asked = run_git(invocation.context, &GitRequest::new(invocation.cwd, &["rev-parse", "--git-dir"]));
90 let output = match asked {
91 Ok(found) => found,
92 Err(error) => return failed(format!("asking Git which repository this directory belongs to failed: {error}")),
93 };
94 if output.succeeded() {
95 return failed(String::from("this directory belongs to a repository whose Git directory could not be located"));
96 }
97 let _reported = std::io::stderr().write_all(output.stderr.as_slice());
98 return TransactionStop::NativeExit(output.code.unwrap_or(1));
99}
100
101pub fn service_commit<F: RepositoryFacts>(
112 invocation: &CommitInvocation<'_>,
113 checks: &mut ShippedChecks<F>,
114 request: &StageRequest,
115) -> Result<ServiceCommit, TransactionStop> {
116 let path_variable = |name: &str| -> Option<PathBuf> { return std::env::var_os(name).filter(|value| { return !value.is_empty(); }).map(PathBuf::from); };
117 let runtime: Option<PathBuf> = path_variable("XDG_RUNTIME_DIR");
118 let config_home: Option<PathBuf> = path_variable("XDG_CONFIG_HOME");
119 let home: Option<PathBuf> = path_variable("HOME");
120 let bus_address: Option<String> = std::env::var("DBUS_SESSION_BUS_ADDRESS").ok();
121 let Some(git_dir) = worktree_git_dir(invocation.context, invocation.cwd) else { return Err(repository_not_found(invocation)) };
123 let route: Option<Route> = choose_route(&RouteInputs {
124 runtime_dir: runtime.as_deref(),
125 bus_address: bus_address.as_deref(),
126 config_home: config_home.as_deref(),
127 home: home.as_deref(),
128 own_executable: remembered_own_executable(),
129 real_git: invocation.context.real_git.as_path(),
130 git_dir: git_dir.as_path(),
131 });
132 let Some(Route { address, provision: provisioning }) = route else {
133 return Err(TransactionStop::Message(render(&ClientFailure::NoActivation(platform_without_manager()))));
134 };
135 let after: &[OsString] = invocation.arguments.get(invocation.subcommand_index + 1..).unwrap_or(&[]);
136 let id: String = random_uuid().map_err(|error| { return failed(format!("an identity for this commit could not be made: {error:?}")); })?;
137 let standard_input: Option<Vec<u8>> = standard_input_for(invocation)?;
138 let mut provisioned: bool = false;
139 let editor_possible: bool = opens_editor(after);
141 let config: ClientConfig = ClientConfig { address, activation: Activation::Managed, start_wait: START_WAIT, commit_wait: COMMIT_WAIT };
142 let scratch_name: String = format!("cli-git-service-{id}");
143 let mut embedded: Option<Embedded> = None;
147 let mut decided: bool = false;
148 let mut fixed_message: Option<PathBuf> = None;
150 let mut last_message: Option<String> = None;
152 let mut retry: Option<Captured> = None;
155 let mut first_reference: Option<String> = None;
157 let mut scratch_watch: Option<ScratchWatch> = None;
161 for attempt in 0..REBUILD_ATTEMPTS {
162 let inputs: PrepareInputs<'_> = PrepareInputs {
163 scratch_name: scratch_name.as_str(),
164 allow_empty: invocation.region.allow_empty,
165 standard_input: standard_input.as_deref(),
166 first: attempt == 0,
167 replay: retry.as_ref().map(|captured| { return Retry { captured, attempt: u64::try_from(attempt).unwrap_or(u64::MAX) }; }),
168 };
169 let ready: Box<Ready> = match prepare(invocation, checks, request, &inputs)? {
170 Prepared::Conflict(conflict) => {
171 let saved: PathBuf = conflict.git_dir.join(format!("cli-git-saved-message-{}", id.get(..8).unwrap_or(id.as_str())));
173 let kept: Option<&PathBuf> = last_message.as_ref().and_then(|text| {
174 return match std::fs::write(&saved, text.as_str()) {
175 Ok(()) => Some(&saved),
176 Err(error) => {
177 warn("serviceCommit", format!("saving the message failed: {error}").as_str());
178 None
179 }
180 };
181 });
182 let result: TransactionResult = conflict_result(&conflict, kept.map(PathBuf::as_path));
183 return Ok(ServiceCommit { result, handoff: None });
184 }
185 Prepared::Ended(result) => {
186 if content_was_removed(result.events.as_slice()) {
189 return Err(TransactionStop::Message(render(&ClientFailure::ContentPruned)));
190 }
191 return Ok(ServiceCommit { result, handoff: None });
192 }
193 Prepared::Ready(ready) => ready,
194 };
195 let first_branch: &str = first_reference.get_or_insert_with(|| { return ready.reference.clone(); });
198 if first_branch != ready.reference {
199 let named = |reference: &str| -> String { return if reference == "HEAD" { String::from("a detached head") } else { reference.trim_start_matches("refs/heads/").to_string() }; };
200 let failure: ClientFailure = ClientFailure::BranchSwitched {
201 branch: named(first_branch),
202 now: named(ready.reference.as_str()),
203 file: fixed_message.as_ref().map(|file| { return file.display().to_string(); }),
204 };
205 return Err(TransactionStop::Message(render(&failure)));
206 }
207 if scratch_watch.is_none() {
208 scratch_watch = watch_scratch(&config, ready.inputs.capture.layout.git_dir.as_path(), ready.scratch.0.as_path());
209 }
210 if !decided {
211 decided = true;
212 if !ready.needs_native
213 && let Some(parsed) = parse_service_commit(after)
214 {
215 embedded = embedded_commit(invocation, parsed)?;
216 }
217 }
218 let native_inputs: &NativeInputs = &ready.inputs;
219 let mut tree: Option<String> = None;
224 if !ready.needs_native
225 && let Some(source) = embedded.as_ref().and_then(|made| { return made.source.as_ref(); })
226 {
227 if source.base != ready.base {
228 return Err(branch_moved(&ready));
229 }
230 let written = run_git_checked(
231 invocation.context,
232 &indexed_request(
233 invocation.cwd,
234 &["write-tree"],
235 Some(native_inputs.commit_index.as_path()),
236 Some(native_inputs.capture.layout.object_directory.as_path()),
237 ),
238 )?;
239 let made: String = String::from_utf8_lossy(written.stdout.as_slice()).trim().to_string();
240 let against: &str = source.parent_tree.as_deref().unwrap_or(native_inputs.capture.layout.empty_tree_oid.as_str());
241 if source.parents.len() < 2 && !invocation.region.allow_empty && made == against {
242 embedded = None;
243 } else {
244 tree = Some(made);
245 }
246 }
247 let native: bool = ready.needs_native || embedded.is_none();
248 let mut typed: Option<String> = None;
249 let (sent, first_parent): (CommitRequest, String) = match embedded.as_ref().filter(|_| { return !native; }) {
250 None => {
251 let layout_prefix: usize = super::global_arguments::global_layout(native_inputs.arguments.as_slice()).prefix_len;
252 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); });
253 let built: GitBuilt = build_with_git(&BuildInput {
254 context: invocation.context,
255 cwd: invocation.cwd,
256 capture: &native_inputs.capture,
257 scratch: ready.scratch.0.as_path(),
258 commit_index: native_inputs.commit_index.as_path(),
259 arguments: rebuilt_arguments.as_deref().unwrap_or(native_inputs.arguments.as_slice()),
260 pathspecs: native_inputs.pathspecs.as_slice(),
261 mode: native_inputs.mode,
262 staged_all: native_inputs.staged_all,
263 selected_privately: native_inputs.selected_privately,
264 quiet: true,
266 })?;
267 let parent: String = built.parents.first().cloned().unwrap_or_default();
268 typed = Some(built.message.clone());
269 let sent: CommitRequest = CommitRequest {
270 id: id.clone(),
271 repository: ready.repository.clone(),
272 common_dir: ready.common_dir.clone(),
273 git_dir: native_inputs.capture.layout.git_dir.clone(),
274 reference: ready.reference.clone(),
275 base: ready.base.clone(),
276 changes: Vec::new(),
278 message: String::new(),
280 author: None,
281 committer: String::new(),
282 amend: invocation.region.amend,
283 tree: None,
284 commit: Some(built.commit),
285 cleanup: ready.cleanup.clone(),
286 index: native_inputs.capture.layout.real_index_path.clone(),
287 captured: ready.captured_at,
288 };
289 (sent, parent)
290 }
291 Some(made) => {
292 let sent: CommitRequest = CommitRequest {
293 id: id.clone(),
294 repository: ready.repository.clone(),
295 common_dir: ready.common_dir.clone(),
296 git_dir: native_inputs.capture.layout.git_dir.clone(),
297 reference: ready.reference.clone(),
298 base: ready.base.clone(),
299 changes: ready.changes.clone().unwrap_or_default(),
300 message: made.message.clone(),
301 author: Some(made.author.clone()),
302 committer: made.committer.clone(),
303 amend: tree.is_some(),
304 tree: tree.clone(),
305 commit: None,
306 cleanup: None,
307 index: native_inputs.capture.layout.real_index_path.clone(),
308 captured: ready.captured_at,
309 };
310 let parent: String = match made.source.as_ref() {
312 Some(replaced) => replaced.parents.first().cloned().unwrap_or_default(),
313 None => ready.base.clone(),
314 };
315 (sent, parent)
316 }
317 };
318 last_message = typed.clone().or_else(|| { return embedded.as_ref().filter(|_| { return !native; }).map(|made| { return made.message.clone(); }); });
319 reach_service_phase(Phase::ServicePrepared)?;
321 if let Some(host) = provisioning.as_ref().filter(|_| { return !provisioned; }) {
324 provision(host).map_err(|failure| { return TransactionStop::Message(render(&failure)); })?;
325 provisioned = true;
326 }
327 match submit(&config, &sent, &mut std::io::stderr()) {
328 Ok(Submitted::Landed { commit, .. }) => {
329 if let Some(file) = fixed_message.as_ref() {
330 let _removed = std::fs::remove_file(file);
332 }
333 let embedded_made: Option<&Embedded> = embedded.as_ref().filter(|_| { return !native; });
336 let quiet: bool = embedded_made.map_or_else(|| { return asks_for_quiet(after); }, |made| { return made.quiet; });
337 if !quiet {
338 let message: &str = embedded_made.map(|made| { return made.message.as_str(); }).or(typed.as_deref()).unwrap_or("");
339 let name: String = short_name(
340 invocation.context,
341 invocation.cwd,
342 native_inputs.capture.layout.object_directory.as_path(),
343 native_inputs.capture.layout.config_snapshot.as_ref(),
344 commit.as_str(),
345 );
346 print_summary(summary_line(ready.reference.as_str(), first_parent.as_str(), name.as_str(), subject_of(message).as_str()).as_str());
347 if embedded_made.is_none() {
348 let changes: String = change_summary(invocation.context, invocation.cwd, first_parent.as_str(), commit.as_str());
350 if !changes.is_empty() {
351 print_summary(changes.as_str());
352 }
353 }
354 }
355 install_added_worktree_files(
357 invocation.context,
358 invocation.cwd,
359 ready.repository.as_path(),
360 ready.worktree_records.as_slice(),
361 Some(native_inputs.capture.layout.object_directory.as_path()),
362 )
363 .map_err(|error| { return TransactionStop::Message(error.0); })?;
364 let handoff: ServiceHandoff = ServiceHandoff {
365 config: config.clone(),
366 repository: ready.repository.clone(),
367 git_dir: native_inputs.capture.layout.git_dir.clone(),
368 reference: ready.reference.clone(),
369 parent: first_parent,
370 };
371 let result: TransactionResult = TransactionResult { events: ready.events, exit_code: 0, landed_oid: Some(commit), uses_lfs: ready.uses_lfs };
372 return Ok(ServiceCommit { result, handoff: Some(handoff) });
373 }
374 Ok(Submitted::PrepareAgain { landed, .. }) => {
375 if native || sent.amend {
376 return Err(branch_moved(&ready));
378 }
379 if attempt + 1 == LANDING_ATTEMPTS {
380 let branch: String = ready.reference.trim_start_matches("refs/heads/").to_string();
381 return Err(TransactionStop::Message(render(&ClientFailure::BranchKeepsChanging { branch, times: LANDING_ATTEMPTS })));
382 }
383 retry = Some(retry_of(invocation.context, invocation.cwd, &ready, landed)?);
385 }
386 Ok(Submitted::Moved) => {
387 let branch: String = ready.reference.trim_start_matches("refs/heads/").to_string();
388 if native && !sent.amend && !editor_possible && !ready.interactive && !ready.fixed_parents {
391 if attempt + 1 == REBUILD_ATTEMPTS {
392 return Err(TransactionStop::Message(render(&ClientFailure::BranchKeepsChanging { branch, times: REBUILD_ATTEMPTS })));
393 }
394 retry = Some(retry_of(invocation.context, invocation.cwd, &ready, Vec::new())?);
396 continue;
397 }
398 if native && editor_possible && let Some(text) = typed.as_deref() {
399 let file: PathBuf = native_inputs.capture.layout.git_dir.join(format!("cli-git-saved-message-{}", id.get(..8).unwrap_or(id.as_str())));
401 match std::fs::write(&file, text) {
402 Ok(()) => {
403 let prefix: usize = super::global_arguments::global_layout(native_inputs.arguments.as_slice()).prefix_len;
406 let repeatable: bool = !sent.amend
407 && !ready.interactive
408 && !ready.fixed_parents
409 && with_fixed_message(native_inputs.arguments.as_slice(), prefix, file.as_path()).is_some();
410 if repeatable && attempt + 1 < REBUILD_ATTEMPTS {
411 fixed_message = Some(file);
412 retry = Some(retry_of(invocation.context, invocation.cwd, &ready, Vec::new())?);
413 continue;
414 }
415 let failure: ClientFailure = ClientFailure::BranchMovedMessageSaved { branch, file: file.display().to_string(), amend: sent.amend };
416 return Err(TransactionStop::Message(render(&failure)));
417 }
418 Err(error) => warn("serviceCommit", format!("saving the typed message failed: {error}").as_str()),
419 }
420 }
421 return Err(branch_moved(&ready));
422 }
423 Err(ClientFailure::Unavailable)
424 if prepared_content_missing(
425 native_inputs.capture.layout.common_dir.as_path(),
426 native_inputs.capture.layout.object_directory.as_path(),
427 native_inputs.capture.layout.object_format.as_str(),
428 &sent,
429 ) =>
430 {
431 return Err(TransactionStop::Message(render(&ClientFailure::ContentPruned)));
434 }
435 Err(failure) => return Err(TransactionStop::Message(render(&failure))),
436 }
437 }
438 return Err(TransactionStop::Message(render(&ClientFailure::BranchKeepsChanging { branch: String::from("the branch"), times: REBUILD_ATTEMPTS })));
439}
440
441fn print_summary(line: &str) {
452 let mut out = std::io::stdout();
453 let written = writeln!(out, "{line}").and_then(|()| { return out.flush(); });
454 let Err(error) = written else { return };
455 if error.kind() == std::io::ErrorKind::BrokenPipe {
456 unsafe {
458 libc::signal(libc::SIGPIPE, libc::SIG_DFL);
459 libc::raise(libc::SIGPIPE);
460 }
461 }
462 let _reported = writeln!(std::io::stderr(), "fatal: write error: {error}");
463 std::process::exit(128);
464}
465
466struct Embedded {
468 message: String,
469 author: String,
470 committer: String,
471 source: Option<AmendSource>,
472 quiet: bool,
473}
474
475fn embedded_commit(invocation: &CommitInvocation<'_>, parsed: ServiceCommitArgs) -> Result<Option<Embedded>, TransactionStop> {
486 let Some(committer) = identity(invocation.context, invocation.cwd, "GIT_COMMITTER_IDENT", &[])? else { return Ok(None) };
487 let source: Option<AmendSource> = if parsed.amend {
488 let Some(read) = read_amend_source(invocation.context, invocation.cwd)? else { return Ok(None) };
489 Some(read)
490 } else {
491 None
492 };
493 let previous: Option<&str> = source.as_ref().map(|found| { return found.message.as_str(); });
494 let Some(built) = build_message(invocation, &parsed, committer.as_str(), previous)? else { return Ok(None) };
495 let author: String = match built.reused_author {
496 Some(reused) => reused,
497 None => match source.as_ref() {
498 Some(replaced) => {
499 let Some(made) = amended_author(invocation.context, invocation.cwd, replaced, &parsed)? else { return Ok(None) };
500 made
501 }
502 None => {
503 let Some(made) = identity(invocation.context, invocation.cwd, "GIT_AUTHOR_IDENT", author_overrides(&parsed).as_slice())? else {
504 return Ok(None);
505 };
506 made
507 }
508 },
509 };
510 return Ok(Some(Embedded { message: built.message, author, committer, source, quiet: parsed.quiet }));
511}
512
513#[cfg(test)]
515#[path = "service_commit_tests.rs"]
516mod tests;