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().copied().map(char::from).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 mut shared: Vec<SharedPath> = Vec::new();
for change in landed {
let mut own: Option<&RawChange> = None;
for candidate in &mine {
if candidate.path == change.path {
own = Some(candidate);
break;
}
}
let Some(own_change) = own else {
continue;
};
shared.push(SharedPath {
path: change.path,
base: change.from,
landed: change.to,
prepared: own_change.to.clone(),
});
}
return Ok(shared);
}
pub fn same_entry(left: &Option<TreeEntry>, right: &Option<TreeEntry>) -> bool {
return left == right;
}
fn is_regular_entry(side: &Option<TreeEntry>) -> bool {
return match side {
Some(entry) => is_regular_mode(entry.mode.as_str()),
None => false,
};
}
pub fn is_regular_triple(shared: &SharedPath) -> bool {
for side in [&shared.base, &shared.landed, &shared.prepared] {
if !is_regular_entry(side) {
return false;
}
}
return true;
}
pub fn is_text_candidate(shared: &SharedPath) -> bool {
let changed_regular: bool = is_regular_entry(&shared.landed) && is_regular_entry(&shared.prepared);
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;