use std::{
borrow::Cow,
cmp::Ordering,
path::Path,
sync::Arc,
thread,
time::{Duration, Instant},
};
use log::{debug, warn};
use crate::{
DisplaySettings, LangProfile, MergeResult,
attempts::AttemptsCache,
git::attr::GitAttrsForMerge,
line_based::{
LINE_BASED_METHOD, line_based_merge, line_based_merge_with_duplicate_signature_detection,
},
resolve_merge, structured_merge,
};
#[derive(Default)]
pub struct CliOpts<'a> {
pub allow_parse_errors: Option<bool>,
pub language: Option<&'a str>,
}
#[allow(clippy::too_many_arguments)]
pub fn line_merge_and_structured_resolution(
contents_base: Cow<'static, str>,
contents_left: Cow<'static, str>,
contents_right: Cow<'static, str>,
fname_base: &'static Path,
settings: DisplaySettings<'static>,
full_merge: bool,
attempts_cache: Option<&AttemptsCache>,
cli_opts: CliOpts,
repo_dir: Option<&Path>,
debug_dir: Option<&'static Path>,
timeout: Duration,
) -> MergeResult {
let (language_git, allow_parse_errors_git) = if let Some(repo_dir) = repo_dir
&& let Some(git_attrs) = GitAttrsForMerge::new(repo_dir, fname_base)
{
(git_attrs.language, git_attrs.allow_parse_errors)
} else {
(None, None)
};
let allow_parse_errors = cli_opts.allow_parse_errors.or(allow_parse_errors_git);
let Ok(lang_profile) =
LangProfile::find(fname_base, cli_opts.language, language_git.as_deref())
else {
return line_based_merge(&contents_base, &contents_left, &contents_right, &settings);
};
let mut lang_profile = Cow::Borrowed(lang_profile);
if let Some(allow_parse_errors) = allow_parse_errors {
lang_profile.to_mut().allow_parse_errors = allow_parse_errors;
}
let lang_profile = Arc::new(lang_profile);
let contents_base = Arc::new(contents_base);
let contents_left = Arc::new(contents_left);
let contents_right = Arc::new(contents_right);
let merges = cascading_merge(
Arc::clone(&contents_base),
Arc::clone(&contents_left),
Arc::clone(&contents_right),
lang_profile,
settings,
full_merge,
debug_dir,
timeout,
);
let merge_result = match select_best_merge(merges) {
LineBasedAndBestAre::TheSame(merge) => merge,
LineBasedAndBestAre::NotTheSame { line_based, best } => {
if best.conflict_count == 0 {
if let Some(cache) = attempts_cache {
match cache.new_attempt(
fname_base,
&contents_base,
&contents_left,
&contents_right,
) {
Ok(attempt) => {
best.store_in_attempt(&attempt);
line_based.store_in_attempt(&attempt);
best.mark_as_best_merge_in_attempt(&attempt, line_based.conflict_count);
}
Err(err) => {
warn!("Could not store merging attempt for later review: {err}");
}
}
}
}
best
}
};
debug!("Selected best merge: {}", merge_result.method);
merge_result
}
#[allow(clippy::too_many_arguments)]
pub fn cascading_merge(
contents_base: Arc<Cow<'static, str>>,
contents_left: Arc<Cow<'static, str>>,
contents_right: Arc<Cow<'static, str>>,
lang_profile: Arc<Cow<'static, LangProfile>>,
settings: DisplaySettings<'static>,
full_merge: bool,
debug_dir: Option<&'static Path>,
timeout: Duration,
) -> Vec<MergeResult> {
let start = Instant::now();
let (parsed_conflicts, line_based_merge) = line_based_merge_with_duplicate_signature_detection(
&contents_base,
&contents_left,
&contents_right,
&settings,
&lang_profile,
);
debug!("line-based merge took {:?}", start.elapsed());
if line_based_merge.conflict_count == 0 && !line_based_merge.has_additional_issues {
return vec![line_based_merge];
}
let (tx, rx) = oneshot::channel();
thread::spawn(move || {
let mut merges = Vec::new();
if !line_based_merge.has_additional_issues {
let solved_merge =
resolve_merge(&parsed_conflicts, &settings, &lang_profile, debug_dir);
match solved_merge {
Ok(recovered_merge) => {
if recovered_merge.conflict_count == 0 && !recovered_merge.has_additional_issues
{
let _ = tx.send(vec![recovered_merge]);
return;
}
merges.push(recovered_merge);
}
Err(err) => {
debug!("error while attempting conflict resolution of line-based merge: {err}");
}
}
}
if full_merge || line_based_merge.has_additional_issues {
let structured_merge = structured_merge(
&contents_base,
&contents_left,
&contents_right,
None,
&settings,
&lang_profile,
debug_dir,
);
match structured_merge {
Ok(successful_merge) => merges.push(successful_merge),
Err(parse_error) => {
debug!("full structured merge encountered an error: {parse_error}");
}
};
}
let _ = tx.send(merges);
});
let mut merges = if timeout.is_zero() {
rx.recv().unwrap()
} else {
match rx.recv_timeout(timeout) {
Ok(merges) => merges,
Err(oneshot::RecvTimeoutError::Timeout) => {
warn!("structured merge took too long, falling back to Git");
vec![]
}
Err(oneshot::RecvTimeoutError::Disconnected) => unreachable!(),
}
};
merges.push(line_based_merge);
merges
}
enum LineBasedAndBestAre {
TheSame(MergeResult),
NotTheSame {
line_based: MergeResult,
best: MergeResult,
},
}
fn select_best_merge(mut merges: Vec<MergeResult>) -> LineBasedAndBestAre {
merges.sort_by_key(|merge| merge.conflict_mass);
debug!("~~~ Merge statistics ~~~");
for merge in &merges {
let s = if merge.conflict_count == 1 { "" } else { "s" };
debug!(
"{}: {} conflict{s}, {} mass, has_additional_issues: {}",
merge.method, merge.conflict_count, merge.conflict_mass, merge.has_additional_issues
);
}
let best_pos = merges
.iter()
.position(|merge| !merge.has_additional_issues)
.unwrap_or_default();
let line_based_pos = merges
.iter()
.position(|merge| merge.method == LINE_BASED_METHOD)
.expect("No line-based merge available");
match best_pos.cmp(&line_based_pos) {
Ordering::Equal => {
let best = merges.swap_remove(best_pos);
LineBasedAndBestAre::TheSame(best)
}
Ordering::Less => {
let line_based = merges.swap_remove(line_based_pos);
let best = merges.swap_remove(best_pos);
LineBasedAndBestAre::NotTheSame { line_based, best }
}
Ordering::Greater => {
let best = merges.swap_remove(best_pos);
let line_based = merges.swap_remove(line_based_pos);
LineBasedAndBestAre::NotTheSame { line_based, best }
}
}
}