1use super::capture_order::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::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::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
78pub fn try_service_commit<F: RepositoryFacts>(
89 invocation: &CommitInvocation<'_>,
90 checks: &mut ShippedChecks<F>,
91 request: &StageRequest,
92) -> Result<Option<ServiceCommit>, TransactionStop> {
93 let path_variable = |name: &str| -> Option<PathBuf> { return std::env::var_os(name).filter(|value| { return !value.is_empty(); }).map(PathBuf::from); };
94 let runtime: Option<PathBuf> = path_variable("XDG_RUNTIME_DIR");
95 let config_home: Option<PathBuf> = path_variable("XDG_CONFIG_HOME");
96 let home: Option<PathBuf> = path_variable("HOME");
97 let bus_address: Option<String> = std::env::var("DBUS_SESSION_BUS_ADDRESS").ok();
98 let Some(git_dir) = worktree_git_dir(invocation.context, invocation.cwd) else { return Ok(None) };
100 let route: Route = choose_route(&RouteInputs {
101 runtime_dir: runtime.as_deref(),
102 bus_address: bus_address.as_deref(),
103 config_home: config_home.as_deref(),
104 home: home.as_deref(),
105 own_executable: remembered_own_executable(),
106 real_git: invocation.context.real_git.as_path(),
107 git_dir: git_dir.as_path(),
108 });
109 let Route::Service { address, provision: provisioning } = route else { return Ok(None) };
110 let after: &[OsString] = invocation.arguments.get(invocation.subcommand_index + 1..).unwrap_or(&[]);
111 let id: String = random_uuid().map_err(|error| { return failed(format!("an identity for this commit could not be made: {error:?}")); })?;
112 let standard_input: Option<Vec<u8>> = standard_input_for(invocation)?;
113 let mut provisioned: bool = false;
114 let editor_possible: bool = opens_editor(after);
116 let config: ClientConfig = ClientConfig { address, activation: Activation::Managed, start_wait: START_WAIT, commit_wait: COMMIT_WAIT };
117 let scratch_name: String = format!("cli-git-service-{id}");
118 let mut embedded: Option<Embedded> = None;
122 let mut decided: bool = false;
123 let mut fixed_message: Option<PathBuf> = None;
125 let mut last_message: Option<String> = None;
127 let mut retry: Option<Captured> = None;
130 let mut first_reference: Option<String> = None;
132 let mut scratch_watch: Option<ScratchWatch> = None;
136 for attempt in 0..REBUILD_ATTEMPTS {
137 let inputs: PrepareInputs<'_> = PrepareInputs {
138 scratch_name: scratch_name.as_str(),
139 allow_empty: invocation.region.allow_empty,
140 standard_input: standard_input.as_deref(),
141 first: attempt == 0,
142 replay: retry.as_ref().map(|captured| { return Retry { captured, attempt: u64::try_from(attempt).unwrap_or(u64::MAX) }; }),
143 };
144 let ready: Box<Ready> = match prepare(invocation, checks, request, &inputs)? {
145 Prepared::Conflict(conflict) => {
146 let saved: PathBuf = conflict.git_dir.join(format!("cli-git-saved-message-{}", id.get(..8).unwrap_or(id.as_str())));
148 let kept: Option<&PathBuf> = last_message.as_ref().and_then(|text| {
149 return match std::fs::write(&saved, text.as_str()) {
150 Ok(()) => Some(&saved),
151 Err(error) => {
152 warn("serviceCommit", format!("saving the message failed: {error}").as_str());
153 None
154 }
155 };
156 });
157 let result: TransactionResult = conflict_result(&conflict, kept.map(PathBuf::as_path));
158 return Ok(Some(ServiceCommit { result, handoff: None }));
159 }
160 Prepared::Ended(result) => {
161 if content_was_removed(result.events.as_slice()) {
164 return Err(TransactionStop::Message(render(&ClientFailure::ContentPruned)));
165 }
166 return Ok(Some(ServiceCommit { result, handoff: None }));
167 }
168 Prepared::Ready(ready) => ready,
169 };
170 let first_branch: &str = first_reference.get_or_insert_with(|| { return ready.reference.clone(); });
173 if first_branch != ready.reference {
174 let named = |reference: &str| -> String { return if reference == "HEAD" { String::from("a detached head") } else { reference.trim_start_matches("refs/heads/").to_string() }; };
175 let failure: ClientFailure = ClientFailure::BranchSwitched {
176 branch: named(first_branch),
177 now: named(ready.reference.as_str()),
178 file: fixed_message.as_ref().map(|file| { return file.display().to_string(); }),
179 };
180 return Err(TransactionStop::Message(render(&failure)));
181 }
182 if scratch_watch.is_none() {
183 scratch_watch = watch_scratch(&config, ready.inputs.capture.layout.git_dir.as_path(), ready.scratch.0.as_path());
184 }
185 if !decided {
186 decided = true;
187 if !ready.needs_native
188 && let Some(parsed) = parse_service_commit(after)
189 {
190 embedded = embedded_commit(invocation, parsed)?;
191 }
192 }
193 let native_inputs: &NativeInputs = &ready.inputs;
194 let mut tree: Option<String> = None;
199 if !ready.needs_native
200 && let Some(source) = embedded.as_ref().and_then(|made| { return made.source.as_ref(); })
201 {
202 if source.base != ready.base {
203 return Err(branch_moved(&ready));
204 }
205 let written = run_git_checked(
206 invocation.context,
207 &indexed_request(
208 invocation.cwd,
209 &["write-tree"],
210 Some(native_inputs.commit_index.as_path()),
211 Some(native_inputs.capture.layout.object_directory.as_path()),
212 ),
213 )?;
214 let made: String = String::from_utf8_lossy(written.stdout.as_slice()).trim().to_string();
215 let against: &str = source.parent_tree.as_deref().unwrap_or(native_inputs.capture.layout.empty_tree_oid.as_str());
216 if source.parents.len() < 2 && !invocation.region.allow_empty && made == against {
217 embedded = None;
218 } else {
219 tree = Some(made);
220 }
221 }
222 let native: bool = ready.needs_native || embedded.is_none();
223 let mut typed: Option<String> = None;
224 let (sent, first_parent): (CommitRequest, String) = match embedded.as_ref().filter(|_| { return !native; }) {
225 None => {
226 let layout_prefix: usize = super::global_arguments::global_layout(native_inputs.arguments.as_slice()).prefix_len;
227 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); });
228 let built: GitBuilt = build_with_git(&BuildInput {
229 context: invocation.context,
230 cwd: invocation.cwd,
231 capture: &native_inputs.capture,
232 scratch: ready.scratch.0.as_path(),
233 commit_index: native_inputs.commit_index.as_path(),
234 arguments: rebuilt_arguments.as_deref().unwrap_or(native_inputs.arguments.as_slice()),
235 pathspecs: native_inputs.pathspecs.as_slice(),
236 mode: native_inputs.mode,
237 staged_all: native_inputs.staged_all,
238 selected_privately: native_inputs.selected_privately,
239 quiet: true,
241 })?;
242 let parent: String = built.parents.first().cloned().unwrap_or_default();
243 typed = Some(built.message.clone());
244 let sent: CommitRequest = CommitRequest {
245 id: id.clone(),
246 repository: ready.repository.clone(),
247 common_dir: ready.common_dir.clone(),
248 git_dir: native_inputs.capture.layout.git_dir.clone(),
249 reference: ready.reference.clone(),
250 base: ready.base.clone(),
251 changes: Vec::new(),
253 message: String::new(),
255 author: None,
256 committer: String::new(),
257 amend: invocation.region.amend,
258 tree: None,
259 commit: Some(built.commit),
260 cleanup: ready.cleanup.clone(),
261 index: native_inputs.capture.layout.real_index_path.clone(),
262 captured: ready.captured_at,
263 };
264 (sent, parent)
265 }
266 Some(made) => {
267 let sent: CommitRequest = CommitRequest {
268 id: id.clone(),
269 repository: ready.repository.clone(),
270 common_dir: ready.common_dir.clone(),
271 git_dir: native_inputs.capture.layout.git_dir.clone(),
272 reference: ready.reference.clone(),
273 base: ready.base.clone(),
274 changes: ready.changes.clone().unwrap_or_default(),
275 message: made.message.clone(),
276 author: Some(made.author.clone()),
277 committer: made.committer.clone(),
278 amend: tree.is_some(),
279 tree: tree.clone(),
280 commit: None,
281 cleanup: None,
282 index: native_inputs.capture.layout.real_index_path.clone(),
283 captured: ready.captured_at,
284 };
285 let parent: String = match made.source.as_ref() {
287 Some(replaced) => replaced.parents.first().cloned().unwrap_or_default(),
288 None => ready.base.clone(),
289 };
290 (sent, parent)
291 }
292 };
293 last_message = typed.clone().or_else(|| { return embedded.as_ref().filter(|_| { return !native; }).map(|made| { return made.message.clone(); }); });
294 reach_service_phase(Phase::ServicePrepared)?;
296 if let Some(host) = provisioning.as_ref().filter(|_| { return !provisioned; }) {
299 provision(host).map_err(|failure| { return TransactionStop::Message(render(&failure)); })?;
300 provisioned = true;
301 }
302 match submit(&config, &sent, &mut std::io::stderr()) {
303 Ok(Submitted::Landed { commit, .. }) => {
304 if let Some(file) = fixed_message.as_ref() {
305 let _removed = std::fs::remove_file(file);
307 }
308 let embedded_made: Option<&Embedded> = embedded.as_ref().filter(|_| { return !native; });
311 let quiet: bool = embedded_made.map_or_else(|| { return asks_for_quiet(after); }, |made| { return made.quiet; });
312 if !quiet {
313 let message: &str = embedded_made.map(|made| { return made.message.as_str(); }).or(typed.as_deref()).unwrap_or("");
314 let name: String = short_name(
315 invocation.context,
316 invocation.cwd,
317 native_inputs.capture.layout.object_directory.as_path(),
318 native_inputs.capture.layout.config_snapshot.as_ref(),
319 commit.as_str(),
320 );
321 print_summary(summary_line(ready.reference.as_str(), first_parent.as_str(), name.as_str(), subject_of(message).as_str()).as_str());
322 if embedded_made.is_none() {
323 let changes: String = change_summary(invocation.context, invocation.cwd, first_parent.as_str(), commit.as_str());
325 if !changes.is_empty() {
326 print_summary(changes.as_str());
327 }
328 }
329 }
330 install_added_worktree_files(
332 invocation.context,
333 invocation.cwd,
334 ready.repository.as_path(),
335 ready.worktree_records.as_slice(),
336 Some(native_inputs.capture.layout.object_directory.as_path()),
337 )
338 .map_err(|error| { return TransactionStop::Message(error.0); })?;
339 let handoff: ServiceHandoff = ServiceHandoff {
340 config: config.clone(),
341 repository: ready.repository.clone(),
342 git_dir: native_inputs.capture.layout.git_dir.clone(),
343 reference: ready.reference.clone(),
344 parent: first_parent,
345 };
346 let result: TransactionResult = TransactionResult { events: ready.events, exit_code: 0, landed_oid: Some(commit), uses_lfs: ready.uses_lfs };
347 return Ok(Some(ServiceCommit { result, handoff: Some(handoff) }));
348 }
349 Ok(Submitted::PrepareAgain { landed, .. }) => {
350 if native || sent.amend {
351 return Err(branch_moved(&ready));
353 }
354 if attempt + 1 == LANDING_ATTEMPTS {
355 let branch: String = ready.reference.trim_start_matches("refs/heads/").to_string();
356 return Err(TransactionStop::Message(render(&ClientFailure::BranchKeepsChanging { branch, times: LANDING_ATTEMPTS })));
357 }
358 retry = Some(retry_of(invocation.context, invocation.cwd, &ready, landed)?);
360 }
361 Ok(Submitted::Moved) => {
362 let branch: String = ready.reference.trim_start_matches("refs/heads/").to_string();
363 if native && !sent.amend && !editor_possible && !ready.interactive && !ready.fixed_parents {
366 if attempt + 1 == REBUILD_ATTEMPTS {
367 return Err(TransactionStop::Message(render(&ClientFailure::BranchKeepsChanging { branch, times: REBUILD_ATTEMPTS })));
368 }
369 retry = Some(retry_of(invocation.context, invocation.cwd, &ready, Vec::new())?);
371 continue;
372 }
373 if native && editor_possible && let Some(text) = typed.as_deref() {
374 let file: PathBuf = native_inputs.capture.layout.git_dir.join(format!("cli-git-saved-message-{}", id.get(..8).unwrap_or(id.as_str())));
376 match std::fs::write(&file, text) {
377 Ok(()) => {
378 let prefix: usize = super::global_arguments::global_layout(native_inputs.arguments.as_slice()).prefix_len;
381 let repeatable: bool = !sent.amend
382 && !ready.interactive
383 && !ready.fixed_parents
384 && with_fixed_message(native_inputs.arguments.as_slice(), prefix, file.as_path()).is_some();
385 if repeatable && attempt + 1 < REBUILD_ATTEMPTS {
386 fixed_message = Some(file);
387 retry = Some(retry_of(invocation.context, invocation.cwd, &ready, Vec::new())?);
388 continue;
389 }
390 let failure: ClientFailure = ClientFailure::BranchMovedMessageSaved { branch, file: file.display().to_string(), amend: sent.amend };
391 return Err(TransactionStop::Message(render(&failure)));
392 }
393 Err(error) => warn("serviceCommit", format!("saving the typed message failed: {error}").as_str()),
394 }
395 }
396 return Err(branch_moved(&ready));
397 }
398 Err(ClientFailure::Unavailable)
399 if prepared_content_missing(
400 native_inputs.capture.layout.common_dir.as_path(),
401 native_inputs.capture.layout.object_directory.as_path(),
402 native_inputs.capture.layout.object_format.as_str(),
403 &sent,
404 ) =>
405 {
406 return Err(TransactionStop::Message(render(&ClientFailure::ContentPruned)));
409 }
410 Err(failure) => return Err(TransactionStop::Message(render(&failure))),
411 }
412 }
413 return Err(TransactionStop::Message(render(&ClientFailure::BranchKeepsChanging { branch: String::from("the branch"), times: REBUILD_ATTEMPTS })));
414}
415
416fn print_summary(line: &str) {
427 let mut out = std::io::stdout();
428 let written = writeln!(out, "{line}").and_then(|()| { return out.flush(); });
429 let Err(error) = written else { return };
430 if error.kind() == std::io::ErrorKind::BrokenPipe {
431 unsafe {
433 libc::signal(libc::SIGPIPE, libc::SIG_DFL);
434 libc::raise(libc::SIGPIPE);
435 }
436 }
437 let _reported = writeln!(std::io::stderr(), "fatal: write error: {error}");
438 std::process::exit(128);
439}
440
441struct Embedded {
443 message: String,
444 author: String,
445 committer: String,
446 source: Option<AmendSource>,
447 quiet: bool,
448}
449
450fn embedded_commit(invocation: &CommitInvocation<'_>, parsed: ServiceCommitArgs) -> Result<Option<Embedded>, TransactionStop> {
461 let Some(committer) = identity(invocation.context, invocation.cwd, "GIT_COMMITTER_IDENT", &[])? else { return Ok(None) };
462 let source: Option<AmendSource> = if parsed.amend {
463 let Some(read) = read_amend_source(invocation.context, invocation.cwd)? else { return Ok(None) };
464 Some(read)
465 } else {
466 None
467 };
468 let previous: Option<&str> = source.as_ref().map(|found| { return found.message.as_str(); });
469 let Some(built) = build_message(invocation, &parsed, committer.as_str(), previous)? else { return Ok(None) };
470 let author: String = match built.reused_author {
471 Some(reused) => reused,
472 None => match source.as_ref() {
473 Some(replaced) => {
474 let Some(made) = amended_author(invocation.context, invocation.cwd, replaced, &parsed)? else { return Ok(None) };
475 made
476 }
477 None => {
478 let Some(made) = identity(invocation.context, invocation.cwd, "GIT_AUTHOR_IDENT", author_overrides(&parsed).as_slice())? else {
479 return Ok(None);
480 };
481 made
482 }
483 },
484 };
485 return Ok(Some(Embedded { message: built.message, author, committer, source, quiet: parsed.quiet }));
486}
487
488#[cfg(test)]
490#[path = "service_commit_tests.rs"]
491mod tests;