use crate::diagnostic_log::debug;
use crate::merge_second_pass::{Conflict, GitRunner, Left, Merged, Verdict, journal_merged, report_lines, second_pass, tree_with_files, write_blob};
use crate::path_bytes::path_message_text;
use crate::repo_read_config_list::list_config;
use crate::repo_read_config_scan::ScopeEnvironment;
use crate::repo_read_layout::read_layout_after_success_with;
use crate::random_id::random_uuid;
use crate::scratch_location::prepare_repository_scratch;
use std::ffi::OsString;
use std::path::Path;
const TAG: &str = "wordMergeTree";
pub const ATTRIBUTE_SOURCE_VARIABLE: &str = "GIT_ATTR_SOURCE";
const ATTRIBUTE_TREE_KEY: &str = "attr.tree";
pub struct AttributeProbe<'a> {
pub directory: &'a Path,
pub global_prefix: &'a [OsString],
pub variables: &'a [(OsString, OsString)],
pub runner: &'a dyn GitRunner,
}
fn configured_attribute_tree(probe: &AttributeProbe<'_>) -> Option<Option<String>> {
let scope: ScopeEnvironment = ScopeEnvironment::from_variables(probe.variables);
let listed = read_layout_after_success_with(probe.directory, probe.variables, &scope)
.and_then(|layout| { return list_config(layout.common_dir.as_path(), layout.git_dir.as_path(), &scope, probe.variables); });
if let Some(entries) = listed {
return Some(
entries
.iter()
.rev()
.find(|item| { return item.entry.key == ATTRIBUTE_TREE_KEY; })
.and_then(|item| { return item.entry.value.as_deref().and_then(|bytes| { return std::str::from_utf8(bytes).ok().map(str::to_owned); }); })
.filter(|value| { return !value.is_empty(); }),
);
}
debug(TAG, "the in-process readers decline the repository, so Git is asked for attr.tree");
let reply = probe.runner.run(&["config", "--get", ATTRIBUTE_TREE_KEY], None, None).ok()?;
return match reply.code {
Some(0) => Some(Some(String::from_utf8_lossy(&reply.stdout).trim().to_owned()).filter(|value| { return !value.is_empty(); })),
Some(1) => Some(None),
_ => None,
};
}
pub fn attribute_tree_variables(tip: &str, probe: &AttributeProbe<'_>) -> Vec<(OsString, OsString)> {
if probe.variables.iter().any(|(name, _)| { return name == ATTRIBUTE_SOURCE_VARIABLE; }) {
return Vec::new();
}
if probe.global_prefix.iter().any(|token| { return token.to_str().is_some_and(|text| { return text == "--attr-source" || text.starts_with("--attr-source="); }); }) {
return Vec::new();
}
let Some(configured) = configured_attribute_tree(probe) else {
debug(TAG, "attr.tree cannot be read, so no attribute tree is named");
return Vec::new();
};
if let Some(value) = configured {
let spec: String = format!("{value}^{{tree}}");
let resolves: bool = probe.runner.run(&["rev-parse", "--verify", "--quiet", spec.as_str()], None, None).is_ok_and(|reply| { return reply.code == Some(0); });
if resolves {
return Vec::new();
}
debug(TAG, format!("attr.tree names {value}, which is no tree; Git ignores it, so the first side's tip is named").as_str());
}
return vec![(OsString::from(ATTRIBUTE_SOURCE_VARIABLE), OsString::from(tip))];
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct Resolved {
pub tree: String,
pub merged: Vec<Merged>,
}
pub fn resolve_tree(runner: &dyn GitRunner, common_dir: &Path, tree: &str, conflicts: &[Conflict]) -> Option<Resolved> {
if conflicts.is_empty() {
return None;
}
let verdicts: Vec<Verdict> = match second_pass(runner, conflicts) {
Ok(found) => found,
Err(reason) => {
debug(TAG, format!("the second pass could not run, so the conflicts stay: {reason}").as_str());
return None;
}
};
let mut merged: Vec<Merged> = Vec::new();
let mut left: Vec<String> = Vec::new();
for verdict in verdicts {
match verdict {
Verdict::Merged(file) => merged.push(*file),
Verdict::Left { path, why } => left.push(format!("{} ({})", path_message_text(path.as_slice()), left_words(&why))),
}
}
if !left.is_empty() {
debug(TAG, format!("the merge stays conflicted; left to the person: {}", left.join(", ")).as_str());
return None;
}
let mut oids: Vec<String> = Vec::new();
for file in &merged {
match write_blob(runner, file.bytes.as_slice()) {
Ok(oid) => oids.push(oid),
Err(reason) => {
debug(TAG, format!("the merged blob of {} could not be written: {reason}", path_message_text(file.path.as_slice())).as_str());
return None;
}
}
}
let scratch = match prepare_repository_scratch(common_dir) {
Ok(found) => found,
Err(reason) => {
debug(TAG, format!("no scratch directory for the second pass: {reason}").as_str());
return None;
}
};
let name: String = match random_uuid() {
Ok(found) => found,
Err(reason) => {
debug(TAG, format!("no unique name for the scratch index: {}", reason.0).as_str());
return None;
}
};
let index = scratch.join(format!("second-pass-{name}.index"));
let files: Vec<(&[u8], u32, &str)> = merged.iter().zip(oids.iter()).map(|(file, oid)| { return (file.path.as_slice(), file.mode, oid.as_str()); }).collect();
let rewritten: Result<String, String> = tree_with_files(runner, tree, files.as_slice(), index.as_path());
if let Err(error) = std::fs::remove_file(index.as_path()) {
debug(TAG, format!("the scratch index could not be removed ({error}); the repository scratch sweep collects it").as_str());
}
return match rewritten {
Ok(found) => Some(Resolved { tree: found, merged }),
Err(reason) => {
debug(TAG, format!("the merged tree could not be built: {reason}").as_str());
None
}
};
}
fn left_words(why: &Left) -> String {
return match why {
Left::MissingStage => String::from("a stage is missing"),
Left::NotAPlainFile => String::from("not a plain file with one mode"),
Left::Binary => String::from("binary"),
Left::TooLarge => String::from("too large"),
Left::Driver(value) => format!("merge attribute {value}"),
Left::Overlap(count) => format!("{count} regions hold two changes of the same words"),
};
}
pub fn report(prefix: &str, merged: &[Merged]) -> Vec<String> {
return merged.iter().flat_map(|file| { return report_lines(prefix, file); }).collect();
}
pub fn announce(prefix: &str, sender: (&str, &str), merged: &[Merged]) -> Vec<String> {
let (identifier, operation): (&str, &str) = sender;
for file in merged {
journal_merged(identifier, operation, file);
}
return report(prefix, merged);
}
#[cfg(test)]
#[path = "merge_second_pass_tree_tests.rs"]
mod tests;