use crate::replay_shadow::run_shadow_checked;
use crate::transaction_git::{GitContext, GitFailure};
use std::path::Path;
const ABSENT_MODE: &str = "000000";
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct TreeEntry {
pub mode: String,
pub oid: String,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct RawChange {
pub path: String,
pub from: Option<TreeEntry>,
pub to: Option<TreeEntry>,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct SharedPath {
pub path: String,
pub base: Option<TreeEntry>,
pub landed: Option<TreeEntry>,
pub prepared: Option<TreeEntry>,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct RawFormatError(pub String);
fn entry_of(mode: &str, oid: &str) -> Option<TreeEntry> {
if mode == ABSENT_MODE {
return None;
}
return Some(TreeEntry {
mode: String::from(mode),
oid: String::from(oid),
});
}
pub fn parse_raw_changes(output: &str) -> Result<Vec<RawChange>, RawFormatError> {
let mut changes: Vec<RawChange> = Vec::new();
let tokens: Vec<&str> = output.split('\0').collect();
let mut cursor: usize = 0;
while cursor < tokens.len() {
let header: &str = tokens[cursor];
cursor += 1;
if header.is_empty() {
continue;
}
let fields: Vec<&str> = header.trim_start_matches(':').split(' ').collect();
let path: &str = match tokens.get(cursor) {
Some(found) => found,
None => return Err(RawFormatError(format!("raw record {header} has no path"))),
};
cursor += 1;
if fields.len() != 5 || !header.starts_with(':') {
return Err(RawFormatError(format!("malformed raw record {header}")));
}
changes.push(RawChange {
path: String::from(path),
from: entry_of(fields[0], fields[2]),
to: entry_of(fields[1], fields[3]),
});
}
return Ok(changes);
}
fn raw_changes(
context: &GitContext,
shadow: &Path,
cwd: &Path,
from: &str,
to: &str,
) -> Result<Vec<RawChange>, SharedPathError> {
let output = run_shadow_checked(
context,
shadow,
cwd,
&[
"diff-tree",
"-r",
"-z",
"--raw",
"--no-renames",
"--no-abbrev",
from,
to,
],
None,
None,
&[],
)
.map_err(SharedPathError::Git)?;
let text: String = output.stdout.iter().map(|byte| { return char::from(*byte); }).collect();
return parse_raw_changes(text.as_str()).map_err(SharedPathError::Format);
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum SharedPathError {
Git(GitFailure),
Format(RawFormatError),
}
pub fn list_shared_paths(
context: &GitContext,
shadow: &Path,
cwd: &Path,
merge_base: &str,
current: &str,
prepared: &str,
) -> Result<Vec<SharedPath>, SharedPathError> {
let landed: Vec<RawChange> = raw_changes(context, shadow, cwd, merge_base, current)?;
let mine: Vec<RawChange> = raw_changes(context, shadow, cwd, merge_base, prepared)?;
let shared: Vec<SharedPath> = landed
.into_iter()
.filter_map(|change| {
let own: &RawChange = mine.iter().find(|candidate| { return candidate.path == change.path; })?;
return Some(SharedPath {
path: change.path,
base: change.from,
landed: change.to,
prepared: own.to.clone(),
});
})
.collect();
return Ok(shared);
}
pub fn same_entry(left: &Option<TreeEntry>, right: &Option<TreeEntry>) -> bool {
return left == right;
}
pub fn is_regular_triple(shared: &SharedPath) -> bool {
return [&shared.base, &shared.landed, &shared.prepared]
.iter()
.all(|side| { return side.as_ref().is_some_and(|entry| { return is_regular_mode(entry.mode.as_str()); }); });
}
pub fn is_text_candidate(shared: &SharedPath) -> bool {
let changed_regular: bool = [&shared.landed, &shared.prepared]
.iter()
.all(|side| { return side.as_ref().is_some_and(|entry| { return is_regular_mode(entry.mode.as_str()); }); });
let base_regular: bool = match &shared.base {
None => true,
Some(entry) => is_regular_mode(entry.mode.as_str()),
};
return changed_regular && base_regular;
}
fn is_regular_mode(mode: &str) -> bool {
return mode == "100644" || mode == "100755";
}
#[cfg(test)]
#[path = "replay_shared_paths_tests.rs"]
mod tests;