use super::private_index_request::indexed_request;
use super::commit_transaction::{TransactionResult, TransactionStop, failed};
use super::path_bytes::{os_from_path_bytes, path_message_text};
use super::policy_events::PolicyEvent;
use super::replay_capture_order::CaptureOrderDecision;
use super::replay_merge::{ReplayMerge, merge_replay_tree};
use super::replay_shared_paths::{SharedPath, list_shared_paths};
use super::replay_subsumption::{SubsumptionBase, subsume_landed_changes};
use super::service_messages::NOTHING_CHANGED;
use super::service_prepare::Ready;
use super::service_protocol::LandedCapture;
use super::transaction_git::{GitContext, GitRequest, run_git_checked};
use std::collections::BTreeMap;
use std::ffi::OsString;
use std::path::{Path, PathBuf};
const CORE_ID: &str = "concurrent-commit";
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct Captured {
pub base: String,
pub tree: String,
pub captured: u64,
pub landed: Vec<LandedCapture>,
}
pub fn capture_stamp() -> u64 {
#[cfg(unix)]
{
let mut now = libc::timespec { tv_sec: 0, tv_nsec: 0 };
let read: i32 = unsafe { libc::clock_gettime(libc::CLOCK_MONOTONIC, &raw mut now) };
if read != 0 {
return 0;
}
let seconds: u64 = u64::try_from(now.tv_sec).unwrap_or(0);
let nanoseconds: u64 = u64::try_from(now.tv_nsec).unwrap_or(0);
return seconds.saturating_mul(1_000_000_000).saturating_add(nanoseconds).max(1);
}
#[cfg(not(unix))]
{
return 0;
}
}
pub struct Retry<'a> {
pub captured: &'a Captured,
pub attempt: u64,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct LandedFirst {
pub short: String,
pub subject: String,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct ReplayConflict {
pub paths: Vec<Vec<u8>>,
pub landed: Option<LandedFirst>,
pub git_dir: PathBuf,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum Merged {
Tree(String),
Conflict(Vec<Vec<u8>>),
}
pub struct MergeSite<'a> {
pub context: &'a GitContext,
pub cwd: &'a Path,
pub common_dir: &'a Path,
pub object_directory: &'a Path,
pub scratch: &'a Path,
}
pub fn retry_of(context: &GitContext, cwd: &Path, ready: &Ready, landed: Vec<LandedCapture>) -> Result<Captured, TransactionStop> {
let tree: String = captured_tree(context, cwd, ready.inputs.commit_index.as_path(), ready.inputs.capture.layout.object_directory.as_path())?;
return Ok(Captured { base: ready.base.clone(), tree, captured: ready.captured_at, landed });
}
pub fn captured_tree(context: &GitContext, cwd: &Path, commit_index: &Path, object_directory: &Path) -> Result<String, TransactionStop> {
let written = run_git_checked(context, &indexed_request(cwd, &["write-tree"], Some(commit_index), Some(object_directory)))?;
return Ok(String::from_utf8_lossy(written.stdout.as_slice()).trim().to_string());
}
fn latin1(bytes: &[u8]) -> String {
return bytes.iter().map(|byte| { return char::from(*byte); }).collect();
}
pub fn merge_captured(site: &MergeSite<'_>, captured: &Captured, tip: &str, attempt: u64) -> Result<Merged, TransactionStop> {
let merge = |base: &str| -> Result<ReplayMerge, TransactionStop> {
return merge_replay_tree(site.context, site.common_dir, site.cwd, base, tip, captured.tree.as_str())
.map_err(|error| { return failed(format!("merging the commit onto the branch failed: {error:?}")); });
};
let conflicting: Vec<Vec<u8>> = match merge(captured.base.as_str())? {
ReplayMerge::Merged { tree_oid } => return Ok(Merged::Tree(tree_oid)),
ReplayMerge::Conflict { paths } => paths,
};
let names: Vec<String> = conflicting.iter().map(|path| { return latin1(path.as_slice()); }).collect();
let shared: Vec<SharedPath> = list_shared_paths(site.context, site.common_dir, site.cwd, captured.base.as_str(), tip, captured.tree.as_str())
.map_err(|error| { return failed(format!("listing the paths the branch changed failed: {error:?}")); })?
.into_iter()
.filter(|entry| { return names.contains(&entry.path); })
.collect();
let mut order: BTreeMap<String, CaptureOrderDecision> = BTreeMap::new();
for entry in &shared {
let decision: CaptureOrderDecision = capture_order(site, captured, tip, entry.path.as_str())?;
order.insert(entry.path.clone(), decision);
}
let settled: SubsumptionBase = subsume_landed_changes(
site.context,
site.common_dir,
site.object_directory,
site.cwd,
site.scratch,
attempt,
captured.base.as_str(),
shared.as_slice(),
&order,
)
.map_err(|error| { return failed(format!("comparing the commit with what landed failed: {error:?}")); })?;
if settled.subsumed_paths.is_empty() && settled.kept_landed_paths.is_empty() {
return Ok(Merged::Conflict(conflicting));
}
return match merge(settled.merge_base.as_str())? {
ReplayMerge::Merged { tree_oid } => Ok(Merged::Tree(tree_oid)),
ReplayMerge::Conflict { paths } => Ok(Merged::Conflict(paths)),
};
}
fn capture_order(site: &MergeSite<'_>, captured: &Captured, tip: &str, path: &str) -> Result<CaptureOrderDecision, TransactionStop> {
if captured.captured == 0 {
return Ok(CaptureOrderDecision::Unordered);
}
let bytes: Vec<u8> = path.chars().filter_map(|character| { return u8::try_from(u32::from(character)).ok(); }).collect();
let exclusion: String = format!("^{}", captured.base);
let mut request: GitRequest = GitRequest::new(site.cwd, &["log", "--format=%H", "--no-color", tip, exclusion.as_str(), "--"]);
let mut pathspec: Vec<u8> = b":(top,literal)".to_vec();
pathspec.extend_from_slice(bytes.as_slice());
request.arguments.push(os_from_path_bytes(pathspec.as_slice()));
request.environment = vec![(OsString::from("GIT_LITERAL_PATHSPECS"), OsString::from("0"))];
let listed = run_git_checked(site.context, &request)?;
let text: String = String::from_utf8_lossy(listed.stdout.as_slice()).into_owned();
let stamps: Vec<Option<u64>> = text
.lines()
.filter(|line| { return !line.is_empty(); })
.map(|commit| { return captured.landed.iter().find(|known| { return known.commit == commit; }).map(|known| { return known.captured; }); })
.collect();
if stamps.is_empty() || stamps.iter().any(Option::is_none) {
return Ok(CaptureOrderDecision::Unordered);
}
let known: Vec<u64> = stamps.into_iter().flatten().collect();
if known.iter().all(|stamp| { return *stamp < captured.captured; }) {
return Ok(CaptureOrderDecision::Prepared);
}
if known.iter().all(|stamp| { return *stamp > captured.captured; }) {
return Ok(CaptureOrderDecision::Landed);
}
return Ok(CaptureOrderDecision::Unordered);
}
pub fn first_landed(context: &GitContext, cwd: &Path, base: &str, tip: &str, paths: &[Vec<u8>]) -> Result<Option<LandedFirst>, TransactionStop> {
let exclusion: String = format!("^{base}");
let mut request: GitRequest = GitRequest::new(
cwd,
&["log", "--reverse", "--format=%h%x00%s", "--no-color", "--no-show-signature", tip, exclusion.as_str(), "--"],
);
for path in paths {
let mut pathspec: Vec<u8> = b":(top,literal)".to_vec();
pathspec.extend_from_slice(path.as_slice());
request.arguments.push(os_from_path_bytes(pathspec.as_slice()));
}
request.environment = vec![(OsString::from("GIT_LITERAL_PATHSPECS"), OsString::from("0"))];
let listed = run_git_checked(context, &request)?;
let text: String = String::from_utf8_lossy(listed.stdout.as_slice()).into_owned();
let Some(first) = text.lines().find(|line| { return !line.is_empty(); }) else { return Ok(None) };
let (short, subject) = first.split_once('\0').unwrap_or((first, ""));
return Ok(Some(LandedFirst { short: short.to_string(), subject: subject.to_string() }));
}
fn paths_text(paths: &[Vec<u8>]) -> String {
let quoted: Vec<String> = paths.iter().map(|path| { return format!("`{}`", path_message_text(path.as_slice())); }).collect();
return match quoted.as_slice() {
[only] => format!("the file {only}"),
many => format!("these files: {}", many.join(", ")),
};
}
pub fn conflict_text(conflict: &ReplayConflict, saved: Option<&Path>) -> String {
let landed: String = match &conflict.landed {
Some(first) => format!("the commit {} (\"{}\")", first.short, first.subject),
None => String::from("another commit"),
};
let look: String = match &conflict.landed {
Some(first) => format!("Look at what that commit changed with `git show {}`, bring your", first.short),
None => String::from("Look at what the branch changed, bring your"),
};
let again: String = match saved {
Some(file) => format!(
"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.",
file.display(),
file.display()
),
None => format!("{look} edit up to date with it, and run the commit again."),
};
return format!(
"This commit was not made. While it was being prepared, {landed} changed {}, which this commit also changes, and the two \
changes cannot be combined. {again} {NOTHING_CHANGED}",
paths_text(conflict.paths.as_slice())
);
}
pub fn conflict_result(conflict: &ReplayConflict, saved: Option<&Path>) -> TransactionResult {
return TransactionResult {
events: vec![PolicyEvent::CoreFinding { core_id: CORE_ID, code: "replay-conflict", message: conflict_text(conflict, saved) }],
exit_code: 1,
landed_oid: None,
uses_lfs: false,
};
}
pub fn already_committed_result(branch: &str) -> TransactionResult {
return TransactionResult {
events: vec![PolicyEvent::CoreFinding {
core_id: CORE_ID,
code: "already-committed",
message: format!(
"This commit was not made. While it was being prepared, other commits put everything it changes on the branch {branch}, \
so nothing is left to commit. Run `git log` to see them. {NOTHING_CHANGED}"
),
}],
exit_code: 1,
landed_oid: None,
uses_lfs: false,
};
}
#[cfg(test)]
#[path = "service_replay_tests.rs"]
mod tests;