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 {
return [&shared.base, &shared.landed, &shared.prepared]
.iter()
.find_map(|entry| { return entry.as_ref().map(|present| { return present.oid.len(); }); })
.unwrap_or(0);
}
pub fn subsumed_record(shared: &SharedPath) -> Vec<u8> {
let Some(landed) = &shared.landed else {
let width: usize = shared.base.as_ref().map(|entry| { return entry.oid.len(); }).unwrap_or(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 cli-git <cli-git@localhost> 0 +0000\ncommitter cli-git <cli-git@localhost> 0 +0000\n\ncli-git 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);
}
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 decision_of = |shared_path: &SharedPath| -> CaptureOrderDecision {
return order.get(&shared_path.path).copied().unwrap_or(CaptureOrderDecision::Unordered);
};
let unordered: Vec<&SharedPath> = shared
.iter()
.filter(|entry| { return decision_of(entry) == CaptureOrderDecision::Unordered; })
.collect();
let text_candidates: Vec<SharedPath> = unordered
.iter()
.filter(|entry| { return is_text_candidate(entry) && !is_identical(entry); })
.map(|entry| { return (*entry).clone(); })
.collect();
let text: Vec<String> = subsumed_text_paths(context, shadow, cwd, object_directory, &text_candidates)
.map_err(SubsumptionError::Text)?;
let edits: Vec<BaseEdit> = shared
.iter()
.filter_map(|entry| { return match decision_of(entry) {
CaptureOrderDecision::Prepared => Some(BaseEdit { shared: entry.clone(), rule: BaseRule::Prepared }),
CaptureOrderDecision::Landed => Some(BaseEdit { shared: entry.clone(), rule: BaseRule::Landed }),
CaptureOrderDecision::Unordered => {
let subsumed: bool = is_identical(entry) || text.contains(&entry.path);
if subsumed {
return Some(BaseEdit { shared: entry.clone(), rule: BaseRule::Subsumed });
}
return None;
}
}; })
.collect();
let subsumed_paths: Vec<String> = edits
.iter()
.filter(|edit| { return edit.rule != BaseRule::Landed; })
.map(|edit| { return edit.shared.path.clone(); })
.collect();
let kept_landed_paths: Vec<String> = edits
.iter()
.filter(|edit| { return edit.rule == BaseRule::Landed; })
.map(|edit| { return edit.shared.path.clone(); })
.collect();
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;