use crate::replay_shared_paths::{SharedPath, SharedPathError, TreeEntry, is_regular_triple, is_text_candidate, same_entry};
use crate::replay_capture_order::CaptureOrderDecision;
use crate::replay_subsumption_text::{TextSubsumptionError, subsumed_text_paths};
use crate::replay_shadow::{replay_base_index, run_shadow_checked};
use crate::transaction_git::{GitContext, GitFailure};
use std::collections::BTreeMap;
use std::path::Path;
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum SubsumptionError {
Shared(SharedPathError),
Text(TextSubsumptionError),
Git(GitFailure),
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct SubsumptionBase {
pub merge_base: String,
pub subsumed_paths: Vec<String>,
pub kept_landed_paths: Vec<String>,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum BaseRule {
Subsumed,
Prepared,
Landed,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct BaseEdit {
pub shared: SharedPath,
pub rule: BaseRule,
}
fn oid_width(shared: &SharedPath) -> usize {
for entry in [&shared.base, &shared.landed, &shared.prepared] {
if let Some(present) = entry {
return present.oid.len();
}
}
return 0;
}
pub fn subsumed_record(shared: &SharedPath) -> Vec<u8> {
let Some(landed) = &shared.landed else {
let width: usize = match shared.base.as_ref() {
Some(entry) => entry.oid.len(),
None => 0,
};
return format!("0 {}\t{}\0", "0".repeat(width), shared.path).into_bytes();
};
let mode: &str = match &shared.base {
Some(base) if is_regular_triple(shared) => base.mode.as_str(),
_ => landed.mode.as_str(),
};
return format!("{mode} {}\t{}\0", landed.oid, shared.path).into_bytes();
}
pub fn exact_record(path: &str, entry: &Option<TreeEntry>, width: usize) -> Vec<u8> {
return match entry {
None => format!("0 {}\t{path}\0", "0".repeat(width)).into_bytes(),
Some(present) => format!("{} {}\t{path}\0", present.mode, present.oid).into_bytes(),
};
}
pub fn base_record(edit: &BaseEdit) -> Vec<u8> {
return match edit.rule {
BaseRule::Subsumed => subsumed_record(&edit.shared),
BaseRule::Prepared => exact_record(&edit.shared.path, &edit.shared.landed, oid_width(&edit.shared)),
BaseRule::Landed => exact_record(&edit.shared.path, &edit.shared.prepared, oid_width(&edit.shared)),
};
}
fn write_subsumed_base(
context: &GitContext,
shadow: &Path,
cwd: &Path,
directory: &Path,
replay: u64,
merge_base: &str,
edits: &[BaseEdit],
) -> Result<String, SubsumptionError> {
let index = replay_base_index(directory, replay);
run_shadow_checked(context, shadow, cwd, &["read-tree", merge_base], None, Some(index.as_path()), &[])
.map_err(SubsumptionError::Git)?;
let mut input: Vec<u8> = Vec::new();
for edit in edits {
input.extend_from_slice(&base_record(edit));
}
run_shadow_checked(
context,
shadow,
cwd,
&["update-index", "-z", "--index-info"],
Some(&input),
Some(index.as_path()),
&[],
)
.map_err(SubsumptionError::Git)?;
let tree_output = run_shadow_checked(context, shadow, cwd, &["write-tree"], None, Some(index.as_path()), &[])
.map_err(SubsumptionError::Git)?;
let tree: String = String::from_utf8_lossy(&tree_output.stdout).trim().to_string();
let message: String = format!(
"tree {tree}\nauthor monochromatic-git-policy-cli <monochromatic-git-policy-cli@localhost> 0 +0000\ncommitter monochromatic-git-policy-cli <monochromatic-git-policy-cli@localhost> 0 +0000\n\nmonochromatic-git-policy-cli replay base with subsumed landed changes\n"
);
let commit = run_shadow_checked(
context,
shadow,
cwd,
&["hash-object", "-t", "commit", "-w", "--stdin"],
Some(message.as_bytes()),
None,
&[],
)
.map_err(SubsumptionError::Git)?;
return Ok(String::from_utf8_lossy(&commit.stdout).trim().to_string());
}
pub fn is_identical(shared: &SharedPath) -> bool {
return same_entry(&shared.prepared, &shared.landed);
}
fn decision_of(order: &BTreeMap<String, CaptureOrderDecision>, shared_path: &SharedPath) -> CaptureOrderDecision {
return order.get(&shared_path.path).copied().unwrap_or(CaptureOrderDecision::Unordered);
}
pub fn subsume_landed_changes(
context: &GitContext,
shadow: &Path,
object_directory: &Path,
cwd: &Path,
directory: &Path,
replay: u64,
merge_base: &str,
shared: &[SharedPath],
order: &BTreeMap<String, CaptureOrderDecision>,
) -> Result<SubsumptionBase, SubsumptionError> {
let mut text_candidates: Vec<SharedPath> = Vec::new();
for entry in shared {
if decision_of(order, entry) == CaptureOrderDecision::Unordered && is_text_candidate(entry) && !is_identical(entry) {
text_candidates.push(entry.clone());
}
}
let text: Vec<String> = subsumed_text_paths(context, shadow, cwd, object_directory, &text_candidates)
.map_err(SubsumptionError::Text)?;
let mut edits: Vec<BaseEdit> = Vec::new();
for entry in shared {
match decision_of(order, entry) {
CaptureOrderDecision::Prepared => edits.push(BaseEdit { shared: entry.clone(), rule: BaseRule::Prepared }),
CaptureOrderDecision::Landed => edits.push(BaseEdit { shared: entry.clone(), rule: BaseRule::Landed }),
CaptureOrderDecision::Unordered => {
let subsumed: bool = is_identical(entry) || text.contains(&entry.path);
if subsumed {
edits.push(BaseEdit { shared: entry.clone(), rule: BaseRule::Subsumed });
}
}
}
}
let mut subsumed_paths: Vec<String> = Vec::new();
let mut kept_landed_paths: Vec<String> = Vec::new();
for edit in &edits {
if edit.rule == BaseRule::Landed {
kept_landed_paths.push(edit.shared.path.clone());
} else {
subsumed_paths.push(edit.shared.path.clone());
}
}
if edits.is_empty() {
return Ok(SubsumptionBase {
merge_base: String::from(merge_base),
subsumed_paths: Vec::new(),
kept_landed_paths: Vec::new(),
});
}
let synthetic: String = write_subsumed_base(context, shadow, cwd, directory, replay, merge_base, &edits)?;
return Ok(SubsumptionBase {
merge_base: synthetic,
subsumed_paths,
kept_landed_paths,
});
}
#[cfg(test)]
#[path = "replay_subsumption_tests.rs"]
mod tests;