1use super::capture_order::indexed_request;
20use super::commit_transaction::{TransactionResult, TransactionStop, failed};
21use super::path_bytes::{os_from_path_bytes, path_message_text};
22use super::policy_events::PolicyEvent;
23use super::replay_capture_order::CaptureOrderDecision;
24use super::replay_merge::{ReplayMerge, merge_replay_tree};
25use super::replay_shared_paths::{SharedPath, list_shared_paths};
26use super::replay_subsumption::{SubsumptionBase, subsume_landed_changes};
27use super::service_messages::NOTHING_CHANGED;
28use super::service_prepare::Ready;
29use super::service_protocol::LandedCapture;
30use super::transaction_git::{GitContext, GitRequest, run_git_checked};
31use std::collections::BTreeMap;
32use std::ffi::OsString;
33use std::path::{Path, PathBuf};
34
35const CORE_ID: &str = "concurrent-commit";
37
38#[derive(Clone, Debug, Eq, PartialEq)]
40pub struct Captured {
41 pub base: String,
43 pub tree: String,
45 pub captured: u64,
48 pub landed: Vec<LandedCapture>,
50}
51
52pub fn capture_stamp() -> u64 {
63 #[cfg(unix)]
64 {
65 let mut now = libc::timespec { tv_sec: 0, tv_nsec: 0 };
66 let read: i32 = unsafe { libc::clock_gettime(libc::CLOCK_MONOTONIC, &raw mut now) };
68 if read != 0 {
69 return 0;
70 }
71 let seconds: u64 = u64::try_from(now.tv_sec).unwrap_or(0);
72 let nanoseconds: u64 = u64::try_from(now.tv_nsec).unwrap_or(0);
73 return seconds.saturating_mul(1_000_000_000).saturating_add(nanoseconds).max(1);
74 }
75 #[cfg(not(unix))]
76 {
77 return 0;
78 }
79}
80
81pub struct Retry<'a> {
83 pub captured: &'a Captured,
85 pub attempt: u64,
87}
88
89#[derive(Clone, Debug, Eq, PartialEq)]
91pub struct LandedFirst {
92 pub short: String,
94 pub subject: String,
96}
97
98#[derive(Clone, Debug, Eq, PartialEq)]
100pub struct ReplayConflict {
101 pub paths: Vec<Vec<u8>>,
103 pub landed: Option<LandedFirst>,
105 pub git_dir: PathBuf,
107}
108
109#[derive(Clone, Debug, Eq, PartialEq)]
111pub enum Merged {
112 Tree(String),
114 Conflict(Vec<Vec<u8>>),
116}
117
118pub struct MergeSite<'a> {
120 pub context: &'a GitContext,
122 pub cwd: &'a Path,
124 pub common_dir: &'a Path,
126 pub object_directory: &'a Path,
128 pub scratch: &'a Path,
130}
131
132pub fn retry_of(context: &GitContext, cwd: &Path, ready: &Ready, landed: Vec<LandedCapture>) -> Result<Captured, TransactionStop> {
143 let tree: String = captured_tree(context, cwd, ready.inputs.commit_index.as_path(), ready.inputs.capture.layout.object_directory.as_path())?;
144 return Ok(Captured { base: ready.base.clone(), tree, captured: ready.captured_at, landed });
145}
146
147pub fn captured_tree(context: &GitContext, cwd: &Path, commit_index: &Path, object_directory: &Path) -> Result<String, TransactionStop> {
157 let written = run_git_checked(context, &indexed_request(cwd, &["write-tree"], Some(commit_index), Some(object_directory)))?;
158 return Ok(String::from_utf8_lossy(written.stdout.as_slice()).trim().to_string());
159}
160
161fn latin1(bytes: &[u8]) -> String {
163 return bytes.iter().map(|byte| { return char::from(*byte); }).collect();
164}
165
166pub fn merge_captured(site: &MergeSite<'_>, captured: &Captured, tip: &str, attempt: u64) -> Result<Merged, TransactionStop> {
178 let merge = |base: &str| -> Result<ReplayMerge, TransactionStop> {
179 return merge_replay_tree(site.context, site.common_dir, site.cwd, base, tip, captured.tree.as_str())
180 .map_err(|error| { return failed(format!("merging the commit onto the branch failed: {error:?}")); });
181 };
182 let conflicting: Vec<Vec<u8>> = match merge(captured.base.as_str())? {
183 ReplayMerge::Merged { tree_oid } => return Ok(Merged::Tree(tree_oid)),
184 ReplayMerge::Conflict { paths } => paths,
185 };
186 let names: Vec<String> = conflicting.iter().map(|path| { return latin1(path.as_slice()); }).collect();
187 let shared: Vec<SharedPath> = list_shared_paths(site.context, site.common_dir, site.cwd, captured.base.as_str(), tip, captured.tree.as_str())
188 .map_err(|error| { return failed(format!("listing the paths the branch changed failed: {error:?}")); })?
189 .into_iter()
190 .filter(|entry| { return names.contains(&entry.path); })
191 .collect();
192 let mut order: BTreeMap<String, CaptureOrderDecision> = BTreeMap::new();
193 for entry in &shared {
194 let decision: CaptureOrderDecision = capture_order(site, captured, tip, entry.path.as_str())?;
195 order.insert(entry.path.clone(), decision);
196 }
197 let settled: SubsumptionBase = subsume_landed_changes(
198 site.context,
199 site.common_dir,
200 site.object_directory,
201 site.cwd,
202 site.scratch,
203 attempt,
204 captured.base.as_str(),
205 shared.as_slice(),
206 &order,
207 )
208 .map_err(|error| { return failed(format!("comparing the commit with what landed failed: {error:?}")); })?;
209 if settled.subsumed_paths.is_empty() && settled.kept_landed_paths.is_empty() {
210 return Ok(Merged::Conflict(conflicting));
211 }
212 return match merge(settled.merge_base.as_str())? {
213 ReplayMerge::Merged { tree_oid } => Ok(Merged::Tree(tree_oid)),
214 ReplayMerge::Conflict { paths } => Ok(Merged::Conflict(paths)),
215 };
216}
217
218fn capture_order(site: &MergeSite<'_>, captured: &Captured, tip: &str, path: &str) -> Result<CaptureOrderDecision, TransactionStop> {
231 if captured.captured == 0 {
232 return Ok(CaptureOrderDecision::Unordered);
233 }
234 let bytes: Vec<u8> = path.chars().filter_map(|character| { return u8::try_from(u32::from(character)).ok(); }).collect();
235 let exclusion: String = format!("^{}", captured.base);
236 let mut request: GitRequest = GitRequest::new(site.cwd, &["log", "--format=%H", "--no-color", tip, exclusion.as_str(), "--"]);
237 let mut pathspec: Vec<u8> = b":(top,literal)".to_vec();
238 pathspec.extend_from_slice(bytes.as_slice());
239 request.arguments.push(os_from_path_bytes(pathspec.as_slice()));
240 request.environment = vec![(OsString::from("GIT_LITERAL_PATHSPECS"), OsString::from("0"))];
241 let listed = run_git_checked(site.context, &request)?;
242 let text: String = String::from_utf8_lossy(listed.stdout.as_slice()).into_owned();
243 let stamps: Vec<Option<u64>> = text
244 .lines()
245 .filter(|line| { return !line.is_empty(); })
246 .map(|commit| { return captured.landed.iter().find(|known| { return known.commit == commit; }).map(|known| { return known.captured; }); })
247 .collect();
248 if stamps.is_empty() || stamps.iter().any(Option::is_none) {
249 return Ok(CaptureOrderDecision::Unordered);
250 }
251 let known: Vec<u64> = stamps.into_iter().flatten().collect();
252 if known.iter().all(|stamp| { return *stamp < captured.captured; }) {
253 return Ok(CaptureOrderDecision::Prepared);
254 }
255 if known.iter().all(|stamp| { return *stamp > captured.captured; }) {
256 return Ok(CaptureOrderDecision::Landed);
257 }
258 return Ok(CaptureOrderDecision::Unordered);
259}
260
261pub fn first_landed(context: &GitContext, cwd: &Path, base: &str, tip: &str, paths: &[Vec<u8>]) -> Result<Option<LandedFirst>, TransactionStop> {
271 let exclusion: String = format!("^{base}");
272 let mut request: GitRequest = GitRequest::new(
273 cwd,
274 &["log", "--reverse", "--format=%h%x00%s", "--no-color", "--no-show-signature", tip, exclusion.as_str(), "--"],
275 );
276 for path in paths {
277 let mut pathspec: Vec<u8> = b":(top,literal)".to_vec();
278 pathspec.extend_from_slice(path.as_slice());
279 request.arguments.push(os_from_path_bytes(pathspec.as_slice()));
280 }
281 request.environment = vec![(OsString::from("GIT_LITERAL_PATHSPECS"), OsString::from("0"))];
282 let listed = run_git_checked(context, &request)?;
283 let text: String = String::from_utf8_lossy(listed.stdout.as_slice()).into_owned();
284 let Some(first) = text.lines().find(|line| { return !line.is_empty(); }) else { return Ok(None) };
285 let (short, subject) = first.split_once('\0').unwrap_or((first, ""));
286 return Ok(Some(LandedFirst { short: short.to_string(), subject: subject.to_string() }));
287}
288
289fn paths_text(paths: &[Vec<u8>]) -> String {
291 let quoted: Vec<String> = paths.iter().map(|path| { return format!("`{}`", path_message_text(path.as_slice())); }).collect();
292 return match quoted.as_slice() {
293 [only] => format!("the file {only}"),
294 many => format!("these files: {}", many.join(", ")),
295 };
296}
297
298pub fn conflict_text(conflict: &ReplayConflict, saved: Option<&Path>) -> String {
308 let landed: String = match &conflict.landed {
309 Some(first) => format!("the commit {} (\"{}\")", first.short, first.subject),
310 None => String::from("another commit"),
311 };
312 let look: String = match &conflict.landed {
313 Some(first) => format!("Look at what that commit changed with `git show {}`, bring your", first.short),
314 None => String::from("Look at what the branch changed, bring your"),
315 };
316 let again: String = match saved {
317 Some(file) => format!(
318 "Your message is saved in {}. {look} edit up to date with it, and run the commit again with `git commit -F {}` and the same paths.",
319 file.display(),
320 file.display()
321 ),
322 None => format!("{look} edit up to date with it, and run the commit again."),
323 };
324 return format!(
325 "This commit was not made. While it was being prepared, {landed} changed {}, which this commit also changes, and the two \
326 changes cannot be combined. {again} {NOTHING_CHANGED}",
327 paths_text(conflict.paths.as_slice())
328 );
329}
330
331pub fn conflict_result(conflict: &ReplayConflict, saved: Option<&Path>) -> TransactionResult {
333 return TransactionResult {
334 events: vec![PolicyEvent::CoreFinding { core_id: CORE_ID, code: "replay-conflict", message: conflict_text(conflict, saved) }],
335 exit_code: 1,
336 landed_oid: None,
337 uses_lfs: false,
338 };
339}
340
341pub fn already_committed_result(branch: &str) -> TransactionResult {
343 return TransactionResult {
344 events: vec![PolicyEvent::CoreFinding {
345 core_id: CORE_ID,
346 code: "already-committed",
347 message: format!(
348 "This commit was not made. While it was being prepared, other commits put everything it changes on the branch {branch}, \
349 so nothing is left to commit. Run `git log` to see them. {NOTHING_CHANGED}"
350 ),
351 }],
352 exit_code: 1,
353 landed_oid: None,
354 uses_lfs: false,
355 };
356}
357
358#[cfg(test)]
360#[path = "service_replay_tests.rs"]
361mod tests;