aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorMattias Andrée <m@maandree.se>2026-09-24 23:19:13 +0200
committerMattias Andrée <m@maandree.se>2026-09-24 23:19:13 +0200
commit9b98f1a9222e5e96949bf74538a97900f2d847b1 (patch)
tree468ab35cb2ac75ef1c48d76189520ee9bad23495
parentAdd conflcit resultions strategies and interactive mode, and a lot of tests (diff)
downloadgit-rediff-9b98f1a9222e5e96949bf74538a97900f2d847b1.tar.gz
git-rediff-9b98f1a9222e5e96949bf74538a97900f2d847b1.tar.bz2
git-rediff-9b98f1a9222e5e96949bf74538a97900f2d847b1.tar.xz
Add --symmetricHEADmaster
Signed-off-by: Mattias Andrée <m@maandree.se>
-rw-r--r--Makefile33
-rw-r--r--README48
-rw-r--r--git-rediff.122
-rw-r--r--git-rediff.c23
-rwxr-xr-xt/all-positive-options-symmetric23
-rwxr-xr-xt/combination-2-symmetric22
-rwxr-xr-xt/combination-remove-base-symmetric-reduce34
-rwxr-xr-xt/combination-symmetric-2-no-symmetric28
-rwxr-xr-xt/combination-symmetric-reduce36
-rw-r--r--t/common.sh3
-rwxr-xr-xt/invalid-option-combinations49
-rwxr-xr-xt/no-option-merge12
-rwxr-xr-xt/no-option-merge-yes-no6
-rwxr-xr-xt/no-option-reduce-no-yes13
-rwxr-xr-xt/no-option-reduce-yes-no3
-rwxr-xr-xt/no-option-symmetric28
-rwxr-xr-xt/no-option-symmetric-no-yes18
-rwxr-xr-xt/no-option-symmetric-yes-no17
-rwxr-xr-xt/reduce-short-option26
-rwxr-xr-xt/symmetric-111
-rwxr-xr-xt/symmetric-2-different15
-rwxr-xr-xt/symmetric-2-identical16
-rwxr-xr-xt/symmetric-3-all-different17
-rwxr-xr-xt/symmetric-3-all-identical18
-rwxr-xr-xt/symmetric-3-different-base18
-rwxr-xr-xt/symmetric-3-different-head17
-rwxr-xr-xt/symmetric-3-different-tail17
-rwxr-xr-xt/symmetric-3-empty-head16
-rwxr-xr-xt/symmetric-3-empty-head-tail15
-rwxr-xr-xt/symmetric-3-empty-tail16
-rwxr-xr-xt/symmetric-4-different-head19
-rwxr-xr-xt/symmetric-4-different-lower-middle19
-rwxr-xr-xt/symmetric-4-different-tail19
-rwxr-xr-xt/symmetric-4-different-upper-middle19
-rwxr-xr-xt/symmetric-4-identical20
-rwxr-xr-xt/symmetric-4-same-different-middles25
-rwxr-xr-xt/symmetric-4-unique-different-middles23
-rwxr-xr-xt/symmetric-5-different-lower-middle21
-rwxr-xr-xt/symmetric-5-different-upper-middle21
-rwxr-xr-xt/symmetric-multiple-conflicts42
-rwxr-xr-xt/symmetric-multiple-files29
-rwxr-xr-xt/symmetric-no-conflicts11
-rwxr-xr-xt/symmetric-short-option18
43 files changed, 876 insertions, 30 deletions
diff --git a/Makefile b/Makefile
index e3fc3df..50591ad 100644
--- a/Makefile
+++ b/Makefile
@@ -86,27 +86,36 @@ TEST =\
t/combination-select-head-reduce\
t/combination-select-tail-reduce\
t/combination-merge-reduce\
+ t/combination-symmetric-reduce\
t/combination-remove-base-select-head-reduce\
t/combination-remove-base-select-tail-reduce\
t/combination-remove-base-merge-reduce\
+ t/combination-remove-base-symmetric-reduce\
t/combination-remove-base-reduce\
t/all-positive-options-merge\
+ t/all-positive-options-symmetric\
t/all-positive-options-select-head\
t/all-positive-options-select-tail\
t/combination-2-select-head\
t/combination-2-select-tail\
t/combination-2-merge\
+ t/combination-2-symmetric\
t/combination-2-remove-base\
t/combination-2-interactive\
t/combination-select-head-2-no-select-head\
t/combination-select-tail-2-no-select-tail\
t/combination-merge-2-no-merge\
+ t/combination-symmetric-2-no-symmetric\
t/combination-remove-base-2-no-remove-base\
t/combination-interactive-2-no-interactive\
t/merge-short-option\
+ t/symmetric-short-option\
+ t/reduce-short-option\
t/interactive-short-option\
t/no-option-merge-yes-no\
t/no-option-merge-no-yes\
+ t/no-option-symmetric-yes-no\
+ t/no-option-symmetric-no-yes\
t/no-option-remove-base-yes-no\
t/no-option-remove-base-no-yes\
t/no-option-select-head-yes-no\
@@ -120,6 +129,7 @@ TEST =\
t/no-option-select-head-yes-then-select-tail-yes-no\
t/no-option-select-tail-yes-then-select-head-yes-no\
t/no-option-merge\
+ t/no-option-symmetric\
t/no-option-remove-base\
t/no-option-select-head\
t/no-option-select-tail\
@@ -172,6 +182,29 @@ TEST =\
t/merge-empty-head\
t/merge-empty-base\
t/merge-empty-tail\
+ t/symmetric-1\
+ t/symmetric-2-different\
+ t/symmetric-2-identical\
+ t/symmetric-3-different-head\
+ t/symmetric-3-different-tail\
+ t/symmetric-3-different-base\
+ t/symmetric-3-all-different\
+ t/symmetric-3-all-identical\
+ t/symmetric-4-different-upper-middle\
+ t/symmetric-4-different-lower-middle\
+ t/symmetric-4-unique-different-middles\
+ t/symmetric-4-same-different-middles\
+ t/symmetric-4-identical\
+ t/symmetric-4-different-head\
+ t/symmetric-4-different-tail\
+ t/symmetric-5-different-upper-middle\
+ t/symmetric-5-different-lower-middle\
+ t/symmetric-no-conflicts\
+ t/symmetric-multiple-conflicts\
+ t/symmetric-multiple-files\
+ t/symmetric-3-empty-head\
+ t/symmetric-3-empty-tail\
+ t/symmetric-3-empty-head-tail\
t/marker-size-10\
t/marker-size-3\
t/marker-size-shorter-ignored\
diff --git a/README b/README
index 0816f27..9940917 100644
--- a/README
+++ b/README
@@ -2,7 +2,8 @@ NAME
git-rediff - Reduce partially resolved merge conflicts
SYNOPSIS
- git rediff [--no-reduce] [--merge|--select-head|--select-tail] [--remove-base] [-i] [<path>...]
+ git rediff [--merge|--symmetric|--select-head|--select-tail]
+ [--remove-base] [--no-reduce] [-i] [<path>...]
DESCRIPTION
git rediff rewrites files containing merge conflicts to
@@ -11,8 +12,8 @@ DESCRIPTION
OPTIONS
-i
--interactive
- Interactively select the action for each merge conflict.
- See INTERACTIVE COMMANDS.
+ Interactively select the action for each merge
+ conflict. See INTERACTIVE COMMANDS.
This overrides all other options.
@@ -21,24 +22,39 @@ OPTIONS
-m
--merge
- For conflicts with at least three branches, select the sole branch
- whose content differs from all other identical branches, but only
- if that is the top or bottom branch.
+ For conflicts with at least three branches, select
+ the sole branch whose content differs from all other
+ identical branches, but only if that is the top or
+ bottom branch.
Applied before --remove-base.
--no-merge
Disable --merge.
+ -y
+ --symmetric
+ When the top and bottom branches are identical, select
+ their content if all other branches are identical to
+ each other.
+
+ Applied before --reduce.
+
+ --no-symmetric
+ Disable --symmetric.
+
+ -r
--reduce
Enable reduction of remaining conflicts. (Default)
--no-reduce
- Do not reduce remaining conflicts after --merge, --remove-base,
- --select-head, and --select-tail have been applied.
+ Do not reduce remaining conflicts after --merge,
+ --remove-base, --select-head, and --select-tail have
+ been applied.
--remove-base
- Remove the middle branch from three-way merge conflicts.
+ Remove the middle branch from three-way merge
+ conflicts.
Applied before --reduce.
@@ -46,7 +62,8 @@ OPTIONS
Disable --remove-base.
--select-head
- Resolve each merge conflict by selecting its top branch.
+ Resolve each merge conflict by selecting its top
+ branch.
Applied before --reduce.
@@ -54,7 +71,8 @@ OPTIONS
Disable --select-head.
--select-tail
- Resolve each merge conflict by selecting its bottom branch.
+ Resolve each merge conflict by selecting its bottom
+ branch.
Applied before --reduce.
@@ -71,15 +89,19 @@ INTERACTIVE COMMANDS
m Apply merge selection.
+ y Select symmetric branches.
+
r Reduce the conflict.
e Manually edit the conflict.
s Skip the conflict.
- d Skip the conflict and all later conflicts in the current file.
+ d Skip the conflict and all later conflicts in the
+ current file.
- q Quit interaction and skip the conflict and all remaining conflicts.
+ q Quit interaction and skip the conflict and all
+ remaining conflicts.
? Display help.
diff --git a/git-rediff.1 b/git-rediff.1
index 84598d5..e06a9fb 100644
--- a/git-rediff.1
+++ b/git-rediff.1
@@ -3,9 +3,9 @@
git-rediff - Reduce partially resolved merge conflicts
.SH SYNOPSIS
.B git rediff
-.RB [ --no-reduce ]
-.RB [ --merge | --select-head | --select-tail ]
+.RB [ --merge | --symmetric | --select-head | --select-tail ]
.RB [ --remove-base ]
+.RB [ --no-reduce ]
.RB [ -i ]
.RI [ <path> "...]"
.SH DESCRIPTION
@@ -40,6 +40,21 @@ Applied before
Disable
.BR --merge .
.TP
+.B -y
+.TQ
+.B --symmetric
+When the top and bottom branches are identical, select their
+content if all other branches are identical to each other.
+
+Applied before
+.BR --reduce .
+.TP
+.B --no-symmetric
+Disable
+.BR --symmetric .
+.TP
+.B -r
+.TQ
.B --reduce
Enable reduction of remaining conflicts. (Default)
.TP
@@ -95,6 +110,9 @@ Remove the middle branch (three-way conflicts only).
.B m
Apply merge selection.
.TP
+.B y
+Select symmetric branches.
+.TP
.B r
Reduce the conflict.
.TP
diff --git a/git-rediff.c b/git-rediff.c
index e856504..0dc68c4 100644
--- a/git-rediff.c
+++ b/git-rediff.c
@@ -8,7 +8,7 @@
#include <signal.h>
#include <termios.h>
-NUSAGE(2, "[--no-reduce] [--merge|--select-head|--select-tail] [--remove-base] [-i] [<path>...]");
+NUSAGE(2, "[--merge|--symmetric|--select-head|--select-tail] [--remove-base] [--no-reduce] [-i] [<path>...]");
#if defined(__clang__)
# pragma clang diagnostic ignored "-Wunsafe-buffer-usage"
@@ -41,6 +41,7 @@ static int ttyfd_out = -1;
#endif
static struct stat tty_st;
static int merge = 0;
+static int symmetric = 0;
static int remove_base = 0;
static int select_head = 0;
static int select_tail = 0;
@@ -552,6 +553,7 @@ display_help(size_t nsubhunks)
if (nsubhunks == 3U)
dprintf(ttyfd_out, "b - remove base\n");
dprintf(ttyfd_out, "m - merge\n");
+ dprintf(ttyfd_out, "y - select symmetric branches\n");
dprintf(ttyfd_out, "r - reduce\n");
dprintf(ttyfd_out, "e - manually edit this hunk\n");
dprintf(ttyfd_out, "s - skip this hunk\n");
@@ -627,7 +629,7 @@ get_command(size_t nsubhunks, size_t number, size_t total)
int matched = 0;
again:
- dprintf(ttyfd_out, "\033[1;34m(%zu/%zu) Resolve this conflict [h,t,b,m,r,e,s,d,q,?]? \033[0m", number, total);
+ dprintf(ttyfd_out, "\033[1;34m(%zu/%zu) Resolve this conflict [h,t,b,m,y,r,e,s,d,q,?]? \033[0m", number, total);
for (;;) {
n = read(ttyfd_in, &c, 1);
@@ -653,6 +655,7 @@ again:
matched |= select_head = c == 'h';
matched |= select_tail = c == 't';
matched |= merge = c == 'm';
+ matched |= symmetric = c == 'y';
matched |= reduce = c == 'r';
matched |= remove_base = c == 'b';
matched |= /* skip = */ c == 's';
@@ -806,6 +809,14 @@ rediff_hunk(struct text *resp, const struct hunk *hunk, const struct line *tail)
else
goto genuine_conflict;
return MERGED;
+ } else if (symmetric) {
+ if (!texts_equal(&hunk->subs[0].text, &hunk->subs[hunk->nsubs - 1U].text))
+ goto genuine_conflict;
+ for (i = 2U; i < hunk->nsubs - 1U; i++)
+ if (!texts_equal(&hunk->subs[i].text, &hunk->subs[1U].text))
+ goto genuine_conflict;
+ append_text(resp, &hunk->subs[0].text);
+ return MERGED;
}
genuine_conflict:
@@ -1134,8 +1145,12 @@ main(int argc, char *argv[])
case 'm': merge = 1;
else if (TESTLONG("--no-merge", 0))
merge = 0;
+ else if (TESTLONG("--symmetric", 0))
+ case 'y': symmetric = 1;
+ else if (TESTLONG("--no-symmetric", 0))
+ symmetric = 0;
else if (TESTLONG("--reduce", 0))
- reduce = 1;
+ case 'r': reduce = 1;
else if (TESTLONG("--no-reduce", 0))
reduce = 0;
else if (TESTLONG("--remove-base", 0))
@@ -1157,7 +1172,7 @@ main(int argc, char *argv[])
usage();
} ARGEND;
- if (merge + select_head + select_tail > 1)
+ if (merge + symmetric + select_head + select_tail > 1)
usage();
originally_interactive = interactive;
diff --git a/t/all-positive-options-symmetric b/t/all-positive-options-symmetric
new file mode 100755
index 0000000..eb53cb9
--- /dev/null
+++ b/t/all-positive-options-symmetric
@@ -0,0 +1,23 @@
+#!/bin/sh -e
+. t/common.sh
+interactive
+
+printf '%s\n' > file \
+ "${HEAD} ours" \
+ same \
+ ours \
+ "${PARENT} base" \
+ same \
+ base \
+ "${TAIL}" \
+ same \
+ theirs \
+ "${END} theirs"
+
+printf '%s\n' > file.expected \
+ same \
+ ours
+
+printf '%s' h > input
+normal --symmetric --remove-base --reduce --interactive file 8<input 9>output
+test -s output
diff --git a/t/combination-2-symmetric b/t/combination-2-symmetric
new file mode 100755
index 0000000..059897d
--- /dev/null
+++ b/t/combination-2-symmetric
@@ -0,0 +1,22 @@
+#!/bin/sh -e
+. t/common.sh
+
+printf '%s\n' > file \
+ "${HEAD} ours" \
+ same \
+ new \
+ "${PARENT} base" \
+ same \
+ old \
+ "${TAIL}" \
+ same \
+ new \
+ "${END} theirs"
+
+printf '%s\n' > file.expected \
+ same \
+ new
+
+normal --symmetric --symmetric --no-reduce file
+
+also_interactive y
diff --git a/t/combination-remove-base-symmetric-reduce b/t/combination-remove-base-symmetric-reduce
new file mode 100755
index 0000000..c4b1cf9
--- /dev/null
+++ b/t/combination-remove-base-symmetric-reduce
@@ -0,0 +1,34 @@
+#!/bin/sh -e
+. t/common.sh
+
+printf '%s\n' > file \
+ "${HEAD} ours" \
+ first \
+ "${PARENT} base" \
+ not-first \
+ "${TAIL}" \
+ first \
+ "${END} theirs" \
+ "${HEAD} ours" \
+ ours \
+ common \
+ "${PARENT} base" \
+ base \
+ common \
+ "${TAIL}" \
+ theirs \
+ common \
+ "${END} theirs"
+
+printf '%s\n' > file.expected \
+ first \
+ "${HEAD} ours" \
+ ours \
+ "${TAIL}" \
+ theirs \
+ "${END} theirs" \
+ common
+
+conflict --remove-base --symmetric --reduce file
+
+also_interactive yy
diff --git a/t/combination-symmetric-2-no-symmetric b/t/combination-symmetric-2-no-symmetric
new file mode 100755
index 0000000..6aaf0e0
--- /dev/null
+++ b/t/combination-symmetric-2-no-symmetric
@@ -0,0 +1,28 @@
+#!/bin/sh -e
+. t/common.sh
+
+printf '%s\n' > file \
+ "${HEAD} ours" \
+ same \
+ new \
+ "${PARENT} base" \
+ same \
+ old \
+ "${TAIL}" \
+ same \
+ new \
+ "${END} theirs"
+
+printf '%s\n' > file.expected \
+ same \
+ "${HEAD} ours" \
+ new \
+ "${PARENT} base" \
+ old \
+ "${TAIL}" \
+ new \
+ "${END} theirs"
+
+conflict --symmetric --no-symmetric --no-symmetric file
+
+also_interactive r
diff --git a/t/combination-symmetric-reduce b/t/combination-symmetric-reduce
new file mode 100755
index 0000000..e5e370a
--- /dev/null
+++ b/t/combination-symmetric-reduce
@@ -0,0 +1,36 @@
+#!/bin/sh -e
+. t/common.sh
+
+printf '%s\n' > file \
+ "${HEAD} ours" \
+ first \
+ "${PARENT} base" \
+ not-first \
+ "${TAIL}" \
+ first \
+ "${END} theirs" \
+ "${HEAD} ours" \
+ ours \
+ common \
+ "${PARENT} base" \
+ base \
+ common \
+ "${TAIL}" \
+ theirs \
+ common \
+ "${END} theirs"
+
+printf '%s\n' > file.expected \
+ first \
+ "${HEAD} ours" \
+ ours \
+ "${PARENT} base" \
+ base \
+ "${TAIL}" \
+ theirs \
+ "${END} theirs" \
+ common
+
+conflict --symmetric --reduce file
+
+also_interactive yr
diff --git a/t/common.sh b/t/common.sh
index 30e4b6e..38d2845 100644
--- a/t/common.sh
+++ b/t/common.sh
@@ -141,7 +141,7 @@ interactive_filename () {
interactive_prompt () {
test $# = 2
- printf '\e[1;34m(%s/%s) Resolve this conflict [h,t,b,m,r,e,s,d,q,?]? \e[0m\n' "$1" "$2"
+ printf '\e[1;34m(%s/%s) Resolve this conflict [h,t,b,m,y,r,e,s,d,q,?]? \e[0m\n' "$1" "$2"
}
interactive_help () {
@@ -155,6 +155,7 @@ interactive_help () {
fi
printf '%s\n' \
'm - merge' \
+ 'y - select symmetric branches' \
'r - reduce' \
'e - manually edit this hunk' \
's - skip this hunk' \
diff --git a/t/invalid-option-combinations b/t/invalid-option-combinations
index 527ce63..1a75005 100755
--- a/t/invalid-option-combinations
+++ b/t/invalid-option-combinations
@@ -16,5 +16,54 @@ error --select-head --merge --select-tail file
error --select-head --select-tail --merge file
error --select-tail --merge --select-head file
error --select-tail --select-head --merge file
+error --symmetric --merge file
+error --merge --symmetric file
+error --symmetric --select-head file
+error --select-head --symmetric file
+error --symmetric --select-tail file
+error --select-tail --symmetric file
+error --symmetric --merge --select-head file
+error --symmetric --select-head --merge file
+error --symmetric --merge --select-tail file
+error --symmetric --select-tail --merge file
+error --symmetric --select-head --select-tail file
+error --symmetric --select-tail --select-head file
+error --merge --symmetric --select-head file
+error --select-head --symmetric --merge file
+error --merge --symmetric --select-tail file
+error --select-tail --symmetric --merge file
+error --select-head --symmetric --select-tail file
+error --select-tail --symmetric --select-head file
+error --merge --select-head --symmetric file
+error --select-head --merge --symmetric file
+error --merge --select-tail --symmetric file
+error --select-tail --merge --symmetric file
+error --select-head --select-tail --symmetric file
+error --select-tail --select-head --symmetric file
+error --symmetric --merge --select-head --select-tail file
+error --symmetric --merge --select-tail --select-head file
+error --symmetric --select-head --merge --select-tail file
+error --symmetric --select-head --select-tail --merge file
+error --symmetric --select-tail --merge --select-head file
+error --symmetric --select-tail --select-head --merge file
+error --merge --symmetric --select-head --select-tail file
+error --merge --symmetric --select-tail --select-head file
+error --select-head --symmetric --merge --select-tail file
+error --select-head --symmetric --select-tail --merge file
+error --select-tail --symmetric --merge --select-head file
+error --select-tail --symmetric --select-head --merge file
+error --merge --select-head --symmetric --select-tail file
+error --merge --select-tail --symmetric --select-head file
+error --select-head --merge --symmetric --select-tail file
+error --select-head --select-tail --symmetric --merge file
+error --select-tail --merge --symmetric --select-head file
+error --select-tail --select-head --symmetric --merge file
+error --merge --select-head --select-tail --symmetric file
+error --merge --select-tail --select-head --symmetric file
+error --select-head --merge --select-tail --symmetric file
+error --select-head --select-tail --merge --symmetric file
+error --select-tail --merge --select-head --symmetric file
+error --select-tail --select-head --merge --symmetric file
+
error --unknown-option file
error '-$' file
diff --git a/t/no-option-merge b/t/no-option-merge
index 5f55d74..8f9a63e 100755
--- a/t/no-option-merge
+++ b/t/no-option-merge
@@ -4,23 +4,23 @@
printf '%s\n' > file \
"${HEAD} ours" \
same \
- ours \
+ new \
"${PARENT} base" \
same \
- base \
+ old \
"${TAIL}" \
same \
- theirs \
+ old \
"${END} theirs"
printf '%s\n' > file.expected \
same \
"${HEAD} ours" \
- ours \
+ new \
"${PARENT} base" \
- base \
+ old \
"${TAIL}" \
- theirs \
+ old \
"${END} theirs"
conflict --no-merge file
diff --git a/t/no-option-merge-yes-no b/t/no-option-merge-yes-no
index f351e7c..af35ed8 100755
--- a/t/no-option-merge-yes-no
+++ b/t/no-option-merge-yes-no
@@ -3,11 +3,11 @@
printf '%s\n' > file \
"${HEAD} ours" \
- ours \
+ old \
"${PARENT} base" \
- base \
+ old \
"${TAIL}" \
- theirs \
+ new \
"${END} theirs"
cp -- file file.expected
diff --git a/t/no-option-reduce-no-yes b/t/no-option-reduce-no-yes
index e6a7d8c..9078a9b 100755
--- a/t/no-option-reduce-no-yes
+++ b/t/no-option-reduce-no-yes
@@ -3,14 +3,25 @@
printf '%s\n' > file \
"${HEAD} ours" \
+ same \
ours \
"${PARENT} base" \
+ same \
base \
"${TAIL}" \
+ same \
theirs \
"${END} theirs"
-cp -- file file.expected
+printf '%s\n' > file.expected \
+ same \
+ "${HEAD} ours" \
+ ours \
+ "${PARENT} base" \
+ base \
+ "${TAIL}" \
+ theirs \
+ "${END} theirs"
conflict --no-reduce --reduce file
diff --git a/t/no-option-reduce-yes-no b/t/no-option-reduce-yes-no
index 82fa7bd..8a55b15 100755
--- a/t/no-option-reduce-yes-no
+++ b/t/no-option-reduce-yes-no
@@ -3,10 +3,13 @@
printf '%s\n' > file \
"${HEAD} ours" \
+ same \
ours \
"${PARENT} base" \
+ same \
base \
"${TAIL}" \
+ same \
theirs \
"${END} theirs"
diff --git a/t/no-option-symmetric b/t/no-option-symmetric
new file mode 100755
index 0000000..82d6b73
--- /dev/null
+++ b/t/no-option-symmetric
@@ -0,0 +1,28 @@
+#!/bin/sh -e
+. t/common.sh
+
+printf '%s\n' > file \
+ "${HEAD} ours" \
+ same \
+ new \
+ "${PARENT} base" \
+ same \
+ old \
+ "${TAIL}" \
+ same \
+ new \
+ "${END} theirs"
+
+printf '%s\n' > file.expected \
+ same \
+ "${HEAD} ours" \
+ new \
+ "${PARENT} base" \
+ old \
+ "${TAIL}" \
+ new \
+ "${END} theirs"
+
+conflict --no-symmetric file
+
+also_interactive r
diff --git a/t/no-option-symmetric-no-yes b/t/no-option-symmetric-no-yes
new file mode 100755
index 0000000..45954cb
--- /dev/null
+++ b/t/no-option-symmetric-no-yes
@@ -0,0 +1,18 @@
+#!/bin/sh -e
+. t/common.sh
+
+printf '%s\n' > file \
+ "${HEAD} ours" \
+ new \
+ "${PARENT} base" \
+ old \
+ "${TAIL}" \
+ new \
+ "${END} theirs"
+
+printf '%s\n' > file.expected \
+ new
+
+normal --no-symmetric --symmetric --no-reduce file
+
+also_interactive y
diff --git a/t/no-option-symmetric-yes-no b/t/no-option-symmetric-yes-no
new file mode 100755
index 0000000..415467c
--- /dev/null
+++ b/t/no-option-symmetric-yes-no
@@ -0,0 +1,17 @@
+#!/bin/sh -e
+. t/common.sh
+
+printf '%s\n' > file \
+ "${HEAD} ours" \
+ same \
+ "${PARENT} base" \
+ base \
+ "${TAIL}" \
+ same \
+ "${END} theirs"
+
+cp -- file file.expected
+
+conflict --symmetric --no-symmetric --no-reduce file
+
+also_interactive r
diff --git a/t/reduce-short-option b/t/reduce-short-option
new file mode 100755
index 0000000..0e2b5b7
--- /dev/null
+++ b/t/reduce-short-option
@@ -0,0 +1,26 @@
+#!/bin/sh -e
+. t/common.sh
+
+printf '%s\n' > file \
+ "${HEAD} 1" \
+ common-first \
+ head-second \
+ common-third \
+ "${TAIL} 2" \
+ common-first \
+ tail-second \
+ common-third \
+ "${END} 2"
+
+printf '%s\n' > file.expected \
+ common-first \
+ "${HEAD} 1" \
+ head-second \
+ "${TAIL} 2" \
+ tail-second \
+ "${END} 2" \
+ common-third
+
+conflict -r file
+
+also_interactive r
diff --git a/t/symmetric-1 b/t/symmetric-1
new file mode 100755
index 0000000..f0d1a4b
--- /dev/null
+++ b/t/symmetric-1
@@ -0,0 +1,11 @@
+#!/bin/sh -e
+. t/common.sh
+
+printf '%s\n' > file \
+ "${HEAD} only" \
+ only \
+ "${END} only"
+
+cp -- file file.expected
+
+error --symmetric --no-reduce file
diff --git a/t/symmetric-2-different b/t/symmetric-2-different
new file mode 100755
index 0000000..889e4ed
--- /dev/null
+++ b/t/symmetric-2-different
@@ -0,0 +1,15 @@
+#!/bin/sh -e
+. t/common.sh
+
+printf '%s\n' > file \
+ "${HEAD} ours" \
+ ours \
+ "${TAIL}" \
+ theirs \
+ "${END} theirs"
+
+cp -- file file.expected
+
+conflict --symmetric --no-reduce file
+
+also_interactive y
diff --git a/t/symmetric-2-identical b/t/symmetric-2-identical
new file mode 100755
index 0000000..90f8369
--- /dev/null
+++ b/t/symmetric-2-identical
@@ -0,0 +1,16 @@
+#!/bin/sh -e
+. t/common.sh
+
+printf '%s\n' > file \
+ "${HEAD} ours" \
+ same \
+ "${TAIL}" \
+ same \
+ "${END} theirs"
+
+printf '%s\n' > file.expected \
+ same
+
+normal --symmetric --no-reduce file
+
+also_interactive y
diff --git a/t/symmetric-3-all-different b/t/symmetric-3-all-different
new file mode 100755
index 0000000..0693a80
--- /dev/null
+++ b/t/symmetric-3-all-different
@@ -0,0 +1,17 @@
+#!/bin/sh -e
+. t/common.sh
+
+printf '%s\n' > file \
+ "${HEAD} ours" \
+ first \
+ "${PARENT} 2" \
+ second \
+ "${TAIL}" \
+ third \
+ "${END} theirs"
+
+cp -- file file.expected
+
+conflict --symmetric --no-reduce file
+
+also_interactive y
diff --git a/t/symmetric-3-all-identical b/t/symmetric-3-all-identical
new file mode 100755
index 0000000..986d367
--- /dev/null
+++ b/t/symmetric-3-all-identical
@@ -0,0 +1,18 @@
+#!/bin/sh -e
+. t/common.sh
+
+printf '%s\n' > file \
+ "${HEAD} ours" \
+ same \
+ "${PARENT} 2" \
+ same \
+ "${TAIL}" \
+ same \
+ "${END} theirs"
+
+printf '%s\n' > file.expected \
+ same
+
+normal --symmetric --no-reduce file
+
+also_interactive y
diff --git a/t/symmetric-3-different-base b/t/symmetric-3-different-base
new file mode 100755
index 0000000..176c212
--- /dev/null
+++ b/t/symmetric-3-different-base
@@ -0,0 +1,18 @@
+#!/bin/sh -e
+. t/common.sh
+
+printf '%s\n' > file \
+ "${HEAD} ours" \
+ same \
+ "${PARENT} 2" \
+ different \
+ "${TAIL}" \
+ same \
+ "${END} theirs"
+
+printf '%s\n' > file.expected \
+ same
+
+normal --symmetric --no-reduce file
+
+also_interactive y
diff --git a/t/symmetric-3-different-head b/t/symmetric-3-different-head
new file mode 100755
index 0000000..435d828
--- /dev/null
+++ b/t/symmetric-3-different-head
@@ -0,0 +1,17 @@
+#!/bin/sh -e
+. t/common.sh
+
+printf '%s\n' > file \
+ "${HEAD} ours" \
+ different \
+ "${PARENT} 2" \
+ same \
+ "${TAIL}" \
+ same \
+ "${END} theirs"
+
+cp -- file file.expected
+
+conflict --symmetric --no-reduce file
+
+also_interactive y
diff --git a/t/symmetric-3-different-tail b/t/symmetric-3-different-tail
new file mode 100755
index 0000000..733c7d3
--- /dev/null
+++ b/t/symmetric-3-different-tail
@@ -0,0 +1,17 @@
+#!/bin/sh -e
+. t/common.sh
+
+printf '%s\n' > file \
+ "${HEAD} ours" \
+ same \
+ "${PARENT} 2" \
+ same \
+ "${TAIL}" \
+ different \
+ "${END} theirs"
+
+cp -- file file.expected
+
+conflict --symmetric --no-reduce file
+
+also_interactive y
diff --git a/t/symmetric-3-empty-head b/t/symmetric-3-empty-head
new file mode 100755
index 0000000..af61802
--- /dev/null
+++ b/t/symmetric-3-empty-head
@@ -0,0 +1,16 @@
+#!/bin/sh -e
+. t/common.sh
+
+printf '%s\n' > file \
+ "${HEAD} ours" \
+ "${PARENT} 2" \
+ 2 \
+ "${TAIL}" \
+ 2 \
+ "${END} theirs"
+
+cp -- file file.expected
+
+conflict --symmetric --no-reduce file
+
+also_interactive y
diff --git a/t/symmetric-3-empty-head-tail b/t/symmetric-3-empty-head-tail
new file mode 100755
index 0000000..f93cd84
--- /dev/null
+++ b/t/symmetric-3-empty-head-tail
@@ -0,0 +1,15 @@
+#!/bin/sh -e
+. t/common.sh
+
+printf '%s\n' > file \
+ "${HEAD} ours" \
+ "${PARENT} 2" \
+ base \
+ "${TAIL}" \
+ "${END} theirs"
+
+: > file.expected
+
+normal --symmetric --no-reduce file
+
+also_interactive y
diff --git a/t/symmetric-3-empty-tail b/t/symmetric-3-empty-tail
new file mode 100755
index 0000000..bbd4e7a
--- /dev/null
+++ b/t/symmetric-3-empty-tail
@@ -0,0 +1,16 @@
+#!/bin/sh -e
+. t/common.sh
+
+printf '%s\n' > file \
+ "${HEAD} ours" \
+ 1 \
+ "${PARENT} 2" \
+ 1 \
+ "${TAIL}" \
+ "${END} theirs"
+
+cp -- file file.expected
+
+conflict --symmetric --no-reduce file
+
+also_interactive y
diff --git a/t/symmetric-4-different-head b/t/symmetric-4-different-head
new file mode 100755
index 0000000..0d4d704
--- /dev/null
+++ b/t/symmetric-4-different-head
@@ -0,0 +1,19 @@
+#!/bin/sh -e
+. t/common.sh
+
+printf '%s\n' > file \
+ "${HEAD} ours" \
+ different \
+ "${PARENT} 2" \
+ same \
+ "${PARENT} 3" \
+ same \
+ "${TAIL}" \
+ same \
+ "${END} theirs"
+
+cp -- file file.expected
+
+conflict --symmetric --no-reduce file
+
+also_interactive y
diff --git a/t/symmetric-4-different-lower-middle b/t/symmetric-4-different-lower-middle
new file mode 100755
index 0000000..f8c334e
--- /dev/null
+++ b/t/symmetric-4-different-lower-middle
@@ -0,0 +1,19 @@
+#!/bin/sh -e
+. t/common.sh
+
+printf '%s\n' > file \
+ "${HEAD} ours" \
+ same \
+ "${PARENT} 2" \
+ same \
+ "${PARENT} 3" \
+ different \
+ "${TAIL}" \
+ same \
+ "${END} theirs"
+
+cp -- file file.expected
+
+conflict --symmetric --no-reduce file
+
+also_interactive y
diff --git a/t/symmetric-4-different-tail b/t/symmetric-4-different-tail
new file mode 100755
index 0000000..1bf80ff
--- /dev/null
+++ b/t/symmetric-4-different-tail
@@ -0,0 +1,19 @@
+#!/bin/sh -e
+. t/common.sh
+
+printf '%s\n' > file \
+ "${HEAD} ours" \
+ same \
+ "${PARENT} 2" \
+ same \
+ "${PARENT} 3" \
+ same \
+ "${TAIL}" \
+ different \
+ "${END} theirs"
+
+cp -- file file.expected
+
+conflict --symmetric --no-reduce file
+
+also_interactive y
diff --git a/t/symmetric-4-different-upper-middle b/t/symmetric-4-different-upper-middle
new file mode 100755
index 0000000..d78a5a2
--- /dev/null
+++ b/t/symmetric-4-different-upper-middle
@@ -0,0 +1,19 @@
+#!/bin/sh -e
+. t/common.sh
+
+printf '%s\n' > file \
+ "${HEAD} ours" \
+ same \
+ "${PARENT} 2" \
+ different \
+ "${PARENT} 3" \
+ same \
+ "${TAIL}" \
+ same \
+ "${END} theirs"
+
+cp -- file file.expected
+
+conflict --symmetric --no-reduce file
+
+also_interactive y
diff --git a/t/symmetric-4-identical b/t/symmetric-4-identical
new file mode 100755
index 0000000..40c82d7
--- /dev/null
+++ b/t/symmetric-4-identical
@@ -0,0 +1,20 @@
+#!/bin/sh -e
+. t/common.sh
+
+printf '%s\n' > file \
+ "${HEAD} ours" \
+ same \
+ "${PARENT} 2" \
+ same \
+ "${PARENT} 3" \
+ same \
+ "${TAIL}" \
+ same \
+ "${END} theirs"
+
+printf '%s\n' > file.expected \
+ same
+
+normal --symmetric --no-reduce file
+
+also_interactive y
diff --git a/t/symmetric-4-same-different-middles b/t/symmetric-4-same-different-middles
new file mode 100755
index 0000000..9d95e43
--- /dev/null
+++ b/t/symmetric-4-same-different-middles
@@ -0,0 +1,25 @@
+#!/bin/sh -e
+. t/common.sh
+
+printf '%s\n' > file \
+ "${HEAD} ours" \
+ new \
+ new \
+ "${PARENT} 2" \
+ old \
+ old \
+ "${PARENT} 3" \
+ old \
+ old \
+ "${TAIL}" \
+ new \
+ new \
+ "${END} theirs"
+
+printf '%s\n' > file.expected \
+ new \
+ new
+
+normal --symmetric --no-reduce file
+
+also_interactive y
diff --git a/t/symmetric-4-unique-different-middles b/t/symmetric-4-unique-different-middles
new file mode 100755
index 0000000..1b76187
--- /dev/null
+++ b/t/symmetric-4-unique-different-middles
@@ -0,0 +1,23 @@
+#!/bin/sh -e
+. t/common.sh
+
+printf '%s\n' > file \
+ "${HEAD} ours" \
+ same \
+ "${PARENT} 2" \
+ common \
+ first \
+ common \
+ "${PARENT} 3" \
+ common \
+ second \
+ common \
+ "${TAIL}" \
+ same \
+ "${END} theirs"
+
+cp -- file file.expected
+
+conflict --symmetric --no-reduce file
+
+also_interactive y
diff --git a/t/symmetric-5-different-lower-middle b/t/symmetric-5-different-lower-middle
new file mode 100755
index 0000000..530ea01
--- /dev/null
+++ b/t/symmetric-5-different-lower-middle
@@ -0,0 +1,21 @@
+#!/bin/sh -e
+. t/common.sh
+
+printf '%s\n' > file \
+ "${HEAD} ours" \
+ same \
+ "${PARENT} 2" \
+ same \
+ "${PARENT} 3" \
+ same \
+ "${PARENT} 4" \
+ different \
+ "${TAIL}" \
+ same \
+ "${END} theirs"
+
+cp -- file file.expected
+
+conflict --symmetric --no-reduce file
+
+also_interactive y
diff --git a/t/symmetric-5-different-upper-middle b/t/symmetric-5-different-upper-middle
new file mode 100755
index 0000000..b2fe2dd
--- /dev/null
+++ b/t/symmetric-5-different-upper-middle
@@ -0,0 +1,21 @@
+#!/bin/sh -e
+. t/common.sh
+
+printf '%s\n' > file \
+ "${HEAD} ours" \
+ same \
+ "${PARENT} 2" \
+ different \
+ "${PARENT} 3" \
+ add \
+ "${PARENT} 4" \
+ add \
+ "${TAIL}" \
+ same \
+ "${END} theirs"
+
+cp -- file file.expected
+
+conflict --symmetric --no-reduce file
+
+also_interactive y
diff --git a/t/symmetric-multiple-conflicts b/t/symmetric-multiple-conflicts
new file mode 100755
index 0000000..713a96c
--- /dev/null
+++ b/t/symmetric-multiple-conflicts
@@ -0,0 +1,42 @@
+#!/bin/sh -e
+. t/common.sh
+
+printf '%s\n' > file \
+ before \
+ "${HEAD} ours" \
+ same \
+ "${PARENT} base" \
+ different-interior \
+ "${TAIL}" \
+ same \
+ "${END} theirs" \
+ between \
+ "${HEAD} ours" \
+ different-head \
+ "${PARENT} base" \
+ interior \
+ "${PARENT} other" \
+ interior \
+ "${TAIL}" \
+ same \
+ "${END} theirs" \
+ after
+
+printf '%s\n' > file.expected \
+ before \
+ same \
+ between \
+ "${HEAD} ours" \
+ different-head \
+ "${PARENT} base" \
+ interior \
+ "${PARENT} other" \
+ interior \
+ "${TAIL}" \
+ same \
+ "${END} theirs" \
+ after
+
+conflict --symmetric --no-reduce file
+
+also_interactive yy
diff --git a/t/symmetric-multiple-files b/t/symmetric-multiple-files
new file mode 100755
index 0000000..86d2235
--- /dev/null
+++ b/t/symmetric-multiple-files
@@ -0,0 +1,29 @@
+#!/bin/sh -e
+. t/common.sh
+
+printf '%s\n' > first \
+ "${HEAD} ours" \
+ same \
+ "${PARENT} base" \
+ different \
+ "${TAIL}" \
+ same \
+ "${END} theirs"
+
+printf '%s\n' > first.expected \
+ same
+
+printf '%s\n' > second \
+ "${HEAD} ours" \
+ different \
+ "${PARENT} base" \
+ interior \
+ "${TAIL}" \
+ same \
+ "${END} theirs"
+
+cp -- second second.expected
+
+conflict --symmetric --no-reduce first second
+
+also_interactive yy
diff --git a/t/symmetric-no-conflicts b/t/symmetric-no-conflicts
new file mode 100755
index 0000000..d7c8c7f
--- /dev/null
+++ b/t/symmetric-no-conflicts
@@ -0,0 +1,11 @@
+#!/bin/sh -e
+. t/common.sh
+
+printf '%s\n' > file \
+ no conflicts in this file
+
+cp -- file file.expected
+
+normal --symmetric --no-reduce file
+
+also_interactive ''
diff --git a/t/symmetric-short-option b/t/symmetric-short-option
new file mode 100755
index 0000000..66c257a
--- /dev/null
+++ b/t/symmetric-short-option
@@ -0,0 +1,18 @@
+#!/bin/sh -e
+. t/common.sh
+
+printf '%s\n' > file \
+ "${HEAD} ours" \
+ same \
+ "${PARENT} base" \
+ different \
+ "${TAIL}" \
+ same \
+ "${END} theirs"
+
+printf '%s\n' > file.expected \
+ same
+
+normal -y --no-reduce file
+
+also_interactive y