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 = match read_layout_after_success_with(probe.directory, probe.variables, &scope) {
Some(layout) => list_config(layout.common_dir.as_path(), layout.git_dir.as_path(), &scope, probe.variables),
None => None,
};
if let Some(entries) = listed {
let mut value: Option<String> = None;
for item in entries.iter().rev() {
if item.entry.key == ATTRIBUTE_TREE_KEY {
if let Some(bytes) = item.entry.value.as_deref()
&& let Ok(text) = std::str::from_utf8(bytes)
&& !text.is_empty()
{
value = Some(text.to_owned());
}
break;
}
}
return Some(value);
}
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) => {
let text: String = String::from_utf8_lossy(&reply.stdout).trim().to_owned();
Some(if text.is_empty() { None } else { Some(text) })
}
Some(1) => Some(None),
_ => None,
};
}
pub fn attribute_tree_variables(tip: &str, probe: &AttributeProbe<'_>) -> Vec<(OsString, OsString)> {
for (name, _value) in probe.variables {
if name == ATTRIBUTE_SOURCE_VARIABLE {
return Vec::new();
}
}
for token in probe.global_prefix {
if let Some(text) = token.to_str()
&& (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 = match probe.runner.run(&["rev-parse", "--verify", "--quiet", spec.as_str()], None, None) {
Ok(reply) => reply.code == Some(0),
Err(_) => false,
};
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 mut files: Vec<(&[u8], u32, &str)> = Vec::new();
for (file, oid) in merged.iter().zip(oids.iter()) {
files.push((file.path.as_slice(), file.mode, oid.as_str()));
}
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> {
let mut lines: Vec<String> = Vec::new();
for file in merged {
lines.extend(report_lines(prefix, file));
}
return lines;
}
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;