Skip to content

Commit e5cc468

Browse files
authored
merge: add an option to select how incomplete hunks are rendered (#88)
Commit 31de940 (merge: keep conflict markers on their own lines, #85) changed how conflict markers are rendered when a conflicting hunk ends in an incomplete line (one without a trailing newline), inserting a newline so that every marker starts at the beginning of a line. That matched the behavior of `git merge-file`, but silently diverged from GNU `diff3 -m`, which the diffutils manual documents as appending the succeeding markers directly to the incomplete line. With this commit, the behavior is now selectable via a new two-variant enum, `IncompleteHunkStyle`, on `MergeOptions`: * `Diff3` (the default) appends markers directly to the incomplete line, matching GNU `diff3 -m` and restoring the pre-#85 output. * `Git` inserts a newline after the incomplete line, matching `git merge-file`. Also add a table-driven test covering all eight permutations of the three inputs having or lacking a trailing newline, for both styles and for both the str and bytes paths. The expected outputs were verified against GNU diff3 3.12 and git 2.55.0: git produces byte-identical output for every permutation, while GNU diff3 glues each side's succeeding marker independently.
1 parent 51e8723 commit e5cc468

3 files changed

Lines changed: 315 additions & 17 deletions

File tree

‎src/lib.rs‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -330,6 +330,7 @@ pub use diff::DiffOptions;
330330
pub use diff::create_patch;
331331
pub use diff::create_patch_bytes;
332332
pub use merge::ConflictStyle;
333+
pub use merge::IncompleteHunkStyle;
333334
pub use merge::MergeOptions;
334335
pub use merge::merge;
335336
pub use merge::merge_bytes;

‎src/merge/mod.rs‎

Lines changed: 108 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,40 @@ pub enum ConflictStyle {
115115
Diff3,
116116
}
117117

118+
/// Style used when rendering conflict markers after a hunk that ends in an
119+
/// incomplete line (one without a trailing newline).
120+
///
121+
/// An incomplete line can only appear as the final line of a file, so this
122+
/// only affects conflicts that include the end of the file.
123+
#[derive(Copy, Clone, Debug)]
124+
pub enum IncompleteHunkStyle {
125+
/// Appends conflict markers directly to the incomplete line, matching the
126+
/// behavior of GNU `diff3 -m`.
127+
///
128+
/// ```console
129+
/// <<<<<<< ours
130+
/// ours line||||||| original
131+
/// original line=======
132+
/// theirs line>>>>>>> theirs
133+
/// ```
134+
Diff3,
135+
136+
/// Inserts a newline after the incomplete line so that every conflict
137+
/// marker starts at the beginning of a line, matching the behavior of
138+
/// `git merge-file`.
139+
///
140+
/// ```console
141+
/// <<<<<<< ours
142+
/// ours line
143+
/// ||||||| original
144+
/// original line
145+
/// =======
146+
/// theirs line
147+
/// >>>>>>> theirs
148+
/// ```
149+
Git,
150+
}
151+
118152
/// A collection of options for modifying the way a merge is performed
119153
///
120154
/// # Examples
@@ -145,6 +179,7 @@ pub enum ConflictStyle {
145179
pub struct MergeOptions {
146180
conflict_marker_length: usize,
147181
style: ConflictStyle,
182+
incomplete_hunk_style: IncompleteHunkStyle,
148183
}
149184

150185
impl MergeOptions {
@@ -153,10 +188,12 @@ impl MergeOptions {
153188
/// ## Defaults
154189
/// * conflict_marker_length = 7
155190
/// * style = ConflictStyle::Diff3
191+
/// * incomplete_hunk_style = IncompleteHunkStyle::Diff3
156192
pub fn new() -> Self {
157193
Self {
158194
conflict_marker_length: DEFAULT_CONFLICT_MARKER_LENGTH,
159195
style: ConflictStyle::Diff3,
196+
incomplete_hunk_style: IncompleteHunkStyle::Diff3,
160197
}
161198
}
162199

@@ -172,6 +209,13 @@ impl MergeOptions {
172209
self
173210
}
174211

212+
/// Set the style used when rendering conflict markers after a hunk that
213+
/// ends in an incomplete line
214+
pub fn set_incomplete_hunk_style(&mut self, style: IncompleteHunkStyle) -> &mut Self {
215+
self.incomplete_hunk_style = style;
216+
self
217+
}
218+
175219
/// Merge two files, given a common ancestor, based on the configured options
176220
pub fn merge<'a>(
177221
&self,
@@ -200,6 +244,7 @@ impl MergeOptions {
200244
&merge,
201245
self.conflict_marker_length,
202246
self.style,
247+
self.incomplete_hunk_style,
203248
)
204249
}
205250

@@ -231,6 +276,7 @@ impl MergeOptions {
231276
&merge,
232277
self.conflict_marker_length,
233278
self.style,
279+
self.incomplete_hunk_style,
234280
)
235281
}
236282
}
@@ -556,6 +602,7 @@ fn output_result<'a, T: ?Sized>(
556602
merge: &[MergeRange<T>],
557603
marker_len: usize,
558604
style: ConflictStyle,
605+
incomplete_hunk_style: IncompleteHunkStyle,
559606
) -> Result<String, String> {
560607
let mut conflicts = 0;
561608
let mut output = String::new();
@@ -566,17 +613,35 @@ fn output_result<'a, T: ?Sized>(
566613
output.extend(ancestor[range.range()].iter().copied());
567614
}
568615
MergeRange::Conflict(ancestor_range, ours_range, theirs_range) => {
569-
add_conflict_marker(&mut output, '<', marker_len, Some("ours"));
616+
add_conflict_marker(
617+
&mut output,
618+
'<',
619+
marker_len,
620+
Some("ours"),
621+
incomplete_hunk_style,
622+
);
570623
output.extend(ours[ours_range.range()].iter().copied());
571624

572625
if let ConflictStyle::Diff3 = style {
573-
add_conflict_marker(&mut output, '|', marker_len, Some("original"));
626+
add_conflict_marker(
627+
&mut output,
628+
'|',
629+
marker_len,
630+
Some("original"),
631+
incomplete_hunk_style,
632+
);
574633
output.extend(ancestor[ancestor_range.range()].iter().copied());
575634
}
576635

577-
add_conflict_marker(&mut output, '=', marker_len, None);
636+
add_conflict_marker(&mut output, '=', marker_len, None, incomplete_hunk_style);
578637
output.extend(theirs[theirs_range.range()].iter().copied());
579-
add_conflict_marker(&mut output, '>', marker_len, Some("theirs"));
638+
add_conflict_marker(
639+
&mut output,
640+
'>',
641+
marker_len,
642+
Some("theirs"),
643+
incomplete_hunk_style,
644+
);
580645
conflicts += 1;
581646
}
582647
MergeRange::Ours(range) => {
@@ -603,8 +668,12 @@ fn add_conflict_marker(
603668
marker: char,
604669
marker_len: usize,
605670
filename: Option<&str>,
671+
incomplete_hunk_style: IncompleteHunkStyle,
606672
) {
607-
if !output.is_empty() && !output.ends_with('\n') {
673+
if matches!(incomplete_hunk_style, IncompleteHunkStyle::Git)
674+
&& !output.is_empty()
675+
&& !output.ends_with('\n')
676+
{
608677
output.push('\n');
609678
}
610679
for _ in 0..marker_len {
@@ -625,6 +694,7 @@ fn output_result_bytes<'a, T: ?Sized>(
625694
merge: &[MergeRange<T>],
626695
marker_len: usize,
627696
style: ConflictStyle,
697+
incomplete_hunk_style: IncompleteHunkStyle,
628698
) -> Result<Vec<u8>, Vec<u8>> {
629699
let mut conflicts = 0;
630700
let mut output: Vec<u8> = Vec::new();
@@ -637,23 +707,47 @@ fn output_result_bytes<'a, T: ?Sized>(
637707
.for_each(|line| output.extend_from_slice(line));
638708
}
639709
MergeRange::Conflict(ancestor_range, ours_range, theirs_range) => {
640-
add_conflict_marker_bytes(&mut output, b'<', marker_len, Some(b"ours"));
710+
add_conflict_marker_bytes(
711+
&mut output,
712+
b'<',
713+
marker_len,
714+
Some(b"ours"),
715+
incomplete_hunk_style,
716+
);
641717
ours[ours_range.range()]
642718
.iter()
643719
.for_each(|line| output.extend_from_slice(line));
644720

645721
if let ConflictStyle::Diff3 = style {
646-
add_conflict_marker_bytes(&mut output, b'|', marker_len, Some(b"original"));
722+
add_conflict_marker_bytes(
723+
&mut output,
724+
b'|',
725+
marker_len,
726+
Some(b"original"),
727+
incomplete_hunk_style,
728+
);
647729
ancestor[ancestor_range.range()]
648730
.iter()
649731
.for_each(|line| output.extend_from_slice(line));
650732
}
651733

652-
add_conflict_marker_bytes(&mut output, b'=', marker_len, None);
734+
add_conflict_marker_bytes(
735+
&mut output,
736+
b'=',
737+
marker_len,
738+
None,
739+
incomplete_hunk_style,
740+
);
653741
theirs[theirs_range.range()]
654742
.iter()
655743
.for_each(|line| output.extend_from_slice(line));
656-
add_conflict_marker_bytes(&mut output, b'>', marker_len, Some(b"theirs"));
744+
add_conflict_marker_bytes(
745+
&mut output,
746+
b'>',
747+
marker_len,
748+
Some(b"theirs"),
749+
incomplete_hunk_style,
750+
);
657751
conflicts += 1;
658752
}
659753
MergeRange::Ours(range) => {
@@ -686,8 +780,12 @@ fn add_conflict_marker_bytes(
686780
marker: u8,
687781
marker_len: usize,
688782
filename: Option<&[u8]>,
783+
incomplete_hunk_style: IncompleteHunkStyle,
689784
) {
690-
if !output.is_empty() && output.last() != Some(&b'\n') {
785+
if matches!(incomplete_hunk_style, IncompleteHunkStyle::Git)
786+
&& !output.is_empty()
787+
&& output.last() != Some(&b'\n')
788+
{
691789
output.push(b'\n');
692790
}
693791
for _ in 0..marker_len {

0 commit comments

Comments
 (0)