Script 'mail_helper' called by obssrc Hello community, here is the log from the commit of package mergiraf for openSUSE:Factory checked in at 2026-09-01 15:51:58 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Comparing /work/SRC/openSUSE:Factory/mergiraf (Old) and /work/SRC/openSUSE:Factory/.mergiraf.new.1265 (New) ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "mergiraf" Tue Sep 1 15:51:58 2026 rev:13 rq:1375086 version:0.19.1 Changes: -------- --- /work/SRC/openSUSE:Factory/mergiraf/mergiraf.changes 2026-08-21 22:17:17.310870205 +0200 +++ /work/SRC/openSUSE:Factory/.mergiraf.new.1265/mergiraf.changes 2026-09-01 15:52:36.757665291 +0200 @@ -1,0 +2,10 @@ +Tue Sep 01 10:38:15 UTC 2026 - Andrei Dziahel <[email protected]> + +- Update to version 0.19.1: + * Set version to 0.19.1 + * fix: don't abort the overall merge when merging binary files (#817) + * fix(kotlin): add missing signature for visibility modifiers (#820) + * refactor: simplify code using the `in_data_dir` helper (#814) + * cargo-update-20260817 (#811) + +------------------------------------------------------------------- Old: ---- mergiraf-0.19.0.obscpio New: ---- mergiraf-0.19.1.obscpio ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Other differences: ------------------ ++++++ mergiraf.spec ++++++ --- /var/tmp/diff_new_pack.anfPkC/_old 2026-09-01 15:52:37.865703948 +0200 +++ /var/tmp/diff_new_pack.anfPkC/_new 2026-09-01 15:52:37.868704052 +0200 @@ -18,7 +18,7 @@ Name: mergiraf -Version: 0.19.0 +Version: 0.19.1 Release: 0 Summary: A syntax-aware git merge driver License: GPL-3.0-or-later ++++++ _service ++++++ --- /var/tmp/diff_new_pack.anfPkC/_old 2026-09-01 15:52:37.916705727 +0200 +++ /var/tmp/diff_new_pack.anfPkC/_new 2026-09-01 15:52:37.919705832 +0200 @@ -3,7 +3,7 @@ <param name="url">https://codeberg.org/mergiraf/mergiraf.git</param> <param name="versionformat">@PARENT_TAG@</param> <param name="scm">git</param> - <param name="revision">v0.19.0</param> + <param name="revision">v0.19.1</param> <param name="versionrewrite-pattern">v(\d+\.\d+\.\d+)</param> <param name="changesgenerate">enable</param> </service> ++++++ _servicedata ++++++ --- /var/tmp/diff_new_pack.anfPkC/_old 2026-09-01 15:52:37.943706669 +0200 +++ /var/tmp/diff_new_pack.anfPkC/_new 2026-09-01 15:52:37.947706808 +0200 @@ -1,6 +1,6 @@ <servicedata> <service name="tar_scm"> <param name="url">https://codeberg.org/mergiraf/mergiraf.git</param> - <param name="changesrevision">7246b03c08c02248035bc20668fde1edf94a09e0</param></service></servicedata> + <param name="changesrevision">e1c6514fb364faca51132be2b336aa739cdfadb9</param></service></servicedata> (No newline at EOF) ++++++ mergiraf-0.19.0.obscpio -> mergiraf-0.19.1.obscpio ++++++ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/mergiraf-0.19.0/Cargo.lock new/mergiraf-0.19.1/Cargo.lock --- old/mergiraf-0.19.0/Cargo.lock 2026-08-16 17:58:44.000000000 +0200 +++ new/mergiraf-0.19.1/Cargo.lock 2026-09-01 10:50:21.000000000 +0200 @@ -146,9 +146,9 @@ [[package]] name = "cc" -version = "1.4.0" +version = "1.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5add81bb678e6cb321aff7fa0dc7689ad82b112dbc032cea19f91d6b8e3582b9" +checksum = "509591b7bcd67f4ef775afad7662703b4935daaa6ec0e5605cfb1090b32a2b6d" dependencies = [ "find-msvc-tools", "shlex", @@ -175,9 +175,9 @@ [[package]] name = "clap" -version = "4.6.5" +version = "4.6.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "301b56658598e48f3648647ac6fc887be7e7108eddfa4e9b63fcf3ec58c0cadf" +checksum = "473c7e07f409a8d772161724aa8db6a765a2532a70f9667eeb7b49d3d02fbdca" dependencies = [ "clap_builder", "clap_derive", @@ -185,9 +185,9 @@ [[package]] name = "clap_builder" -version = "4.6.5" +version = "4.6.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94a65403d1a1bd28f7dc68eb8506e8874808ee5eecb59298de588e2e1407a078" +checksum = "7b48fea5a88e9ae728a2dcbedbfc0e730f7d60da42e1cb049a83c9fb8b789889" dependencies = [ "anstream", "anstyle", @@ -345,9 +345,9 @@ [[package]] name = "find-msvc-tools" -version = "0.1.9" +version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" +checksum = "d45db016d36b838f563236e9193d0ee6ce38f3f68b6c94e914b4929c96bbb890" [[package]] name = "fixedbitset" @@ -366,6 +366,15 @@ ] [[package]] +name = "float-cmp" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b09cf3155332e944990140d967ff5eceb70df778b34f77d8075db46e4704e6d8" +dependencies = [ + "num-traits", +] + +[[package]] name = "foldhash" version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -615,7 +624,7 @@ [[package]] name = "mergiraf" -version = "0.19.0" +version = "0.19.1" dependencies = [ "assert_cmd", "caplog", @@ -631,6 +640,7 @@ "nonempty-collections", "nu-ansi-term", "oneshot", + "predicates", "pretty_assertions", "rand", "regex", @@ -638,7 +648,7 @@ "rustc-hash 2.1.3", "stderrlog", "tempfile", - "thiserror 2.0.19", + "thiserror 2.0.20", "tree-edit-distance", "tree-sitter", "tree-sitter-bash", @@ -719,6 +729,12 @@ checksum = "988fb92b275335f315a9bddf7a8881a02c79c6084e9a626f656f12c50776a045" [[package]] +name = "normalize-line-endings" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61807f77802ff30975e01f4f071c8ba10c022052f98b3294119f3e615d13e5be" + +[[package]] name = "nu-ansi-term" version = "0.50.3" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -792,7 +808,10 @@ dependencies = [ "anstyle", "difflib", + "float-cmp", + "normalize-line-endings", "predicates-core", + "regex", ] [[package]] @@ -1140,11 +1159,11 @@ [[package]] name = "thiserror" -version = "2.0.19" +version = "2.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09a43598840e33d5b0331f38c5e30d13bb11c11210a4b58f0d9b18a5a5eefcd9" +checksum = "ec86235f5fcc2a73650310756d2ac5b138a5780bbbdfae3eeccec992c435ba4f" dependencies = [ - "thiserror-impl 2.0.19", + "thiserror-impl 2.0.20", ] [[package]] @@ -1160,9 +1179,9 @@ [[package]] name = "thiserror-impl" -version = "2.0.19" +version = "2.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43cbfe0cf76104d42a574802844187e84a305e531ed54455f11fbde0f10541cd" +checksum = "bc04cd3e1236dd4a98afca4569f2deb3f120e5422a4023be2cb683f8486292af" dependencies = [ "proc-macro2", "quote", @@ -1192,9 +1211,9 @@ [[package]] name = "tree-sitter" -version = "0.26.11" +version = "0.26.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af1c71c1c4cc0920b20d6b0f6572e7682cd07a6a2faec71067a31fa394c586df" +checksum = "83c567a8e18ae93f20982c90370b16fd24023aeaf52f6052b96957ab253a0fec" dependencies = [ "cc", "regex", @@ -1256,9 +1275,9 @@ [[package]] name = "tree-sitter-dart-orchard" -version = "0.5.0" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26a7282fbb2427b043d84368ff62d54e098e93f47a0bede90c75db7c08f315b4" +checksum = "5e9e47f28354ae26b1ecfe2b2baa157e5145665d78838fe189008e921c0971eb" dependencies = [ "cc", "tree-sitter-language", @@ -1386,9 +1405,9 @@ [[package]] name = "tree-sitter-java-orchard" -version = "0.5.10" +version = "0.5.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4cc3d047c3a4ebc4f59ef396031db2b82552aa4d1016c5697c5d611b7bd2e6b6" +checksum = "d653f4d9f16496ec19de6bcb7b7688d3f6b203c9c0af979bd5294fc058a3e16f" dependencies = [ "cc", "tree-sitter-language", @@ -1562,9 +1581,9 @@ [[package]] name = "tree-sitter-scala" -version = "0.26.0" +version = "0.26.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "de5a4a7ff23a55474ce6a741d52aaeca7a82fe9421bb982b86e98c6ac8629397" +checksum = "24e0ab4505990bfe30051761d40a7bf4033ce5a81c9eda9e20e987a5cdc84826" dependencies = [ "cc", "tree-sitter-language", diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/mergiraf-0.19.0/Cargo.toml new/mergiraf-0.19.1/Cargo.toml --- old/mergiraf-0.19.0/Cargo.toml 2026-08-16 17:58:44.000000000 +0200 +++ new/mergiraf-0.19.1/Cargo.toml 2026-09-01 10:50:21.000000000 +0200 @@ -11,7 +11,7 @@ [package] name = "mergiraf" -version = "0.19.0" +version = "0.19.1" edition = "2024" license = "GPL-3.0-only" repository = "https://codeberg.org/mergiraf/mergiraf" @@ -44,7 +44,7 @@ tree-sitter-go = "0.25" tree-sitter-gomod-orchard = "0.5" tree-sitter-gosum-orchard = "0.3" -tree-sitter-dart-orchard = "0.5" +tree-sitter-dart-orchard = "0.6" tree-sitter-typescript = "0.23" tree-sitter-python-orchard = "0.4" tree-sitter-php = "0.24" @@ -103,6 +103,7 @@ insta = { version = "1.43", default-features = false, features = ["colors"] } pretty_assertions = "1.4.1" assert_cmd = { version = "2.2.0", features = ["color"] } +predicates = "3.1.4" [features] dev = ["dep:tempfile"] diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/mergiraf-0.19.0/src/attempts.rs new/mergiraf-0.19.1/src/attempts.rs --- old/mergiraf-0.19.0/src/attempts.rs 2026-08-16 17:58:44.000000000 +0200 +++ new/mergiraf-0.19.1/src/attempts.rs 2026-09-01 10:50:21.000000000 +0200 @@ -91,7 +91,7 @@ app_name: "Mergiraf".to_owned(), }) .ok(); - strategy.map(|project_dir| project_dir.data_dir().clone().join(ATTEMPTS_DIRECTORY)) + strategy.map(|project_dir| project_dir.in_data_dir(ATTEMPTS_DIRECTORY)) }) .ok_or_else(|| { "Could not determine a suitable application data directory to store merge attempts" diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/mergiraf-0.19.0/src/lib.rs new/mergiraf-0.19.1/src/lib.rs --- old/mergiraf-0.19.0/src/lib.rs 2026-08-16 17:58:44.000000000 +0200 +++ new/mergiraf-0.19.1/src/lib.rs 2026-09-01 10:50:21.000000000 +0200 @@ -68,14 +68,14 @@ /// ``` pub const ENABLING_ENV_VAR: &str = "mergiraf"; -pub const EXIT_SUCCESS: i32 = 0; +pub const EXIT_SUCCESS: u8 = 0; // Exit code for merge sub command -pub const EXIT_MERGE_HAS_CONFLICTS: i32 = 1; +pub const EXIT_MERGE_HAS_CONFLICTS: u8 = 1; //Exit code for solve sub command -pub const EXIT_SOLVE_FAILED: i32 = 1; -pub const EXIT_SOLVE_HAS_CONFLICTS: i32 = 2; +pub const EXIT_SOLVE_FAILED: u8 = 1; +pub const EXIT_SOLVE_HAS_CONFLICTS: u8 = 2; pub use merge::line_merge_and_structured_resolution; diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/mergiraf-0.19.0/src/main.rs new/mergiraf-0.19.1/src/main.rs --- old/mergiraf-0.19.0/src/main.rs 2026-08-16 17:58:44.000000000 +0200 +++ new/mergiraf-0.19.1/src/main.rs 2026-09-01 10:50:21.000000000 +0200 @@ -1,13 +1,13 @@ use std::{ borrow::Cow, - env, fs, io, + env, fs, path::{Path, PathBuf}, - process::{Command, exit}, + process::{Command, ExitCode}, time::Duration, }; use clap::{ArgAction, Args, Parser, Subcommand}; -use log::warn; +use log::{error, warn}; use mergiraf::{ ENABLING_ENV_VAR, EXIT_MERGE_HAS_CONFLICTS, EXIT_SOLVE_FAILED, EXIT_SOLVE_HAS_CONFLICTS, EXIT_SUCCESS, @@ -17,7 +17,7 @@ newline::{imitate_newline_style, infer_newline_style, normalize_to_lf}, settings::{ConflictRegexes, DisplaySettings}, solve, - utils::{read_file_to_string, write_string_to_file}, + utils::{buffer_is_binary, read_file, read_file_to_string, write_string_to_file}, }; /// Syntax-aware merge driver for Git. @@ -140,7 +140,17 @@ }, } -fn main() { +#[derive(thiserror::Error, Debug)] +pub enum CliError { + #[error("`git merge-file` returned exit code {exit_code}")] + GitMergeFile { exit_code: i32 }, + #[error("IO error: {0}")] + Io(#[from] std::io::Error), + #[error("{0}")] + Other(String), +} + +fn main() -> ExitCode { let args = CliArgs::parse(); stderrlog::new() @@ -150,15 +160,19 @@ .unwrap(); match real_main(args) { - Ok(exit_code) => exit(exit_code), + Ok(exit_code) => exit_code, + // If we got an internal error from `git merge-file`, we translate that + // to a conflicted merge, so that we don't abort the overall merge. + // See https://codeberg.org/mergiraf/mergiraf/issues/812. + Err(CliError::GitMergeFile { .. }) => ExitCode::from(EXIT_MERGE_HAS_CONFLICTS), Err(error) => { eprintln!("Mergiraf: {error}"); - exit(-1) + ExitCode::from(255) } } } -fn real_main(args: CliArgs) -> Result<i32, String> { +fn real_main(args: CliArgs) -> Result<ExitCode, CliError> { let return_code = match args.command { CliCommand::Merge { base, @@ -215,38 +229,51 @@ let mergiraf_disabled = env::var(ENABLING_ENV_VAR).as_deref() == Ok("0"); if mergiraf_disabled { - return fallback_to_git_merge_file(base, left, right, git, &output, &settings) - .map_err(|e| format!("error when calling git-merge-file: {e}")); + return fallback_to_git_merge_file(base, left, right, git, &output, &settings); } } if let Some(debug_dir) = debug_dir { - fs::create_dir_all(debug_dir) - .map_err(|err| format!("could not create the debug directory: {err}"))?; + fs::create_dir_all(debug_dir).map_err(|err| { + CliError::Other(format!("could not create the debug directory: {err}")) + })?; } let fname_base = &*base; let fname_left = &*left; let fname_right = &*right; + let original_contents_base = read_file(fname_base).map_err(CliError::Other)?; + let original_contents_left = read_file(fname_left).map_err(CliError::Other)?; + let original_contents_right = read_file(fname_right).map_err(CliError::Other)?; + + if [ + &*original_contents_base, + &*original_contents_left, + &*original_contents_base, + ] + .into_iter() + .any(buffer_is_binary) + { + // Don't `return Err` here, as that would make `main` exit with 255, which would + // abort the overall merge. See https://codeberg.org/mergiraf/mergiraf/pulls/81 + error!("cannot merge binary files"); + return Ok(ExitCode::from(EXIT_MERGE_HAS_CONFLICTS)); + } + let ( Ok(original_contents_base), Ok(original_contents_left), Ok(original_contents_right), ) = ( - read_file_to_string(fname_base), - read_file_to_string(fname_left), - read_file_to_string(fname_right), + String::from_utf8(original_contents_base), + String::from_utf8(original_contents_left), + String::from_utf8(original_contents_right), ) else { - // The case we're actually catching here is an input file being non-UTF-8. - // - // The way this is currently implemented, this will also catch IO errors - // like a file not being present etc. -- but that's okay, since in that case - // the output of `git merge-file` is comparable to what we would've emitted - // (debug representation of `io::Error`) - return fallback_to_git_merge_file(base, left, right, git, &output, &settings) - .map_err(|e| format!("error when calling git-merge-file: {e}")); + // if you change this warning message, update `misc::test_git_merge_file_fallback_on_files` + warn!("input files are not UTF-8, falling back to Git"); + return fallback_to_git_merge_file(base, left, right, git, &output, &settings); }; { @@ -268,8 +295,7 @@ warn!("{side} side contains conflict markers, falling back to Git"); return fallback_to_git_merge_file( base, left, right, git, &output, &settings, - ) - .map_err(|e| format!("error when calling git-merge-file: {e}")); + ); } } } @@ -305,9 +331,11 @@ merge_result.contents = imitate_newline_style(&merge_result.contents, original_newline_style); if let Some(fname_out) = output { - write_string_to_file(&fname_out, &merge_result.contents)?; + write_string_to_file(&fname_out, &merge_result.contents) + .map_err(CliError::Other)?; } else if git { - write_string_to_file(fname_left, &merge_result.contents)?; + write_string_to_file(fname_left, &merge_result.contents) + .map_err(CliError::Other)?; } else { print!("{}", merge_result.contents); } @@ -337,13 +365,15 @@ keep_backup, } => { if let Some(debug_dir) = &debug_dir { - fs::create_dir_all(debug_dir) - .map_err(|err| format!("could not create the debug directory: {err}"))?; + fs::create_dir_all(debug_dir).map_err(|err| { + CliError::Other(format!("could not create the debug directory: {err}")) + })?; } // Unlike `mergiraf merge`, there is no `git merge-file` we can fall back on in case of // non-UTF-8 input, so just bail out. - let original_conflict_contents = read_file_to_string(&fname_conflicts)?; + let original_conflict_contents = + read_file_to_string(&fname_conflicts).map_err(CliError::Other)?; if file_seems_to_have_a_jj_conflict(&fname_conflicts, &original_conflict_contents) { // Our current logger doesn't handle multiline messages well, so we split them manually. @@ -385,12 +415,14 @@ if stdout { print!("{}", merged.contents); } else { - write_string_to_file(&fname_conflicts, &merged.contents)?; + write_string_to_file(&fname_conflicts, &merged.contents) + .map_err(CliError::Other)?; if keep_backup { write_string_to_file( fname_conflicts.with_added_extension("orig"), &original_conflict_contents, - )?; + ) + .map_err(CliError::Other)?; } }; if merged.conflict_count > 0 { @@ -406,8 +438,10 @@ } } CliCommand::Review { merge_id } => { - let attempts_cache = AttemptsCache::new(None, None)?; - attempts_cache.review_merge(&merge_id)?; + let attempts_cache = AttemptsCache::new(None, None).map_err(CliError::Other)?; + attempts_cache + .review_merge(&merge_id) + .map_err(CliError::Other)?; EXIT_SUCCESS } CliCommand::Languages { gitattributes } => { @@ -416,11 +450,11 @@ EXIT_SUCCESS } CliCommand::Report { merge_id_or_file } => { - report_bug(&merge_id_or_file)?; + report_bug(&merge_id_or_file).map_err(CliError::Other)?; EXIT_SUCCESS } }; - Ok(return_code) + Ok(ExitCode::from(return_code)) } fn fallback_to_git_merge_file( @@ -430,7 +464,7 @@ git: bool, output: &Option<PathBuf>, settings: &DisplaySettings, -) -> io::Result<i32> { +) -> Result<ExitCode, CliError> { let mut command = Command::new("git"); command.arg("merge-file").arg("--diff-algorithm=histogram"); if !git { @@ -460,7 +494,19 @@ } else { command.spawn()?.wait()? }; - Ok(code.code().unwrap_or(0)) + let code = code.code().unwrap_or(0); + if code >= 128 { + Err(CliError::GitMergeFile { exit_code: code }) + } else { + // we cannot return the exact same exit code as Git returned to us, + // because Rust exposes that to us as an i32 and we need to return an + // ExitCode (u8), so we map all other errors to 1 (signalling a conflict state) + Ok(ExitCode::from(if code == 0 { + EXIT_SUCCESS + } else { + EXIT_MERGE_HAS_CONFLICTS + })) + } } /// Check if user is using Jujutsu instead of Git, which can lead to issues when running diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/mergiraf-0.19.0/src/snapshots/mergiraf__supported_langs__test__children_of_commutative_parents_have_sigs.snap new/mergiraf-0.19.1/src/snapshots/mergiraf__supported_langs__test__children_of_commutative_parents_have_sigs.snap --- old/mergiraf-0.19.0/src/snapshots/mergiraf__supported_langs__test__children_of_commutative_parents_have_sigs.snap 2026-08-16 17:58:44.000000000 +0200 +++ new/mergiraf-0.19.1/src/snapshots/mergiraf__supported_langs__test__children_of_commutative_parents_have_sigs.snap 2026-09-01 10:50:21.000000000 +0200 @@ -11,9 +11,6 @@ "C#: child `property_declaration` of commutative parent 'declaration_list' doesn't have a signature defined", "Go: child `literal_element` of commutative parent 'literal_value' doesn't have a signature defined", "Java: child `type_identifier` of commutative parent 'throws' doesn't have a signature defined", - "Kotlin: child `inheritance_modifier` of commutative parent 'modifiers' doesn't have a signature defined", - "Kotlin: child `member_modifier` of commutative parent 'modifiers' doesn't have a signature defined", - "Kotlin: child `visibility_modifier` of commutative parent 'modifiers' doesn't have a signature defined", "Markdown: child `link_reference_definition` of commutative parent 'section' doesn't have a signature defined", "PHP: child `use_declaration` of commutative parent 'declaration_list' doesn't have a signature defined", "Python: child `import_statement` of commutative parent 'module' doesn't have a signature defined", diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/mergiraf-0.19.0/src/supported_langs.rs new/mergiraf-0.19.1/src/supported_langs.rs --- old/mergiraf-0.19.0/src/supported_langs.rs 2026-08-16 17:58:44.000000000 +0200 +++ new/mergiraf-0.19.1/src/supported_langs.rs 2026-09-01 10:50:21.000000000 +0200 @@ -262,14 +262,9 @@ signature("delegation_specifier", vec![vec![]]), // modifiers signature("annotation", vec![vec![]]), // annotations can be repeatable, so we can't use the name as key - signature("public", vec![vec![]]), - signature("protected", vec![vec![]]), - signature("private", vec![vec![]]), - signature("internal", vec![vec![]]), - signature("final", vec![vec![]]), - signature("open", vec![vec![]]), - signature("abstract", vec![vec![]]), - signature("override", vec![vec![]]), + signature("visibility_modifier", vec![]), // a single visibility modifier is allowed on any given item + signature("inheritance_modifier", vec![]), // a single visibility modifier is allowed on any given item + signature("member_modifier", vec![vec![]]), // potentially multiple member modifiers on the same element, but all distinct ], injections: None, flattened_nodes: &[], diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/mergiraf-0.19.0/src/utils.rs new/mergiraf-0.19.1/src/utils.rs --- old/mergiraf-0.19.0/src/utils.rs 2026-08-16 17:58:44.000000000 +0200 +++ new/mergiraf-0.19.1/src/utils.rs 2026-09-01 10:50:21.000000000 +0200 @@ -1,6 +1,12 @@ +use std::cmp::min; use std::error::Error; use std::{fs, path::Path}; +pub fn read_file(path: impl AsRef<Path>) -> Result<Vec<u8>, String> { + let path = path.as_ref(); + fs::read(path).map_err(|err| format!("Could not read '{}': {err}", path.display())) +} + pub fn read_file_to_string(path: impl AsRef<Path>) -> Result<String, String> { let path = path.as_ref(); fs::read_to_string(path).map_err(|err| format!("Could not read '{}': {err}", path.display())) @@ -44,3 +50,22 @@ } } } + +// Stolen from https://git.kernel.org/pub/scm/git/git.git/tree/xdiff-interface.c?commit=db6938689f0a9ef5a9d630e8614d7f807758ff39#n197 +pub fn buffer_is_binary(buf: &[u8]) -> bool { + const FIRST_FEW_BYTES: usize = 8000; + buf[..min(buf.len(), FIRST_FEW_BYTES)].contains(&0) +} + +#[cfg(test)] +mod tests { + #[test] + #[allow( + clippy::bool_assert_comparison, + reason = "the suggestion makes it easy to miss the `!`" + )] + fn buffer_is_binary() { + assert_eq!(super::buffer_is_binary(b"\0\xff\0"), true); + assert_eq!(super::buffer_is_binary(b"I'm non-binary"), false); + } +} diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/mergiraf-0.19.0/tests/common/mod.rs new/mergiraf-0.19.1/tests/common/mod.rs --- old/mergiraf-0.19.0/tests/common/mod.rs 2026-08-16 17:58:44.000000000 +0200 +++ new/mergiraf-0.19.1/tests/common/mod.rs 2026-09-01 10:50:21.000000000 +0200 @@ -3,7 +3,7 @@ use core::str; use std::fs::{self, read_to_string}; use std::path::{Path, PathBuf}; -use std::process::Command; +use std::process::{Command, Output}; use assert_cmd::{pkg_name, prelude::*}; use itertools::Itertools; @@ -12,15 +12,9 @@ pub const DEFAULT_FILE_FOR_SOLVE: &str = "<<<<<<< LEFT\n[1, 2, 3, 4]\n||||||| BASE\n[1, 2, 3]\n=======\n[0, 1, 2, 3]\n>>>>>>> RIGHT\n"; -pub(crate) fn run_git(args: &[&str], repo_dir: &Path) { +pub(crate) fn run_git(args: &[&str], repo_dir: &Path) -> Output { let command_str = format!("git {}", args.iter().format(" ")); - let mut command = Command::new("git"); - command.current_dir(repo_dir); - command.args(args); - // Run using a minimal environment to isolate the test better. - command - .env_clear() - .envs(std::env::vars().filter(|(var, _)| var == "PATH")); + let mut command = git_command(args, repo_dir); let output = command.output().expect("Failed to execute git command"); if !output.status.success() { panic!( @@ -28,6 +22,39 @@ str::from_utf8(&output.stdout).unwrap() ); } + output +} + +pub(crate) fn git_command(args: &[&str], repo_dir: &Path) -> Command { + let mut command = Command::new("git"); + command.current_dir(repo_dir); + command.args(args); + // Run using a minimal environment to isolate the test better. + command + .env_clear() + .envs(std::env::vars().filter(|(var, _)| var == "PATH")); + command +} + +/// Given a path to a repo, set up mergiraf as merge driver +pub(crate) fn setup_mergiraf(repo_path: &Path) { + let mergiraf_command = Command::cargo_bin(pkg_name!()).unwrap(); + let mergiraf_binary = mergiraf_command.get_program().to_string_lossy(); + + run_git(&["config", "user.email", "[email protected]"], repo_path); + run_git(&["config", "user.name", "Test User"], repo_path); + run_git(&["config", "merge.mergiraf.name", "mergiraf"], repo_path); + run_git( + &[ + "config", + "merge.mergiraf.driver", + &format!("{mergiraf_binary} merge --git %O %A %B -s %S -x %X -y %Y -p %P -l %L"), + ], + repo_path, + ); + + fs::write(repo_path.join(".git/info/attributes"), "* merge=mergiraf\n") + .expect("failed to write .gitattributes"); } pub(crate) fn write_file_from_rev( diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/mergiraf-0.19.0/tests/data/kotlin/failing/function_modifiers/Base.kt new/mergiraf-0.19.1/tests/data/kotlin/failing/function_modifiers/Base.kt --- old/mergiraf-0.19.0/tests/data/kotlin/failing/function_modifiers/Base.kt 1970-01-01 01:00:00.000000000 +0100 +++ new/mergiraf-0.19.1/tests/data/kotlin/failing/function_modifiers/Base.kt 2026-09-01 10:50:21.000000000 +0200 @@ -0,0 +1,5 @@ +object Recipe { + override fun demonstrate() { + LOGGER.info { "Demo time!" } + } +} diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/mergiraf-0.19.0/tests/data/kotlin/failing/function_modifiers/ExpectedCurrently.kt new/mergiraf-0.19.1/tests/data/kotlin/failing/function_modifiers/ExpectedCurrently.kt --- old/mergiraf-0.19.0/tests/data/kotlin/failing/function_modifiers/ExpectedCurrently.kt 1970-01-01 01:00:00.000000000 +0100 +++ new/mergiraf-0.19.1/tests/data/kotlin/failing/function_modifiers/ExpectedCurrently.kt 2026-09-01 10:50:21.000000000 +0200 @@ -0,0 +1,11 @@ +object Recipe { +<<<<<<< LEFT + override final fun demonstrate() { +||||||| BASE + override fun demonstrate() { +======= + override protected fun demonstrate() { +>>>>>>> RIGHT + LOGGER.info { "Demo time!" } + } +} diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/mergiraf-0.19.0/tests/data/kotlin/failing/function_modifiers/ExpectedIdeally.kt new/mergiraf-0.19.1/tests/data/kotlin/failing/function_modifiers/ExpectedIdeally.kt --- old/mergiraf-0.19.0/tests/data/kotlin/failing/function_modifiers/ExpectedIdeally.kt 1970-01-01 01:00:00.000000000 +0100 +++ new/mergiraf-0.19.1/tests/data/kotlin/failing/function_modifiers/ExpectedIdeally.kt 2026-09-01 10:50:21.000000000 +0200 @@ -0,0 +1,11 @@ +object Recipe { +<<<<<<< LEFT + override final fun demonstrate() { +||||||| BASE + override fun demonstrate() { +======= + override protected fun demonstrate() { +>>>>>>> RIGHT + LOGGER.info { "Demo time!" } + } +} diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/mergiraf-0.19.0/tests/data/kotlin/failing/function_modifiers/Left.kt new/mergiraf-0.19.1/tests/data/kotlin/failing/function_modifiers/Left.kt --- old/mergiraf-0.19.0/tests/data/kotlin/failing/function_modifiers/Left.kt 1970-01-01 01:00:00.000000000 +0100 +++ new/mergiraf-0.19.1/tests/data/kotlin/failing/function_modifiers/Left.kt 2026-09-01 10:50:21.000000000 +0200 @@ -0,0 +1,5 @@ +object Recipe { + override final fun demonstrate() { + LOGGER.info { "Demo time!" } + } +} diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/mergiraf-0.19.0/tests/data/kotlin/failing/function_modifiers/Right.kt new/mergiraf-0.19.1/tests/data/kotlin/failing/function_modifiers/Right.kt --- old/mergiraf-0.19.0/tests/data/kotlin/failing/function_modifiers/Right.kt 1970-01-01 01:00:00.000000000 +0100 +++ new/mergiraf-0.19.1/tests/data/kotlin/failing/function_modifiers/Right.kt 2026-09-01 10:50:21.000000000 +0200 @@ -0,0 +1,5 @@ +object Recipe { + override protected fun demonstrate() { + LOGGER.info { "Demo time!" } + } +} diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/mergiraf-0.19.0/tests/data/kotlin/working/conflicting_inheritance_modifiers/Base.kt new/mergiraf-0.19.1/tests/data/kotlin/working/conflicting_inheritance_modifiers/Base.kt --- old/mergiraf-0.19.0/tests/data/kotlin/working/conflicting_inheritance_modifiers/Base.kt 1970-01-01 01:00:00.000000000 +0100 +++ new/mergiraf-0.19.1/tests/data/kotlin/working/conflicting_inheritance_modifiers/Base.kt 2026-09-01 10:50:21.000000000 +0200 @@ -0,0 +1,2 @@ +class Recipe { +} diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/mergiraf-0.19.0/tests/data/kotlin/working/conflicting_inheritance_modifiers/Expected.kt new/mergiraf-0.19.1/tests/data/kotlin/working/conflicting_inheritance_modifiers/Expected.kt --- old/mergiraf-0.19.0/tests/data/kotlin/working/conflicting_inheritance_modifiers/Expected.kt 1970-01-01 01:00:00.000000000 +0100 +++ new/mergiraf-0.19.1/tests/data/kotlin/working/conflicting_inheritance_modifiers/Expected.kt 2026-09-01 10:50:21.000000000 +0200 @@ -0,0 +1,8 @@ +<<<<<<< LEFT +open class Recipe { +||||||| BASE +class Recipe { +======= +final class Recipe { +>>>>>>> RIGHT +} diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/mergiraf-0.19.0/tests/data/kotlin/working/conflicting_inheritance_modifiers/Left.kt new/mergiraf-0.19.1/tests/data/kotlin/working/conflicting_inheritance_modifiers/Left.kt --- old/mergiraf-0.19.0/tests/data/kotlin/working/conflicting_inheritance_modifiers/Left.kt 1970-01-01 01:00:00.000000000 +0100 +++ new/mergiraf-0.19.1/tests/data/kotlin/working/conflicting_inheritance_modifiers/Left.kt 2026-09-01 10:50:21.000000000 +0200 @@ -0,0 +1,2 @@ +open class Recipe { +} diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/mergiraf-0.19.0/tests/data/kotlin/working/conflicting_inheritance_modifiers/Right.kt new/mergiraf-0.19.1/tests/data/kotlin/working/conflicting_inheritance_modifiers/Right.kt --- old/mergiraf-0.19.0/tests/data/kotlin/working/conflicting_inheritance_modifiers/Right.kt 1970-01-01 01:00:00.000000000 +0100 +++ new/mergiraf-0.19.1/tests/data/kotlin/working/conflicting_inheritance_modifiers/Right.kt 2026-09-01 10:50:21.000000000 +0200 @@ -0,0 +1,2 @@ +final class Recipe { +} diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/mergiraf-0.19.0/tests/data/kotlin/working/conflicting_visibility_modifier/Base.kt new/mergiraf-0.19.1/tests/data/kotlin/working/conflicting_visibility_modifier/Base.kt --- old/mergiraf-0.19.0/tests/data/kotlin/working/conflicting_visibility_modifier/Base.kt 1970-01-01 01:00:00.000000000 +0100 +++ new/mergiraf-0.19.1/tests/data/kotlin/working/conflicting_visibility_modifier/Base.kt 2026-09-01 10:50:21.000000000 +0200 @@ -0,0 +1,5 @@ +object DungeonFinderFeatures { + + fun onInventoryOpen() { + } +} diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/mergiraf-0.19.0/tests/data/kotlin/working/conflicting_visibility_modifier/Expected.kt new/mergiraf-0.19.1/tests/data/kotlin/working/conflicting_visibility_modifier/Expected.kt --- old/mergiraf-0.19.0/tests/data/kotlin/working/conflicting_visibility_modifier/Expected.kt 1970-01-01 01:00:00.000000000 +0100 +++ new/mergiraf-0.19.1/tests/data/kotlin/working/conflicting_visibility_modifier/Expected.kt 2026-09-01 10:50:21.000000000 +0200 @@ -0,0 +1,11 @@ +object DungeonFinderFeatures { + +<<<<<<< LEFT + public fun onInventoryOpen() { +||||||| BASE + fun onInventoryOpen() { +======= + private fun onInventoryOpen() { +>>>>>>> RIGHT + } +} diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/mergiraf-0.19.0/tests/data/kotlin/working/conflicting_visibility_modifier/Left.kt new/mergiraf-0.19.1/tests/data/kotlin/working/conflicting_visibility_modifier/Left.kt --- old/mergiraf-0.19.0/tests/data/kotlin/working/conflicting_visibility_modifier/Left.kt 1970-01-01 01:00:00.000000000 +0100 +++ new/mergiraf-0.19.1/tests/data/kotlin/working/conflicting_visibility_modifier/Left.kt 2026-09-01 10:50:21.000000000 +0200 @@ -0,0 +1,5 @@ +object DungeonFinderFeatures { + + public fun onInventoryOpen() { + } +} diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/mergiraf-0.19.0/tests/data/kotlin/working/conflicting_visibility_modifier/Right.kt new/mergiraf-0.19.1/tests/data/kotlin/working/conflicting_visibility_modifier/Right.kt --- old/mergiraf-0.19.0/tests/data/kotlin/working/conflicting_visibility_modifier/Right.kt 1970-01-01 01:00:00.000000000 +0100 +++ new/mergiraf-0.19.1/tests/data/kotlin/working/conflicting_visibility_modifier/Right.kt 2026-09-01 10:50:21.000000000 +0200 @@ -0,0 +1,5 @@ +object DungeonFinderFeatures { + + private fun onInventoryOpen() { + } +} diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/mergiraf-0.19.0/tests/data/kotlin/working/duplicate_visibility_modifier/Base.kt new/mergiraf-0.19.1/tests/data/kotlin/working/duplicate_visibility_modifier/Base.kt --- old/mergiraf-0.19.0/tests/data/kotlin/working/duplicate_visibility_modifier/Base.kt 1970-01-01 01:00:00.000000000 +0100 +++ new/mergiraf-0.19.1/tests/data/kotlin/working/duplicate_visibility_modifier/Base.kt 2026-09-01 10:50:21.000000000 +0200 @@ -0,0 +1,8 @@ +object DungeonFinderFeatures { + + @HandleEvent(onlyOnSkyblock = true) fun onInventoryOpen() { + floorStackSize = stackTip(event) + highlightParty = highlightingHandler(event) + toolTipMap = toolTipHandler(event) + } +} \ No newline at end of file diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/mergiraf-0.19.0/tests/data/kotlin/working/duplicate_visibility_modifier/Expected.kt new/mergiraf-0.19.1/tests/data/kotlin/working/duplicate_visibility_modifier/Expected.kt --- old/mergiraf-0.19.0/tests/data/kotlin/working/duplicate_visibility_modifier/Expected.kt 1970-01-01 01:00:00.000000000 +0100 +++ new/mergiraf-0.19.1/tests/data/kotlin/working/duplicate_visibility_modifier/Expected.kt 2026-09-01 10:50:21.000000000 +0200 @@ -0,0 +1,11 @@ +object DungeonFinderFeatures { + + @HandleEvent(onlyOnSkyblock = true) + private fun onInventoryOpen() { + val inventoryName = event.inventoryName + val inventoryItems = event.inventoryItems + floorStackSize = stackTip(inventoryName, inventoryItems) + highlightParty = highlightingHandler(inventoryName, inventoryItems) + toolTipMap = toolTipHandler(inventoryName, inventoryItems) + } +} \ No newline at end of file diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/mergiraf-0.19.0/tests/data/kotlin/working/duplicate_visibility_modifier/Left.kt new/mergiraf-0.19.1/tests/data/kotlin/working/duplicate_visibility_modifier/Left.kt --- old/mergiraf-0.19.0/tests/data/kotlin/working/duplicate_visibility_modifier/Left.kt 1970-01-01 01:00:00.000000000 +0100 +++ new/mergiraf-0.19.1/tests/data/kotlin/working/duplicate_visibility_modifier/Left.kt 2026-09-01 10:50:21.000000000 +0200 @@ -0,0 +1,11 @@ +object DungeonFinderFeatures { + + @HandleEvent(onlyOnSkyblock = true) + private fun onInventoryOpen() { + val inventoryName = event.inventoryName + val inventoryItems = event.inventoryItems + floorStackSize = stackTip(inventoryName, inventoryItems) + highlightParty = highlightingHandler(inventoryName, inventoryItems) + toolTipMap = toolTipHandler(inventoryName, inventoryItems) + } +} \ No newline at end of file diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/mergiraf-0.19.0/tests/data/kotlin/working/duplicate_visibility_modifier/Right.kt new/mergiraf-0.19.1/tests/data/kotlin/working/duplicate_visibility_modifier/Right.kt --- old/mergiraf-0.19.0/tests/data/kotlin/working/duplicate_visibility_modifier/Right.kt 1970-01-01 01:00:00.000000000 +0100 +++ new/mergiraf-0.19.1/tests/data/kotlin/working/duplicate_visibility_modifier/Right.kt 2026-09-01 10:50:21.000000000 +0200 @@ -0,0 +1,9 @@ +object DungeonFinderFeatures { + + @HandleEvent(onlyOnSkyblock = true) + private fun onInventoryOpen() { + floorStackSize = stackTip(event) + highlightParty = highlightingHandler(event) + toolTipMap = toolTipHandler(event) + } +} \ No newline at end of file diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/mergiraf-0.19.0/tests/data/kotlin/working/function_modifiers/Base.kt new/mergiraf-0.19.1/tests/data/kotlin/working/function_modifiers/Base.kt --- old/mergiraf-0.19.0/tests/data/kotlin/working/function_modifiers/Base.kt 2026-08-16 17:58:44.000000000 +0200 +++ new/mergiraf-0.19.1/tests/data/kotlin/working/function_modifiers/Base.kt 1970-01-01 01:00:00.000000000 +0100 @@ -1,5 +0,0 @@ -object Recipe { - override fun demonstrate() { - LOGGER.info { "Demo time!" } - } -} diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/mergiraf-0.19.0/tests/data/kotlin/working/function_modifiers/Expected.kt new/mergiraf-0.19.1/tests/data/kotlin/working/function_modifiers/Expected.kt --- old/mergiraf-0.19.0/tests/data/kotlin/working/function_modifiers/Expected.kt 2026-08-16 17:58:44.000000000 +0200 +++ new/mergiraf-0.19.1/tests/data/kotlin/working/function_modifiers/Expected.kt 1970-01-01 01:00:00.000000000 +0100 @@ -1,5 +0,0 @@ -object Recipe { - override final protected fun demonstrate() { - LOGGER.info { "Demo time!" } - } -} diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/mergiraf-0.19.0/tests/data/kotlin/working/function_modifiers/Left.kt new/mergiraf-0.19.1/tests/data/kotlin/working/function_modifiers/Left.kt --- old/mergiraf-0.19.0/tests/data/kotlin/working/function_modifiers/Left.kt 2026-08-16 17:58:44.000000000 +0200 +++ new/mergiraf-0.19.1/tests/data/kotlin/working/function_modifiers/Left.kt 1970-01-01 01:00:00.000000000 +0100 @@ -1,5 +0,0 @@ -object Recipe { - override final fun demonstrate() { - LOGGER.info { "Demo time!" } - } -} diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/mergiraf-0.19.0/tests/data/kotlin/working/function_modifiers/Right.kt new/mergiraf-0.19.1/tests/data/kotlin/working/function_modifiers/Right.kt --- old/mergiraf-0.19.0/tests/data/kotlin/working/function_modifiers/Right.kt 2026-08-16 17:58:44.000000000 +0200 +++ new/mergiraf-0.19.1/tests/data/kotlin/working/function_modifiers/Right.kt 1970-01-01 01:00:00.000000000 +0100 @@ -1,5 +0,0 @@ -object Recipe { - override protected fun demonstrate() { - LOGGER.info { "Demo time!" } - } -} diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/mergiraf-0.19.0/tests/misc.rs new/mergiraf-0.19.1/tests/misc.rs --- old/mergiraf-0.19.0/tests/misc.rs 2026-08-16 17:58:44.000000000 +0200 +++ new/mergiraf-0.19.1/tests/misc.rs 2026-09-01 10:50:21.000000000 +0200 @@ -1,10 +1,15 @@ use assert_cmd::prelude::*; use mergiraf::{EXIT_SOLVE_HAS_CONFLICTS, git, utils::write_string_to_file}; +use predicates::prelude::PredicateBooleanExt as _; +use predicates::str::contains; use std::fs; use std::path::{Path, PathBuf}; mod common; -use common::{DEFAULT_FILE_FOR_SOLVE, create_file_for_solve, create_files_for_merge, merge, solve}; +use common::{ + DEFAULT_FILE_FOR_SOLVE, create_file_for_solve, create_files_for_merge, git_command, merge, + run_git, setup_mergiraf, solve, +}; #[test] fn keep_backup_keeps_backup() { @@ -360,7 +365,7 @@ .arg("--language=html") .arg("--stdout") .assert() - .code(EXIT_SOLVE_HAS_CONFLICTS) + .code(EXIT_SOLVE_HAS_CONFLICTS as i32) .stdout(content); } @@ -422,6 +427,55 @@ } #[test] +fn merging_files_with_binary_files_doesnt_abort_whole_merge() { + test_git_merge_file_fallback_on_files(false); +} + +#[test] +fn merging_files_with_disabled_mergiraf_doesnt_abort_whole_merge() { + test_git_merge_file_fallback_on_files(true); +} + +fn test_git_merge_file_fallback_on_files(disabled_mergiraf: bool) { + let repo_dir = tempfile::tempdir().expect("failed to create the temp dir"); + let repo_path = repo_dir.path(); + + git::init(repo_path); + setup_mergiraf(repo_path); + + let contents_base = b"\0\xff\0"; + let contents_left = b"\0\0\x4a2\xff"; + let contents_right = b"\x003\0\0"; + + fs::write(repo_path.join("tracked_file"), contents_base).unwrap(); + run_git(&["add", "tracked_file"], repo_path); + run_git(&["commit", "-m", "initial"], repo_path); + run_git(&["checkout", "-b", "first_branch"], repo_path); + fs::write(repo_path.join("tracked_file"), contents_left).unwrap(); + run_git(&["commit", "-am", "second"], repo_path); + run_git(&["checkout", "HEAD~"], repo_path); + run_git(&["checkout", "-b", "second_branch"], repo_path); + fs::write(repo_path.join("tracked_file"), contents_right).unwrap(); + run_git(&["commit", "-am", "third"], repo_path); + let mut command = git_command(&["merge", "first_branch"], repo_path); + if disabled_mergiraf { + command.env("mergiraf", "0"); + } + command + .assert() + .append_context("main", "should fail on binary file conflict") + .failure() + // we don't assert the full output as it may not be stable between Git versions + .stderr(contains("WARN input files are not UTF-8, falling back to Git").not()); + + let output = run_git(&["status", "--porcelain"], repo_path); + let stdout = String::from_utf8(output.stdout).unwrap(); + + // the tracked file is marked as conflicted in git (meaning that the whole merge wasn't aborted) + assert!(stdout.contains("UU tracked_file\n"), "stdout={stdout}",); +} + +#[test] fn jj() { let repo_dir = tempfile::tempdir().expect("failed to create the temp dir"); let repo_path = repo_dir.path(); ++++++ mergiraf.obsinfo ++++++ --- /var/tmp/diff_new_pack.anfPkC/_old 2026-09-01 15:52:38.763735278 +0200 +++ /var/tmp/diff_new_pack.anfPkC/_new 2026-09-01 15:52:38.770735522 +0200 @@ -1,5 +1,5 @@ name: mergiraf -version: 0.19.0 -mtime: 1786895924 -commit: 7246b03c08c02248035bc20668fde1edf94a09e0 +version: 0.19.1 +mtime: 1788252621 +commit: e1c6514fb364faca51132be2b336aa739cdfadb9 ++++++ vendor.tar.zst ++++++ /work/SRC/openSUSE:Factory/mergiraf/vendor.tar.zst /work/SRC/openSUSE:Factory/.mergiraf.new.1265/vendor.tar.zst differ: char 7, line 1
