aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorMattias Andrée <m@maandree.se>2026-09-22 19:53:29 +0200
committerMattias Andrée <m@maandree.se>2026-09-22 19:53:29 +0200
commitd8de1b23e8ec896759424a2f146969783cdd89e3 (patch)
treeed1ea3e97add5116230fd2df34e42b829a70ab39
parentfix doc text (diff)
downloadgit-rediff-1.1.tar.gz
git-rediff-1.1.tar.bz2
git-rediff-1.1.tar.xz
Add conflcit resultions strategies and interactive mode, and a lot of testsHEAD1.1master
Signed-off-by: Mattias Andrée <m@maandree.se>
-rw-r--r--.gitignore3
-rw-r--r--Makefile210
-rw-r--r--README77
-rw-r--r--git-rediff.1110
-rw-r--r--git-rediff.c654
-rwxr-xr-xt/all-positive-options-merge23
-rwxr-xr-xt/all-positive-options-select-head23
-rwxr-xr-xt/all-positive-options-select-tail23
-rwxr-xr-xt/combination-2-interactive19
-rwxr-xr-xt/combination-2-merge20
-rwxr-xr-xt/combination-2-remove-base27
-rwxr-xr-xt/combination-2-select-head22
-rwxr-xr-xt/combination-2-select-tail22
-rwxr-xr-xt/combination-interactive-2-no-interactive29
-rwxr-xr-xt/combination-merge-2-no-merge28
-rwxr-xr-xt/combination-merge-reduce36
-rwxr-xr-xt/combination-remove-base-2-no-remove-base28
-rwxr-xr-xt/combination-remove-base-merge-reduce34
-rwxr-xr-xt/combination-remove-base-reduce38
-rwxr-xr-xt/combination-remove-base-select-head-reduce18
-rwxr-xr-xt/combination-remove-base-select-tail-reduce18
-rwxr-xr-xt/combination-select-head-2-no-select-head28
-rwxr-xr-xt/combination-select-head-reduce30
-rwxr-xr-xt/combination-select-tail-2-no-select-tail28
-rwxr-xr-xt/combination-select-tail-reduce30
-rw-r--r--t/common.sh165
-rwxr-xr-xt/interactive-edit64
-rwxr-xr-xt/interactive-help28
-rwxr-xr-xt/interactive-multiple-commands113
-rwxr-xr-xt/interactive-short-option46
-rwxr-xr-xt/interactive-skip30
-rwxr-xr-xt/interactive-two-files44
-rwxr-xr-xt/invalid-option-combinations20
-rwxr-xr-xt/marker-size-1018
-rwxr-xr-xt/marker-size-318
-rwxr-xr-xt/marker-size-longer-ignored16
-rwxr-xr-xt/marker-size-shorter-ignored16
-rwxr-xr-xt/merge-111
-rwxr-xr-xt/merge-2-different15
-rwxr-xr-xt/merge-2-identical15
-rwxr-xr-xt/merge-base17
-rwxr-xr-xt/merge-empty-base16
-rwxr-xr-xt/merge-empty-head16
-rwxr-xr-xt/merge-empty-tail16
-rwxr-xr-xt/merge-head18
-rwxr-xr-xt/merge-head-420
-rwxr-xr-xt/merge-lower-middle-428
-rwxr-xr-xt/merge-multiple-conflicts34
-rwxr-xr-xt/merge-multiple-files30
-rwxr-xr-xt/merge-no-conflicts11
-rwxr-xr-xt/merge-short-option18
-rwxr-xr-xt/merge-tail18
-rwxr-xr-xt/merge-tail-420
-rwxr-xr-xt/merge-upper-middle-428
-rwxr-xr-xt/missing-newline-non-conflict10
-rwxr-xr-xt/mixed-marker-sizes30
-rwxr-xr-xt/no-option-interactive30
-rwxr-xr-xt/no-option-interactive-no-yes20
-rwxr-xr-xt/no-option-interactive-yes-no19
-rwxr-xr-xt/no-option-merge28
-rwxr-xr-xt/no-option-merge-no-yes18
-rwxr-xr-xt/no-option-merge-yes-no17
-rwxr-xr-xt/no-option-reduce20
-rwxr-xr-xt/no-option-reduce-no-yes17
-rwxr-xr-xt/no-option-reduce-yes-no17
-rwxr-xr-xt/no-option-remove-base28
-rwxr-xr-xt/no-option-remove-base-no-yes22
-rwxr-xr-xt/no-option-remove-base-yes-no24
-rwxr-xr-xt/no-option-select-head28
-rwxr-xr-xt/no-option-select-head-no-yes18
-rwxr-xr-xt/no-option-select-head-yes-no17
-rwxr-xr-xt/no-option-select-head-yes-then-select-tail-yes-no18
-rwxr-xr-xt/no-option-select-tail28
-rwxr-xr-xt/no-option-select-tail-no-yes18
-rwxr-xr-xt/no-option-select-tail-yes-no17
-rwxr-xr-xt/no-option-select-tail-yes-then-select-head-yes-no18
-rwxr-xr-xt/no-reduce17
-rwxr-xr-xt/no-reduce-111
-rwxr-xr-xt/no-reduce-2-files24
-rwxr-xr-xt/no-reduce-2-full19
-rwxr-xr-xt/no-reduce-2-head21
-rwxr-xr-xt/no-reduce-2-hunks21
-rwxr-xr-xt/no-reduce-2-middle25
-rwxr-xr-xt/no-reduce-2-multiple-partial33
-rwxr-xr-xt/no-reduce-2-non-aligned27
-rwxr-xr-xt/no-reduce-2-none17
-rwxr-xr-xt/no-reduce-2-tail21
-rwxr-xr-xt/no-reduce-3-full23
-rwxr-xr-xt/no-reduce-3-head26
-rwxr-xr-xt/no-reduce-3-middle32
-rwxr-xr-xt/no-reduce-3-multiple-partial44
-rwxr-xr-xt/no-reduce-3-none20
-rwxr-xr-xt/no-reduce-3-tail26
-rwxr-xr-xt/no-reduce-4-full27
-rwxr-xr-xt/no-reduce-4-head31
-rwxr-xr-xt/no-reduce-4-middle39
-rwxr-xr-xt/no-reduce-4-multiple-partial55
-rwxr-xr-xt/no-reduce-4-none23
-rwxr-xr-xt/no-reduce-4-tail31
-rwxr-xr-xt/no-reduce-5-full31
-rwxr-xr-xt/no-reduce-5-head36
-rwxr-xr-xt/no-reduce-5-middle46
-rwxr-xr-xt/no-reduce-5-multiple-partial66
-rwxr-xr-xt/no-reduce-5-none26
-rwxr-xr-xt/no-reduce-5-tail36
-rwxr-xr-xt/no-reduce-dashdash-dashprefix17
-rwxr-xr-xt/no-reduce-empty-base16
-rwxr-xr-xt/no-reduce-empty-head16
-rwxr-xr-xt/no-reduce-empty-tail16
-rwxr-xr-xt/no-reduce-no-conflicts11
-rwxr-xr-xt/null-bytes15
-rwxr-xr-xt/reduce-19
-rwxr-xr-xt/reduce-2-files26
-rwxr-xr-xt/reduce-2-full22
-rwxr-xr-xt/reduce-2-head30
-rwxr-xr-xt/reduce-2-hunks24
-rwxr-xr-xt/reduce-2-middle41
-rwxr-xr-xt/reduce-2-multiple-partial53
-rwxr-xr-xt/reduce-2-non-aligned43
-rwxr-xr-xt/reduce-2-none24
-rwxr-xr-xt/reduce-2-tail30
-rwxr-xr-xt/reduce-3-full26
-rwxr-xr-xt/reduce-3-head38
-rwxr-xr-xt/reduce-3-middle54
-rwxr-xr-xt/reduce-3-multiple-partial70
-rwxr-xr-xt/reduce-3-none30
-rwxr-xr-xt/reduce-3-tail38
-rwxr-xr-xt/reduce-4-full30
-rwxr-xr-xt/reduce-4-head46
-rwxr-xr-xt/reduce-4-middle67
-rwxr-xr-xt/reduce-4-multiple-partial87
-rwxr-xr-xt/reduce-4-none36
-rwxr-xr-xt/reduce-4-tail46
-rwxr-xr-xt/reduce-5-full34
-rwxr-xr-xt/reduce-5-head54
-rwxr-xr-xt/reduce-5-middle80
-rwxr-xr-xt/reduce-5-multiple-partial104
-rwxr-xr-xt/reduce-5-none42
-rwxr-xr-xt/reduce-5-tail54
-rwxr-xr-xt/reduce-empty-base16
-rwxr-xr-xt/reduce-empty-head16
-rwxr-xr-xt/reduce-empty-tail16
-rwxr-xr-xt/reduce-is-default23
-rwxr-xr-xt/reduce-no-conflicts11
-rwxr-xr-xt/remove-base-112
-rwxr-xr-xt/remove-base-215
-rwxr-xr-xt/remove-base-322
-rwxr-xr-xt/remove-base-3-empty-base21
-rwxr-xr-xt/remove-base-3-empty-head20
-rwxr-xr-xt/remove-base-3-empty-tail20
-rwxr-xr-xt/remove-base-419
-rwxr-xr-xt/remove-base-521
-rwxr-xr-xt/remove-base-multiple-files33
-rwxr-xr-xt/remove-base-multiple-hunks36
-rwxr-xr-xt/remove-base-no-conflicts11
-rwxr-xr-xt/select-head-112
-rwxr-xr-xt/select-head-216
-rwxr-xr-xt/select-head-318
-rwxr-xr-xt/select-head-3-empty-base17
-rwxr-xr-xt/select-head-3-empty-head16
-rwxr-xr-xt/select-head-3-empty-tail17
-rwxr-xr-xt/select-head-422
-rwxr-xr-xt/select-head-multiple-files30
-rwxr-xr-xt/select-head-multiple-hunks28
-rwxr-xr-xt/select-head-no-conflicts11
-rwxr-xr-xt/select-tail-112
-rwxr-xr-xt/select-tail-216
-rwxr-xr-xt/select-tail-318
-rwxr-xr-xt/select-tail-3-empty-base17
-rwxr-xr-xt/select-tail-3-empty-head17
-rwxr-xr-xt/select-tail-3-empty-tail16
-rwxr-xr-xt/select-tail-422
-rwxr-xr-xt/select-tail-multiple-files30
-rwxr-xr-xt/select-tail-multiple-hunks28
-rwxr-xr-xt/select-tail-no-conflicts11
-rwxr-xr-xt/stdin-dash-operand21
-rwxr-xr-xt/stdin-dashdash-dash-operand21
-rwxr-xr-xt/stdin-dashdash-no-operand21
-rwxr-xr-xt/stdin-no-operand21
-rwxr-xr-xt/unclosed-conflict-head12
-rwxr-xr-xt/unclosed-conflict-head-parent14
-rwxr-xr-xt/unclosed-conflict-head-parent-tail16
-rwxr-xr-xt/unclosed-conflict-head-tail14
-rwxr-xr-xt/unexpected-separator-end11
-rwxr-xr-xt/unexpected-separator-parent11
-rwxr-xr-xt/unexpected-separator-tail11
186 files changed, 5891 insertions, 100 deletions
diff --git a/.gitignore b/.gitignore
index 0ec616b..4809db6 100644
--- a/.gitignore
+++ b/.gitignore
@@ -12,4 +12,7 @@
*.gcov
*.gcno
*.gcda
+*.t
+*.to
/git-rediff
+/testdir.*
diff --git a/Makefile b/Makefile
index 803a6e7..e3fc3df 100644
--- a/Makefile
+++ b/Makefile
@@ -8,15 +8,216 @@ OBJ =\
HDR =
-all: git-rediff
+TOBJ = $(OBJ:.o=.to)
+
+TEST =\
+ t/reduce-1\
+ t/reduce-2-full\
+ t/reduce-2-none\
+ t/reduce-2-head\
+ t/reduce-2-tail\
+ t/reduce-2-middle\
+ t/reduce-2-multiple-partial\
+ t/reduce-2-non-aligned\
+ t/reduce-3-full\
+ t/reduce-3-none\
+ t/reduce-3-head\
+ t/reduce-3-tail\
+ t/reduce-3-middle\
+ t/reduce-3-multiple-partial\
+ t/reduce-4-full\
+ t/reduce-4-none\
+ t/reduce-4-head\
+ t/reduce-4-tail\
+ t/reduce-4-middle\
+ t/reduce-4-multiple-partial\
+ t/reduce-5-full\
+ t/reduce-5-none\
+ t/reduce-5-head\
+ t/reduce-5-tail\
+ t/reduce-5-middle\
+ t/reduce-5-multiple-partial\
+ t/reduce-2-hunks\
+ t/reduce-2-files\
+ t/reduce-is-default\
+ t/reduce-no-conflicts\
+ t/reduce-empty-head\
+ t/reduce-empty-base\
+ t/reduce-empty-tail\
+ t/stdin-no-operand\
+ t/stdin-dash-operand\
+ t/stdin-dashdash-no-operand\
+ t/stdin-dashdash-dash-operand\
+ t/no-reduce-1\
+ t/no-reduce-2-full\
+ t/no-reduce-2-none\
+ t/no-reduce-2-head\
+ t/no-reduce-2-tail\
+ t/no-reduce-2-middle\
+ t/no-reduce-2-multiple-partial\
+ t/no-reduce-2-non-aligned\
+ t/no-reduce-3-full\
+ t/no-reduce-3-none\
+ t/no-reduce-3-head\
+ t/no-reduce-3-tail\
+ t/no-reduce-3-middle\
+ t/no-reduce-3-multiple-partial\
+ t/no-reduce-4-full\
+ t/no-reduce-4-none\
+ t/no-reduce-4-head\
+ t/no-reduce-4-tail\
+ t/no-reduce-4-middle\
+ t/no-reduce-4-multiple-partial\
+ t/no-reduce-5-full\
+ t/no-reduce-5-none\
+ t/no-reduce-5-head\
+ t/no-reduce-5-tail\
+ t/no-reduce-5-middle\
+ t/no-reduce-5-multiple-partial\
+ t/no-reduce-2-hunks\
+ t/no-reduce-2-files\
+ t/no-reduce-no-conflicts\
+ t/no-reduce-empty-head\
+ t/no-reduce-empty-base\
+ t/no-reduce-empty-tail\
+ t/no-reduce\
+ t/no-reduce-dashdash-dashprefix\
+ t/invalid-option-combinations\
+ t/combination-select-head-reduce\
+ t/combination-select-tail-reduce\
+ t/combination-merge-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-reduce\
+ t/all-positive-options-merge\
+ 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-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-remove-base-2-no-remove-base\
+ t/combination-interactive-2-no-interactive\
+ t/merge-short-option\
+ t/interactive-short-option\
+ t/no-option-merge-yes-no\
+ t/no-option-merge-no-yes\
+ t/no-option-remove-base-yes-no\
+ t/no-option-remove-base-no-yes\
+ t/no-option-select-head-yes-no\
+ t/no-option-select-head-no-yes\
+ t/no-option-select-tail-yes-no\
+ t/no-option-select-tail-no-yes\
+ t/no-option-reduce-yes-no\
+ t/no-option-reduce-no-yes\
+ t/no-option-interactive-yes-no\
+ t/no-option-interactive-no-yes\
+ 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-remove-base\
+ t/no-option-select-head\
+ t/no-option-select-tail\
+ t/no-option-reduce\
+ t/no-option-interactive\
+ t/select-head-1\
+ t/select-head-2\
+ t/select-head-3\
+ t/select-head-3-empty-head\
+ t/select-head-3-empty-base\
+ t/select-head-3-empty-tail\
+ t/select-head-4\
+ t/select-head-multiple-hunks\
+ t/select-head-multiple-files\
+ t/select-head-no-conflicts\
+ t/select-tail-1\
+ t/select-tail-2\
+ t/select-tail-3\
+ t/select-tail-3-empty-head\
+ t/select-tail-3-empty-base\
+ t/select-tail-3-empty-tail\
+ t/select-tail-4\
+ t/select-tail-multiple-hunks\
+ t/select-tail-multiple-files\
+ t/select-tail-no-conflicts\
+ t/remove-base-1\
+ t/remove-base-2\
+ t/remove-base-3\
+ t/remove-base-3-empty-head\
+ t/remove-base-3-empty-base\
+ t/remove-base-3-empty-tail\
+ t/remove-base-4\
+ t/remove-base-5\
+ t/remove-base-multiple-hunks\
+ t/remove-base-multiple-files\
+ t/remove-base-no-conflicts\
+ t/merge-tail\
+ t/merge-head\
+ t/merge-base\
+ t/merge-tail-4\
+ t/merge-head-4\
+ t/merge-upper-middle-4\
+ t/merge-lower-middle-4\
+ t/merge-1\
+ t/merge-2-different\
+ t/merge-2-identical\
+ t/merge-multiple-conflicts\
+ t/merge-multiple-files\
+ t/merge-no-conflicts\
+ t/merge-empty-head\
+ t/merge-empty-base\
+ t/merge-empty-tail\
+ t/marker-size-10\
+ t/marker-size-3\
+ t/marker-size-shorter-ignored\
+ t/marker-size-longer-ignored\
+ t/mixed-marker-sizes\
+ t/missing-newline-non-conflict\
+ t/null-bytes\
+ t/unclosed-conflict-head\
+ t/unclosed-conflict-head-parent\
+ t/unclosed-conflict-head-tail\
+ t/unclosed-conflict-head-parent-tail\
+ t/unexpected-separator-tail\
+ t/unexpected-separator-parent\
+ t/unexpected-separator-end\
+ t/interactive-skip\
+ t/interactive-edit\
+ t/interactive-help\
+ t/interactive-multiple-commands\
+ t/interactive-two-files
+
+THDR =\
+ t/common.sh
+
+all: git-rediff git-rediff.t
$(OBJ): $(HDR)
+$(TOBJ): $(HDR)
.c.o:
$(CC) -c -o $@ $< $(CFLAGS) $(CPPFLAGS)
+.c.to:
+ $(CC) -c -o $@ $< $(CFLAGS) $(CPPFLAGS) -DTEST
+
git-rediff: $(OBJ)
$(CC) -o $@ $(OBJ) $(LDFLAGS)
+git-rediff.t: $(TOBJ)
+ $(CC) -o $@ $(TOBJ) $(LDFLAGS)
+
+check: git-rediff git-rediff.t $(TEST) $(THDR)
+ @set -e;\
+ for t in $(TEST); do\
+ printf '%s\n' ./$$t >&2;\
+ $(CHECK_PREFIX) ./$$t;\
+ done
+
install: git-rediff
mkdir -p -- "$(DESTDIR)$(PREFIX)/bin"
mkdir -p -- "$(DESTDIR)$(MANPREFIX)/man1/"
@@ -29,9 +230,10 @@ uninstall:
clean:
-rm -f -- *.o *.a *.lo *.su *.so *.so.* *.gch *.gcov *.gcno *.gcda
- -rm -f -- git-rediff
+ -rm -f -- git-rediff *.t *.to
+ -rm -rf -- testdir.*/
.SUFFIXES:
-.SUFFIXES: .o .c
+.SUFFIXES: .o .to .c
-.PHONY: all install uninstall clean
+.PHONY: all check install uninstall clean
diff --git a/README b/README
index c570b1d..0816f27 100644
--- a/README
+++ b/README
@@ -2,12 +2,87 @@ NAME
git-rediff - Reduce partially resolved merge conflicts
SYNOPSIS
- git rediff [<path>...]
+ git rediff [--no-reduce] [--merge|--select-head|--select-tail] [--remove-base] [-i] [<path>...]
DESCRIPTION
git rediff rewrites files containing merge conflicts to
remove conflicts that have been resolved.
+OPTIONS
+ -i
+ --interactive
+ Interactively select the action for each merge conflict.
+ See INTERACTIVE COMMANDS.
+
+ This overrides all other options.
+
+ --no-interactive
+ Disable --interactive.
+
+ -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.
+
+ Applied before --remove-base.
+
+ --no-merge
+ Disable --merge.
+
+ --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.
+
+ --remove-base
+ Remove the middle branch from three-way merge conflicts.
+
+ Applied before --reduce.
+
+ --no-remove-base
+ Disable --remove-base.
+
+ --select-head
+ Resolve each merge conflict by selecting its top branch.
+
+ Applied before --reduce.
+
+ --no-select-head
+ Disable --select-head.
+
+ --select-tail
+ Resolve each merge conflict by selecting its bottom branch.
+
+ Applied before --reduce.
+
+ --no-select-tail
+ Disable --select-tail.
+
+
+INTERACTIVE COMMANDS
+ h Select the top branch.
+
+ t Select the bottom branch.
+
+ b Remove the middle branch (three-way conflicts only).
+
+ m Apply merge selection.
+
+ 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.
+
+ q Quit interaction and skip the conflict and all remaining conflicts.
+
+ ? Display help.
+
OPERANDS
path
File to rewrite. If - is specified, the file
diff --git a/git-rediff.1 b/git-rediff.1
index db970c6..84598d5 100644
--- a/git-rediff.1
+++ b/git-rediff.1
@@ -2,12 +2,116 @@
.SH NAME
git-rediff - Reduce partially resolved merge conflicts
.SH SYNOPSIS
-.I git rediff
-[<path>...]
+.B git rediff
+.RB [ --no-reduce ]
+.RB [ --merge | --select-head | --select-tail ]
+.RB [ --remove-base ]
+.RB [ -i ]
+.RI [ <path> "...]"
.SH DESCRIPTION
-.I git rediff
+.B git rediff
rewrites files containing merge conflicts to
remove conflicts that have been resolved.
+.SH OPTIONS
+.TP
+.B -i
+.TQ
+.B --interactive
+Interactively select the action for each merge conflict. See
+.BR "INTERACTIVE COMMANDS" .
+
+This overrides all other options.
+.TP
+.B --no-interactive
+Disable
+.BR --interactive .
+.TP
+.B -m
+.TQ
+.B --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.
+
+Applied before
+.BR --remove-base .
+.TP
+.B --no-merge
+Disable
+.BR --merge .
+.TP
+.B --reduce
+Enable reduction of remaining conflicts. (Default)
+.TP
+.B --no-reduce
+Do not reduce remaining conflicts after
+.BR --merge ,
+.BR --remove-base ,
+.BR --select-head ,
+and
+.B --select-tail
+have been applied.
+.TP
+.B --remove-base
+Remove the middle branch from three-way merge conflicts.
+
+Applied before
+.BR --reduce .
+.TP
+.B --no-remove-base
+Disable
+.BR --remove-base .
+.TP
+.B --select-head
+Resolve each merge conflict by selecting its top branch.
+
+Applied before
+.BR --reduce .
+.TP
+.B --no-select-head
+Disable
+.BR --select-head .
+.TP
+.B --select-tail
+Resolve each merge conflict by selecting its bottom branch.
+
+Applied before
+.BR --reduce .
+.TP
+.B --no-select-tail
+Disable
+.BR --select-tail .
+.SH INTERACTIVE COMMANDS
+.TP
+.B h
+Select the top branch.
+.TP
+.B t
+Select the bottom branch.
+.TP
+.B b
+Remove the middle branch (three-way conflicts only).
+.TP
+.B m
+Apply merge selection.
+.TP
+.B r
+Reduce the conflict.
+.TP
+.B e
+Manually edit the conflict.
+.TP
+.B s
+Skip the conflict.
+.TP
+.B d
+Skip the conflict and all later conflicts in the current file.
+.TP
+.B q
+Quit interaction and skip the conflict and all remaining conflicts.
+.TP
+.B ?
+Display help.
.SH OPERANDS
.TP
.I path
diff --git a/git-rediff.c b/git-rediff.c
index 7e0727b..e856504 100644
--- a/git-rediff.c
+++ b/git-rediff.c
@@ -5,8 +5,10 @@
#endif
#include <libsimple.h>
#include <libsimple-arg.h>
+#include <signal.h>
+#include <termios.h>
-NUSAGE(2, "[<path>...]");
+NUSAGE(2, "[--no-reduce] [--merge|--select-head|--select-tail] [--remove-base] [-i] [<path>...]");
#if defined(__clang__)
# pragma clang diagnostic ignored "-Wunsafe-buffer-usage"
@@ -18,6 +20,34 @@ enum successfulness {
ERROR = 2
};
+enum interactive_command {
+ INTERACTIVE_ERROR,
+ INTERACTIVE_REDIFF,
+ INTERACTIVE_EDIT,
+ INTERACTIVE_SKIP_FILE
+};
+
+static int interactive = 0;
+static int originally_interactive;
+#ifndef TEST
+static int ttyfd = -1;
+# define ttyfd_in ttyfd
+# define ttyfd_out ttyfd
+static struct termios tty_settings;
+#else
+static int ttyfd_in = -1;
+static int ttyfd_out = -1;
+# define ttyfd ttyfd_out
+#endif
+static struct stat tty_st;
+static int merge = 0;
+static int remove_base = 0;
+static int select_head = 0;
+static int select_tail = 0;
+static int reduce = 1;
+static const char *editor;
+static char *editor_freeable = NULL;
+
struct line {
union {
size_t in_off;
@@ -46,6 +76,40 @@ struct hunk {
size_t nsubs;
};
+#ifndef TEST
+static void
+restore_tty(void)
+{
+ (void)tcsetattr(ttyfd, TCSANOW, &tty_settings);
+ close(ttyfd);
+}
+
+static void
+restore_tty_on_signal(int signo)
+{
+ (void)tcsetattr(ttyfd, TCSANOW, &tty_settings);
+ raise(signo);
+}
+
+static void
+configure_interactive_tty(void)
+{
+ struct termios settings = tty_settings;
+ settings.c_lflag &= (tcflag_t)~(ICANON | ISIG);
+ settings.c_cc[VMIN] = 1;
+ settings.c_cc[VTIME] = 0;
+ if (tcsetattr(ttyfd, TCSANOW, &settings))
+ eprintf("tcsetattr /dev/tty:");
+}
+#endif
+
+static int
+is_controlling_tty(int fd)
+{
+ struct stat st;
+ return !fstat(fd, &st) && S_ISCHR(st.st_mode) && st.st_dev == tty_st.st_dev && st.st_rdev == tty_st.st_rdev;
+}
+
static int
send_line(int fd, const char *fname, const struct line *line)
{
@@ -116,6 +180,31 @@ line_is_marker(struct line *line, char marker_symbol, size_t conflict_marker_siz
return i == line->len || line->text[i] == ' ' || line->text[i] == '\n' || line->text[i] == '\r';
}
+static int
+line_is_a_marker(struct line *line, size_t conflict_marker_size, char *marker_symbolp)
+{
+ switch (line->len ? line->text[0] : '\0') {
+ case '<':
+ case '|':
+ case '=':
+ case '>':
+ if (line_is_marker(line, line->text[0], conflict_marker_size)) {
+ *marker_symbolp = line->text[0];
+ return 1;
+ }
+ /* fall through */
+ default:
+ return 0;
+ }
+}
+
+static int
+line_is_either_marker(struct line *line, const char *marker_symbols, size_t conflict_marker_size)
+{
+ char marker_symbol;
+ return line_is_a_marker(line, conflict_marker_size, &marker_symbol) && strchr(marker_symbols, marker_symbol);
+}
+
static void
append_lines(struct text *textp, const struct line *lines, size_t n)
{
@@ -139,6 +228,24 @@ append_text(struct text *textp, const struct text *text)
append_lines(textp, text->lines, text->nlines);
}
+#if defined(__GNUC__)
+__attribute__((__pure__))
+#endif
+static int
+texts_equal(const struct text *a, const struct text *b)
+{
+ size_t i;
+ if (a->nlines != b->nlines)
+ return 0;
+ for (i = 0; i < a->nlines; i++)
+ if (a->lines[i].len != b->lines[i].len)
+ return 0;
+ for (i = 0; i < a->nlines; i++)
+ if (memcmp(a->lines[i].text, b->lines[i].text, a->lines[i].len))
+ return 0;
+ return 1;
+}
+
static char *
diff_subhunks(const struct subhunk *f1, const struct subhunk *f2)
{
@@ -154,9 +261,9 @@ diff_subhunks(const struct subhunk *f1, const struct subhunk *f2)
ssize_t r;
union {
struct {
- char context[sizeof("-U") + 3 * sizeof(context)];
- char pipe1[sizeof("/dev/fd/-") + 3 * sizeof(pipe1[0])];
- char pipe2[sizeof("/dev/fd/-") + 3 * sizeof(pipe2[0])];
+ char context[sizeof("-U") + 3U * sizeof(context)];
+ char pipe1[sizeof("/dev/fd/-") + 3U * sizeof(pipe1[0])];
+ char pipe2[sizeof("/dev/fd/-") + 3U * sizeof(pipe2[0])];
};
char text[8096];
} buf;
@@ -243,11 +350,11 @@ diff_subhunks(const struct subhunk *f1, const struct subhunk *f2)
if (ret_next) {
ret_next = 0;
if (ret_off == ret_size) {
- if (ret_size > SIZE_MAX - 512) {
+ if (ret_size > SIZE_MAX - 512U) {
errno = ENOMEM;
eprintf("realloc:");
}
- ret = erealloc(ret, ret_size += 512);
+ ret = erealloc(ret, ret_size += 512U);
}
ret[ret_off++] = buf.text[i];
} else if (buf.text[i] == '\n') {
@@ -366,16 +473,359 @@ diff_hunk(struct subhunk *ret, const struct hunk *hunk)
free(ret_buf.text.lines);
}
+static int
+read_lines(struct text *lines_out, char **text_out, int fd, const char *fname)
+{
+ struct line *lines;
+ size_t text_len = 0;
+ size_t text_size = 0;
+ ssize_t r;
+ void *new;
+ size_t i;
+
+ *lines_out = (struct text){0};
+ *text_out = NULL;
+
+ for (;;) {
+ if (text_len == text_size) {
+ if (text_size > SIZE_MAX - 8096U) {
+ errno = ENOMEM;
+ weprintf("realloc:");
+ goto fail;
+ }
+ text_size += 8096U;
+ new = realloc(*text_out, text_size);
+ if (!new)
+ goto fail;
+ *text_out = new;
+ }
+ r = read(fd, &(*text_out)[text_len], text_size - text_len);
+ if (r <= 0) {
+ if (!r)
+ break;
+ if (errno == EINTR)
+ continue;
+ weprintf("read %s:", fname);
+ goto fail;
+ }
+ text_len += (size_t)r;
+ }
+
+ lines_out->nlines = (text_len ? 1U : 0U);
+ for (i = 0; i + 1U < text_len; i++)
+ if ((*text_out)[i] == '\n')
+ lines_out->nlines += 1U;
+
+ if (!lines_out->nlines)
+ return 0;
+
+ lines = lines_out->lines = ecalloc(lines_out->nlines, sizeof(*lines_out->lines));
+
+ for (i = 0; i < lines_out->nlines; i++)
+ lines[i].in = NULL;
+
+ lines->text = *text_out;
+ lines++;
+ for (i = 0; i + 1U < text_len;) {
+ if ((*text_out)[i++] == '\n') {
+ lines->text = &(*text_out)[i];
+ lines[-1].len = (size_t)(lines->text - lines[-1].text);
+ lines++;
+ }
+ }
+ lines[-1].len = (size_t)(&(*text_out)[text_len] - lines[-1].text);
+
+ return 0;
+
+fail:
+ free(*text_out);
+ *text_out = NULL;
+ return -1;
+}
+
+static void
+display_help(size_t nsubhunks)
+{
+ dprintf(ttyfd_out, "\033[1;31m");
+ dprintf(ttyfd_out, "h - select head\n");
+ dprintf(ttyfd_out, "t - select tail\n");
+ if (nsubhunks == 3U)
+ dprintf(ttyfd_out, "b - remove base\n");
+ dprintf(ttyfd_out, "m - merge\n");
+ dprintf(ttyfd_out, "r - reduce\n");
+ dprintf(ttyfd_out, "e - manually edit this hunk\n");
+ dprintf(ttyfd_out, "s - skip this hunk\n");
+ dprintf(ttyfd_out, "d - skip this hunk and all later hunks in the file\n");
+ dprintf(ttyfd_out, "q - quit; skip this hunk and all remaining hunks\n");
+ dprintf(ttyfd_out, "? - print help\n");
+ dprintf(ttyfd_out, "\033[0m");
+}
+
+static void
+display_line(const struct line *line, const char *colour)
+{
+ ssize_t n;
+ size_t off;
+
+ if (colour)
+ dprintf(ttyfd_out, "\033[%sm", colour);
+
+ for (off = 0; off < line->len;) {
+ n = write(ttyfd_out, &line->text[off], line->len - off);
+ if (n < 0) {
+ if (errno == EINTR)
+ continue;
+ eprintf("write /dev/tty:");
+ }
+ off += (size_t)n;
+ }
+
+ if (colour)
+ dprintf(ttyfd_out, "\033[0m");
+}
+
+static void
+display_hunk(const struct text *text, size_t first, size_t last, const struct hunk *hunk, size_t marker_size)
+{
+ size_t i, start, end, subhunk = 0;
+ size_t before = 0U, after = 0U;
+ const char *colour;
+
+ for (; before < 3U && before < first; before++)
+ if (line_is_marker(&text->lines[first - (before + 1U)], '>', marker_size))
+ break;
+ start = first - before;
+
+ for (; after < 3U && after + 1U < text->nlines - last; after++)
+ if (line_is_marker(&text->lines[last + (after + 1U)], '<', marker_size))
+ break;
+ end = last + after;
+
+ for (i = start; i <= end; i++) {
+ /* for simplicity subhunks are indexed from 1 */
+ if (first > i || i > last) {
+ colour = NULL;
+ } else if (line_is_a_marker(&text->lines[i], marker_size, &(char){0})) {
+ colour = "36";
+ subhunk++;
+ } else if (subhunk == 1U || subhunk == hunk->nsubs) {
+ colour = "32";
+ } else if (hunk->nsubs == 3U && subhunk == 2U) {
+ colour = "31";
+ } else {
+ colour = "35";
+ }
+ display_line(&text->lines[i], colour);
+ }
+}
+
+static enum interactive_command
+get_command(size_t nsubhunks, size_t number, size_t total)
+{
+ char c;
+ ssize_t n;
+ 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);
+
+ for (;;) {
+ n = read(ttyfd_in, &c, 1);
+ if (!n)
+ c = 'q';
+ if (n >= 0)
+ break;
+ if (errno != EINTR)
+ return INTERACTIVE_ERROR;
+ }
+ if (c != '\n')
+ dprintf(ttyfd_out, "\n");
+#ifndef TEST
+ if (c == 'Z' - '@') {
+ if (tcsetattr(ttyfd, TCSANOW, &tty_settings))
+ weprintf("tcsetattr /dev/tty:");
+ raise(SIGTSTP);
+ configure_interactive_tty();
+ goto again;
+ }
+#endif
+
+ matched |= select_head = c == 'h';
+ matched |= select_tail = c == 't';
+ matched |= merge = c == 'm';
+ matched |= reduce = c == 'r';
+ matched |= remove_base = c == 'b';
+ matched |= /* skip = */ c == 's';
+
+ switch (c) {
+ case 'e':
+ return INTERACTIVE_EDIT;
+ case 'q':
+ interactive = 0;
+ /* fall through */
+ case 'd':
+ return INTERACTIVE_SKIP_FILE;
+ case 'b':
+ if (nsubhunks == 3U) {
+ default:
+ if (matched)
+ return INTERACTIVE_REDIFF;
+ }
+ /* fall through */
+ case '?':
+ display_help(nsubhunks);
+ goto again;
+ }
+}
+
+static char *
+get_config_editor(const char *command)
+{
+ ssize_t len;
+ FILE *fp;
+
+ free(editor_freeable);
+ editor_freeable = NULL;
+
+ fp = popen(command, "r");
+ if (!fp)
+ return NULL;
+ len = getline(&editor_freeable, &(size_t){0U}, fp);
+ if (len < 1 || editor_freeable[--len] != '\n') {
+ pclose(fp);
+ return NULL;
+ }
+ if (pclose(fp))
+ return NULL;
+ editor_freeable[len] = '\0';
+
+ return editor_freeable;
+}
+
+static void
+select_editor(void)
+{
+ if ((editor = getenv("GIT_EDITOR")) && *editor)
+ return;
+ if ((editor = get_config_editor("git config --get rediff.editor")) && *editor)
+ return;
+ if ((editor = get_config_editor("git config --get core.editor")) && *editor)
+ return;
+ if ((editor = getenv("VISUAL")) && *editor)
+ return;
+ if ((editor = getenv("EDITOR")) && *editor)
+ return;
+ editor = "vi";
+}
+
+static enum successfulness
+manual_edit(struct text *resp, const struct hunk *hunk, const struct line *tail, size_t conflict_marker_size)
+{
+ char filename[] = ".git-rediff-edit.XXXXXX";
+ struct text text;
+ char *contents, *command;
+ int fd;
+ size_t i;
+
+ fd = mkstemp(filename);
+ if (fd < 0) {
+ weprintf("mkstemp %s:", filename);
+ return ERROR;
+ }
+ for (i = 0; i < hunk->nsubs; i++) {
+ if (send_line(fd, filename, hunk->subs[i].head) ||
+ send_text(fd, filename, &hunk->subs[i].text))
+ goto fail_close;
+ }
+ if (send_line(fd, filename, tail)) {
+ weprintf("write %s:", filename);
+ goto fail_close;
+ }
+ if (close(fd)) {
+ weprintf("write %s:", filename);
+ goto fail_unlink;
+ }
+
+ command = emalloc(strlen(editor) + 1U + sizeof(filename));
+ stpcpy(stpcpy(stpcpy(command, editor), " "), filename);
+ if (system(command)) {
+ weprintf("editor failed");
+ free(command);
+ goto fail_unlink;
+ }
+ free(command);
+
+ fd = open(filename, O_RDONLY);
+ if (fd < 0)
+ goto fail_unlink;
+ if (read_lines(&text, &contents, fd, filename))
+ goto fail_close;
+ close(fd);
+ unlink(filename);
+
+ append_text(resp, &text);
+ for (i = 0; i < text.nlines; i++)
+ if (line_is_a_marker(&text.lines[i], conflict_marker_size, &(char){0}))
+ break;
+ free(text.lines);
+
+ return i < text.nlines ? CONFLICT : MERGED;
+
+fail_close:
+ close(fd);
+fail_unlink:
+ unlink(filename);
+ return ERROR;
+}
+
static enum successfulness
rediff_hunk(struct text *resp, const struct hunk *hunk, const struct line *tail)
{
- struct subhunk diff;
+ struct subhunk diff, baseless_subhunks[2];
size_t i, j, full_bytes;
unsigned char last_byte;
- struct hunk uncommon;
+ struct hunk uncommon, baseless_hunk;
int in_uncommon = 0;
enum successfulness ret = MERGED;
+ if (select_head || select_tail) {
+ append_text(resp, &hunk->subs[select_tail ? hunk->nsubs - 1U : 0U].text);
+ return MERGED;
+ }
+
+ if (merge && hunk->nsubs >= 3U) {
+ 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;
+ if (texts_equal(&hunk->subs[1U].text, &hunk->subs[hunk->nsubs - 1U].text))
+ append_text(resp, &hunk->subs[0].text);
+ else if (texts_equal(&hunk->subs[1U].text, &hunk->subs[0].text))
+ append_text(resp, &hunk->subs[hunk->nsubs - 1U].text);
+ else
+ goto genuine_conflict;
+ return MERGED;
+ }
+genuine_conflict:
+
+ if (remove_base && hunk->nsubs == 3U) {
+ baseless_subhunks[0] = hunk->subs[0];
+ baseless_subhunks[1] = hunk->subs[2];
+ baseless_hunk.subs = baseless_subhunks;
+ baseless_hunk.nsubs = 2U;
+ hunk = &baseless_hunk;
+ }
+
+ if (!reduce) {
+ for (i = 0; i < hunk->nsubs; i++) {
+ append_line(resp, hunk->subs[i].head);
+ append_text(resp, &hunk->subs[i].text);
+ }
+ append_line(resp, tail);
+ return CONFLICT;
+ }
+
full_bytes = hunk->nsubs / CHAR_BIT;
last_byte = 0;
if (hunk->nsubs % CHAR_BIT) {
@@ -437,24 +887,31 @@ rediff_hunk(struct text *resp, const struct hunk *hunk, const struct line *tail)
static enum successfulness
rediff_file(struct text *text_out, const struct text *text_in, size_t conflict_marker_size, const char *fname)
{
- size_t i, t;
+ size_t i, t, nhunks = 0, hunkno = 0;
struct hunk hunk = {0};
ssize_t subhunk = -1;
enum successfulness ret = MERGED, r;
+ enum interactive_command command;
*text_out = (struct text){0};
+ if (interactive) {
+ command = INTERACTIVE_REDIFF;
+ for (i = 0; i < text_in->nlines; i++)
+ if (line_is_marker(&text_in->lines[i], '>', conflict_marker_size))
+ nhunks++;
+ } else {
+ command = INTERACTIVE_SKIP_FILE;
+ }
for (i = 0; i < text_in->nlines; i++) {
if (line_is_marker(&text_in->lines[i], '<', conflict_marker_size)) {
if (subhunk >= 0)
goto syntax_error;
goto new_subhunk;
- } else if (line_is_marker(&text_in->lines[i], '|', conflict_marker_size) ||
- line_is_marker(&text_in->lines[i], '=', conflict_marker_size)) {
+ } else if (line_is_either_marker(&text_in->lines[i], "|=", conflict_marker_size)) {
if (subhunk < 0)
goto syntax_error;
- if (!line_is_marker(hunk.subs[subhunk].head, '<', conflict_marker_size) &&
- !line_is_marker(hunk.subs[subhunk].head, '|', conflict_marker_size))
+ if (!line_is_either_marker(hunk.subs[subhunk].head, "<|", conflict_marker_size))
goto syntax_error;
new_subhunk:
subhunk++;
@@ -469,7 +926,17 @@ rediff_file(struct text *text_out, const struct text *text_in, size_t conflict_m
goto syntax_error;
t = hunk.nsubs;
hunk.nsubs = (size_t)subhunk + 1U;
- r = rediff_hunk(text_out, &hunk, &text_in->lines[i]);
+ if (command != INTERACTIVE_SKIP_FILE) {
+ hunkno++;
+ display_hunk(text_in, (size_t)(hunk.subs[0].head - text_in->lines), i, &hunk, conflict_marker_size);
+ command = get_command(hunk.nsubs, hunkno, nhunks);
+ if (command == INTERACTIVE_ERROR)
+ goto error;
+ }
+ if (command == INTERACTIVE_EDIT)
+ r = manual_edit(text_out, &hunk, &text_in->lines[i], conflict_marker_size);
+ else
+ r = rediff_hunk(text_out, &hunk, &text_in->lines[i]);
ret = MAX(ret, r);
hunk.nsubs = t;
subhunk = -1;
@@ -498,76 +965,6 @@ syntax_error:
goto error;
}
-static int
-read_lines(struct text *lines_out, char **text_out, int fd, const char *fname)
-{
- struct line *lines;
- size_t text_len = 0;
- size_t text_size = 0;
- ssize_t r;
- void *new;
- size_t i;
-
- *lines_out = (struct text){0};
- *text_out = NULL;
-
- for (;;) {
- if (text_len == text_size) {
- if (text_size > SIZE_MAX - 8096U) {
- errno = ENOMEM;
- weprintf("realloc:");
- goto fail;
- }
- text_size += 8096U;
- new = realloc(*text_out, text_size);
- if (!new)
- goto fail;
- *text_out = new;
- }
- r = read(fd, &(*text_out)[text_len], text_size - text_len);
- if (r <= 0) {
- if (!r)
- break;
- if (errno == EINTR)
- continue;
- weprintf("read %s:", fname);
- goto fail;
- }
- text_len += (size_t)r;
- }
-
- lines_out->nlines = (text_len ? 1U : 0U);
- for (i = 0; i + 1U < text_len; i++)
- if ((*text_out)[i] == '\n')
- lines_out->nlines += 1U;
-
- if (!lines_out->nlines)
- return 0;
-
- lines = lines_out->lines = ecalloc(lines_out->nlines, sizeof(*lines_out->lines));
-
- for (i = 0; i < lines_out->nlines; i++)
- lines[i].in = NULL;
-
- lines->text = *text_out;
- lines++;
- for (i = 0; i + 1U < text_len;) {
- if ((*text_out)[i++] == '\n') {
- lines->text = &(*text_out)[i];
- lines[-1].len = (size_t)(lines->text - lines[-1].text);
- lines++;
- }
- }
- lines[-1].len = (size_t)(&(*text_out)[text_len] - lines[-1].text);
-
- return 0;
-
-fail:
- free(*text_out);
- *text_out = NULL;
- return -1;
-}
-
static size_t
get_conflict_marker_size(const char *path)
{
@@ -646,7 +1043,7 @@ get_conflict_marker_size(const char *path)
}
static enum successfulness
-rediff(const char *fname)
+rediff(const char *fname, int print_filename)
{
struct text text_in, text_out;
char *text;
@@ -654,6 +1051,9 @@ rediff(const char *fname)
enum successfulness ret;
size_t conflict_marker_size;
+ if (interactive && print_filename)
+ dprintf(ttyfd_out, "\033[1m%s\033[0m\n", fname);
+
if (!strcmp(fname, "-")) {
fname = "<stdout>";
close_fd = 0;
@@ -663,12 +1063,16 @@ rediff(const char *fname)
conflict_marker_size = get_conflict_marker_size(".");
} else {
close_fd = 1;
- fd = open(fname, O_RDWR);
+ fd = open(fname, O_RDWR | O_NOCTTY);
if (fd < 0) {
- weprintf("open %s O_RDWR:", fname);
+ weprintf("open %s O_RDWR | O_NOCTTY:", fname);
return ERROR;
}
- if (read_lines(&text_in, &text, fd, "<stdin>")) {
+ if (originally_interactive && is_controlling_tty(fd)) {
+ weprintf("%s: file is the controlling terminal\n", fname);
+ return ERROR;
+ }
+ if (read_lines(&text_in, &text, fd, fname)) {
close(fd);
return ERROR;
}
@@ -721,17 +1125,82 @@ main(int argc, char *argv[])
libsimple_default_failure_exit = 2;
ARGBEGIN {
+ case '-':
+ if (TESTLONG("--interactive", 0))
+ case 'i': interactive = 1;
+ else if (TESTLONG("--no-interactive", 0))
+ interactive = 0;
+ else if (TESTLONG("--merge", 0))
+ case 'm': merge = 1;
+ else if (TESTLONG("--no-merge", 0))
+ merge = 0;
+ else if (TESTLONG("--reduce", 0))
+ reduce = 1;
+ else if (TESTLONG("--no-reduce", 0))
+ reduce = 0;
+ else if (TESTLONG("--remove-base", 0))
+ remove_base = 1;
+ else if (TESTLONG("--no-remove-base", 0))
+ remove_base = 0;
+ else if (TESTLONG("--select-head", 0))
+ select_head = 1;
+ else if (TESTLONG("--no-select-head", 0))
+ select_head = 0;
+ else if (TESTLONG("--select-tail", 0))
+ select_tail = 1;
+ else if (TESTLONG("--no-select-tail", 0))
+ select_tail = 0;
+ else
+ usage();
+ break;
default:
usage();
} ARGEND;
+ if (merge + select_head + select_tail > 1)
+ usage();
+
+ originally_interactive = interactive;
+
+ if (interactive) {
+#ifdef TEST
+ ttyfd_in = 8;
+ ttyfd_out = 9;
+#else
+ struct sigaction sa = {
+ .sa_handler = &restore_tty_on_signal,
+ .sa_flags = (int)SA_RESETHAND
+ };
+ ttyfd = open("/dev/tty", O_RDWR | O_NOCTTY);
+ if (ttyfd < 0)
+ eprintf("open /dev/tty O_RDWR | O_NOCTTY:");
+ if (tcgetattr(ttyfd, &tty_settings))
+ eprintf("tcgetattr /dev/tty:");
+ configure_interactive_tty();
+ if (atexit(restore_tty))
+ eprintf("atexit:");
+ sigemptyset(&sa.sa_mask);
+ if (sigaction(SIGHUP, &sa, NULL))
+ eprintf("sigaction SIGHUP:");
+ if (sigaction(SIGINT, &sa, NULL))
+ eprintf("sigaction SIGINT:");
+ if (sigaction(SIGQUIT, &sa, NULL))
+ eprintf("sigaction SIGQUIT:");
+ if (sigaction(SIGTERM, &sa, NULL))
+ eprintf("sigaction SIGTERM:");
+#endif
+ if (fstat(ttyfd, &tty_st))
+ eprintf("fstat /dev/tty:");
+ select_editor();
+ }
+
if (fstat(STDERR_FILENO, &(struct stat){0})) {
int fd;
if (errno != EBADF)
eprintf("fstat <stderr>:");
- fd = open("/dev/null", O_WRONLY);
+ fd = open("/dev/null", O_WRONLY | O_NOCTTY);
if (fd < 0)
- eprintf("open /dev/null O_WRONLY:");
+ eprintf("open /dev/null O_WRONLY | O_NOCTTY:");
if (fd != STDERR_FILENO) {
if (dup2(fd, STDERR_FILENO) != STDERR_FILENO)
eprintf("dup2 /dev/null <stderr>:");
@@ -741,12 +1210,13 @@ main(int argc, char *argv[])
if (argc) {
for (; *argv; argv++) {
- r = rediff(*argv);
+ r = rediff(*argv, argc > 1);
ret = MAX(ret, r);
}
} else {
- ret = rediff("-");
+ ret = rediff("-", 0);
}
+ free(editor_freeable);
return (int)ret;
}
diff --git a/t/all-positive-options-merge b/t/all-positive-options-merge
new file mode 100755
index 0000000..eeb21d0
--- /dev/null
+++ b/t/all-positive-options-merge
@@ -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 --merge --remove-base --reduce --interactive file 8<input 9>output
+test -s output
diff --git a/t/all-positive-options-select-head b/t/all-positive-options-select-head
new file mode 100755
index 0000000..999dc31
--- /dev/null
+++ b/t/all-positive-options-select-head
@@ -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 --select-head --remove-base --reduce --interactive file 8<input 9>output
+test -s output
diff --git a/t/all-positive-options-select-tail b/t/all-positive-options-select-tail
new file mode 100755
index 0000000..dbf72a7
--- /dev/null
+++ b/t/all-positive-options-select-tail
@@ -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 --select-tail --remove-base --reduce --interactive file 8<input 9>output
+test -s output
diff --git a/t/combination-2-interactive b/t/combination-2-interactive
new file mode 100755
index 0000000..d2cb14e
--- /dev/null
+++ b/t/combination-2-interactive
@@ -0,0 +1,19 @@
+#!/bin/sh -e
+. t/common.sh
+interactive
+
+printf '%s\n' > file \
+ "${HEAD} ours" \
+ ours \
+ "${PARENT} base" \
+ base \
+ "${TAIL}" \
+ theirs \
+ "${END} theirs"
+
+printf '%s\n' > file.expected \
+ ours
+
+printf '%s' h > input
+normal --interactive --interactive file 8<input 9>output
+test -s output
diff --git a/t/combination-2-merge b/t/combination-2-merge
new file mode 100755
index 0000000..5379244
--- /dev/null
+++ b/t/combination-2-merge
@@ -0,0 +1,20 @@
+#!/bin/sh -e
+. t/common.sh
+
+printf '%s\n' > file \
+ "${HEAD} ours" \
+ same \
+ ours \
+ "${PARENT} base" \
+ same \
+ base \
+ "${TAIL}" \
+ same \
+ ours \
+ "${END} theirs"
+
+cp -- file file.expected
+
+conflict --merge --merge --no-reduce file
+
+also_interactive m
diff --git a/t/combination-2-remove-base b/t/combination-2-remove-base
new file mode 100755
index 0000000..a7db4cd
--- /dev/null
+++ b/t/combination-2-remove-base
@@ -0,0 +1,27 @@
+#!/bin/sh -e
+. t/common.sh
+
+printf '%s\n' > file \
+ "${HEAD} ours" \
+ same \
+ ours \
+ "${PARENT} base" \
+ same \
+ base \
+ "${TAIL}" \
+ same \
+ theirs \
+ "${END} theirs"
+
+printf '%s\n' > file.expected \
+ "${HEAD} ours" \
+ same \
+ ours \
+ "${TAIL}" \
+ same \
+ theirs \
+ "${END} theirs"
+
+conflict --remove-base --remove-base --no-reduce file
+
+also_interactive bb
diff --git a/t/combination-2-select-head b/t/combination-2-select-head
new file mode 100755
index 0000000..e326f15
--- /dev/null
+++ b/t/combination-2-select-head
@@ -0,0 +1,22 @@
+#!/bin/sh -e
+. t/common.sh
+
+printf '%s\n' > file \
+ "${HEAD} ours" \
+ same \
+ ours \
+ "${PARENT} base" \
+ same \
+ base \
+ "${TAIL}" \
+ same \
+ theirs \
+ "${END} theirs"
+
+printf '%s\n' > file.expected \
+ same \
+ ours
+
+normal --select-head --select-head --no-reduce file
+
+also_interactive h
diff --git a/t/combination-2-select-tail b/t/combination-2-select-tail
new file mode 100755
index 0000000..594e315
--- /dev/null
+++ b/t/combination-2-select-tail
@@ -0,0 +1,22 @@
+#!/bin/sh -e
+. t/common.sh
+
+printf '%s\n' > file \
+ "${HEAD} ours" \
+ same \
+ ours \
+ "${PARENT} base" \
+ same \
+ base \
+ "${TAIL}" \
+ same \
+ theirs \
+ "${END} theirs"
+
+printf '%s\n' > file.expected \
+ same \
+ theirs
+
+normal --select-tail --select-tail --no-reduce file
+
+also_interactive t
diff --git a/t/combination-interactive-2-no-interactive b/t/combination-interactive-2-no-interactive
new file mode 100755
index 0000000..1126b8c
--- /dev/null
+++ b/t/combination-interactive-2-no-interactive
@@ -0,0 +1,29 @@
+#!/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 \
+ "${HEAD} ours" \
+ ours \
+ "${PARENT} base" \
+ base \
+ "${TAIL}" \
+ theirs \
+ "${END} theirs"
+
+printf '%s' q > input
+conflict --interactive --no-interactive --no-interactive file 8<input 9>output
+test ! -s output
diff --git a/t/combination-merge-2-no-merge b/t/combination-merge-2-no-merge
new file mode 100755
index 0000000..e249817
--- /dev/null
+++ b/t/combination-merge-2-no-merge
@@ -0,0 +1,28 @@
+#!/bin/sh -e
+. t/common.sh
+
+printf '%s\n' > file \
+ "${HEAD} ours" \
+ same \
+ ours \
+ "${PARENT} base" \
+ same \
+ base \
+ "${TAIL}" \
+ same \
+ ours \
+ "${END} theirs"
+
+printf '%s\n' > file.expected \
+ same \
+ "${HEAD} ours" \
+ ours \
+ "${PARENT} base" \
+ base \
+ "${TAIL}" \
+ ours \
+ "${END} theirs"
+
+conflict --merge --no-merge --no-merge file
+
+also_interactive rr
diff --git a/t/combination-merge-reduce b/t/combination-merge-reduce
new file mode 100755
index 0000000..aec3a30
--- /dev/null
+++ b/t/combination-merge-reduce
@@ -0,0 +1,36 @@
+#!/bin/sh -e
+. t/common.sh
+
+printf '%s\n' > file \
+ "${HEAD} ours" \
+ first \
+ "${PARENT} base" \
+ not first \
+ "${TAIL}" \
+ not 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 --merge --reduce file
+
+also_interactive mm
diff --git a/t/combination-remove-base-2-no-remove-base b/t/combination-remove-base-2-no-remove-base
new file mode 100755
index 0000000..835e751
--- /dev/null
+++ b/t/combination-remove-base-2-no-remove-base
@@ -0,0 +1,28 @@
+#!/bin/sh -e
+. t/common.sh
+
+printf '%s\n' > file \
+ "${HEAD} ours" \
+ same \
+ ours \
+ "${PARENT} base" \
+ same \
+ base \
+ "${TAIL}" \
+ same \
+ theirs \
+ "${END} theirs"
+
+printf '%s\n' > file.expected \
+ same \
+ "${HEAD} ours" \
+ ours \
+ "${PARENT} base" \
+ base \
+ "${TAIL}" \
+ theirs \
+ "${END} theirs"
+
+conflict --remove-base --no-remove-base --no-remove-base file
+
+also_interactive rr
diff --git a/t/combination-remove-base-merge-reduce b/t/combination-remove-base-merge-reduce
new file mode 100755
index 0000000..48025c4
--- /dev/null
+++ b/t/combination-remove-base-merge-reduce
@@ -0,0 +1,34 @@
+#!/bin/sh -e
+. t/common.sh
+
+printf '%s\n' > file \
+ "${HEAD} ours" \
+ not 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 --merge --reduce file
+
+also_interactive mm
diff --git a/t/combination-remove-base-reduce b/t/combination-remove-base-reduce
new file mode 100755
index 0000000..952681e
--- /dev/null
+++ b/t/combination-remove-base-reduce
@@ -0,0 +1,38 @@
+#!/bin/sh -e
+. t/common.sh
+
+printf '%s\n' > file \
+ "${HEAD} ours" \
+ ours \
+ "${PARENT} base" \
+ base \
+ "${TAIL}" \
+ theirs \
+ "${END} theirs" \
+ "${HEAD} ours" \
+ ours \
+ common \
+ "${PARENT} base" \
+ base \
+ common \
+ "${TAIL}" \
+ theirs \
+ common \
+ "${END} theirs"
+
+printf '%s\n' > file.expected \
+ "${HEAD} ours" \
+ ours \
+ "${TAIL}" \
+ theirs \
+ "${END} theirs" \
+ "${HEAD} ours" \
+ ours \
+ "${TAIL}" \
+ theirs \
+ "${END} theirs" \
+ common
+
+conflict --remove-base --reduce file
+
+also_interactive bbbb
diff --git a/t/combination-remove-base-select-head-reduce b/t/combination-remove-base-select-head-reduce
new file mode 100755
index 0000000..96e828d
--- /dev/null
+++ b/t/combination-remove-base-select-head-reduce
@@ -0,0 +1,18 @@
+#!/bin/sh -e
+. t/common.sh
+
+printf '%s\n' > file \
+ "${HEAD} ours" \
+ ours \
+ "${PARENT} base" \
+ base \
+ "${TAIL}" \
+ theirs \
+ "${END} theirs"
+
+printf '%s\n' > file.expected \
+ ours
+
+normal --remove-base --select-head --reduce file
+
+also_interactive h
diff --git a/t/combination-remove-base-select-tail-reduce b/t/combination-remove-base-select-tail-reduce
new file mode 100755
index 0000000..8e0f465
--- /dev/null
+++ b/t/combination-remove-base-select-tail-reduce
@@ -0,0 +1,18 @@
+#!/bin/sh -e
+. t/common.sh
+
+printf '%s\n' > file \
+ "${HEAD} ours" \
+ ours \
+ "${PARENT} base" \
+ base \
+ "${TAIL}" \
+ theirs \
+ "${END} theirs"
+
+printf '%s\n' > file.expected \
+ theirs
+
+normal --remove-base --select-tail --reduce file
+
+also_interactive t
diff --git a/t/combination-select-head-2-no-select-head b/t/combination-select-head-2-no-select-head
new file mode 100755
index 0000000..06a1464
--- /dev/null
+++ b/t/combination-select-head-2-no-select-head
@@ -0,0 +1,28 @@
+#!/bin/sh -e
+. t/common.sh
+
+printf '%s\n' > file \
+ "${HEAD} ours" \
+ same \
+ ours \
+ "${PARENT} base" \
+ same \
+ base \
+ "${TAIL}" \
+ same \
+ theirs \
+ "${END} theirs"
+
+printf '%s\n' > file.expected \
+ same \
+ "${HEAD} ours" \
+ ours \
+ "${PARENT} base" \
+ base \
+ "${TAIL}" \
+ theirs \
+ "${END} theirs"
+
+conflict --select-head --no-select-head --no-select-head file
+
+also_interactive rr
diff --git a/t/combination-select-head-reduce b/t/combination-select-head-reduce
new file mode 100755
index 0000000..796f613
--- /dev/null
+++ b/t/combination-select-head-reduce
@@ -0,0 +1,30 @@
+#!/bin/sh -e
+. t/common.sh
+
+printf '%s\n' > file \
+ "${HEAD} ours" \
+ first \
+ "${PARENT} base" \
+ not first \
+ "${TAIL}" \
+ not first \
+ "${END} theirs" \
+ "${HEAD} ours" \
+ ours \
+ common \
+ "${PARENT} base" \
+ base \
+ common \
+ "${TAIL}" \
+ theirs \
+ common \
+ "${END} theirs"
+
+printf '%s\n' > file.expected \
+ first \
+ ours \
+ common
+
+normal --select-head --reduce file
+
+also_interactive hh
diff --git a/t/combination-select-tail-2-no-select-tail b/t/combination-select-tail-2-no-select-tail
new file mode 100755
index 0000000..55eb1d6
--- /dev/null
+++ b/t/combination-select-tail-2-no-select-tail
@@ -0,0 +1,28 @@
+#!/bin/sh -e
+. t/common.sh
+
+printf '%s\n' > file \
+ "${HEAD} ours" \
+ same \
+ ours \
+ "${PARENT} base" \
+ same \
+ base \
+ "${TAIL}" \
+ same \
+ theirs \
+ "${END} theirs"
+
+printf '%s\n' > file.expected \
+ same \
+ "${HEAD} ours" \
+ ours \
+ "${PARENT} base" \
+ base \
+ "${TAIL}" \
+ theirs \
+ "${END} theirs"
+
+conflict --select-tail --no-select-tail --no-select-tail file
+
+also_interactive rr
diff --git a/t/combination-select-tail-reduce b/t/combination-select-tail-reduce
new file mode 100755
index 0000000..c1504ac
--- /dev/null
+++ b/t/combination-select-tail-reduce
@@ -0,0 +1,30 @@
+#!/bin/sh -e
+. t/common.sh
+
+printf '%s\n' > file \
+ "${HEAD} ours" \
+ not 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 \
+ theirs \
+ common
+
+normal --select-tail --reduce file
+
+also_interactive tt
diff --git a/t/common.sh b/t/common.sh
new file mode 100644
index 0000000..30e4b6e
--- /dev/null
+++ b/t/common.sh
@@ -0,0 +1,165 @@
+set -eu
+
+tmp=testdir.$$
+command="../git-rediff"
+
+interactive () {
+ test $# = 0
+ command="../git-rediff.t"
+}
+
+cleanup () {
+ cd -- ..
+ rm -rf -- "${tmp}"
+}
+
+trap cleanup 0 HUP INT TERM
+
+mkdir -- "${tmp}"
+cd -- "${tmp}"
+git init -q .
+git config user.email test@example.invalid
+git config user.name test
+
+set_marker_variables () {
+ test $# = 1
+ HEAD="$(printf '%*.s\n' "$1" '' | tr ' ' '<')"
+ PARENT="$(printf '%*.s\n' "$1" '' | tr ' ' '|')"
+ TAIL="$(printf '%*.s\n' "$1" '' | tr ' ' '=')"
+ END="$(printf '%*.s\n' "$1" '' | tr ' ' '>')"
+}
+
+set_marker_variables 7
+
+set_conflict_marker_size () {
+ test $# = 2
+ # $1 = marker size
+ # $2 = affected file
+ set_marker_variables "$1"
+ printf '%s conflict-marker-size=%s\n' "$2" "$1" >> .gitattributes
+}
+
+again_command=''
+
+save_files () {
+ while test ! $# = 0; do
+ if test "$1" = - || test "$1" = --; then
+ shift 1
+ break
+ elif printf '%s\n' "$1" | grep -q '^-'; then
+ shift 1
+ else
+ break
+ fi
+ done
+ for f; do
+ if test ! "$f" = -; then
+ cp -- "$f" "$f".original
+ fi
+ done
+}
+
+no_error () {
+ test -z "${again_command}"
+ again_command="$*"
+ shift 1
+ save_files "$@"
+ set +e
+ "$command" "$@"
+ status=$?
+ set -e
+ while test ! $# = 0; do
+ if test "$1" = - || test "$1" = --; then
+ shift 1
+ break
+ elif printf '%s\n' "$1" | grep -q '^-'; then
+ shift 1
+ else
+ break
+ fi
+ done
+ for f; do
+ if test ! "$f" = -; then
+ diff -u -- "$f" "$f".expected >&2
+ cp -- "$f".original "$f"
+ fi
+ done
+}
+
+normal () {
+ no_error normal "$@"
+ test $status = 0
+}
+
+conflict () {
+ no_error conflict "$@"
+ test $status = 1
+}
+
+error () {
+ set +e
+ "$command" "$@" 2>/dev/null
+ status=$?
+ set -e
+ test $status = 2
+}
+
+also_interactive () {
+ test $# = 1
+ test -n "${again_command}"
+ cmd="${again_command}"
+ again_command=''
+ interactive
+ printf '%s' "$1" > i-input
+ ${cmd} 8<i-input 9>/dev/null
+}
+
+interactive_line () {
+ test $# = 2
+ printf '\e[%sm%s\n\e[0m' "$1" "$2"
+}
+
+interactive_marker () {
+ test $# = 1
+ interactive_line 36 "$1"
+}
+
+interactive_side () {
+ test $# = 1
+ interactive_line 32 "$1"
+}
+
+interactive_base () {
+ test $# = 1
+ interactive_line 31 "$1"
+}
+
+interactive_filename () {
+ test $# = 1
+ printf '\e[1m%s\e[0m\n' "$1"
+}
+
+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"
+}
+
+interactive_help () {
+ test $# = 1
+ printf '\e[1;31m'
+ printf '%s\n' \
+ 'h - select head' \
+ 't - select tail'
+ if test "$1" = 3; then
+ printf '%s\n' 'b - remove base'
+ fi
+ printf '%s\n' \
+ 'm - merge' \
+ 'r - reduce' \
+ 'e - manually edit this hunk' \
+ 's - skip this hunk' \
+ 'd - skip this hunk and all later hunks in the file' \
+ 'q - quit; skip this hunk and all remaining hunks' \
+ '? - print help'
+ printf '\e[0m'
+}
diff --git a/t/interactive-edit b/t/interactive-edit
new file mode 100755
index 0000000..e0ff7b4
--- /dev/null
+++ b/t/interactive-edit
@@ -0,0 +1,64 @@
+#!/bin/sh -e
+. t/common.sh
+interactive
+
+printf '%s\n' > file \
+ "${HEAD} ours" \
+ head \
+ "${TAIL}" \
+ tail \
+ "${END} theirs"
+
+for editor in EDITOR VISUAL CORE_EDITOR REDIFF_EDITOR GIT_EDITOR; do
+ printf '%s\n' > "${editor}" \
+ '#!/bin/sh -e' \
+ 'printf "%s\\n" "$(basename -- "$0")" > "$1"'
+ chmod +x -- "$editor"
+done
+
+printf '%s\n' \
+ '#!/bin/sh -e' \
+ 'printf "%s\\n" vi > "$1"' > ./vi
+chmod +x ./vi
+
+(
+ interactive_marker "${HEAD} ours"
+ interactive_side head
+ interactive_marker "${TAIL}"
+ interactive_side tail
+ interactive_marker "${END} theirs"
+ interactive_prompt 1 1
+) > output.expected
+
+run_editor_test () {
+ printf '%s\n' "$1" > file.expected
+ shift 1
+
+ unset GIT_EDITOR VISUAL EDITOR
+ git config --local --unset-all core.editor || :
+ git config --local --unset-all rediff.editor || :
+
+ while test ! $# = 0; do
+ if test "$1" = VI; then
+ export PATH=".:${PATH}"
+ elif test "$1" = CORE_EDITOR; then
+ git config core.editor ./CORE_EDITOR
+ elif test "$1" = REDIFF_EDITOR; then
+ git config rediff.editor ./REDIFF_EDITOR
+ else
+ eval "export $1=./$1"
+ fi
+ shift 1
+ done
+ printf '%s' e > input
+ normal --interactive file 8<input 9>output
+ diff -u -- output.expected output >&2
+ again_command=''
+}
+
+run_editor_test vi VI
+run_editor_test EDITOR EDITOR
+run_editor_test VISUAL EDITOR VISUAL
+run_editor_test CORE_EDITOR EDITOR VISUAL CORE_EDITOR
+run_editor_test REDIFF_EDITOR EDITOR VISUAL CORE_EDITOR REDIFF_EDITOR
+run_editor_test GIT_EDITOR EDITOR VISUAL CORE_EDITOR REDIFF_EDITOR GIT_EDITOR
diff --git a/t/interactive-help b/t/interactive-help
new file mode 100755
index 0000000..398d9dc
--- /dev/null
+++ b/t/interactive-help
@@ -0,0 +1,28 @@
+#!/bin/sh -e
+. t/common.sh
+interactive
+
+printf '%s\n' > file \
+ "${HEAD} ours" \
+ head \
+ "${TAIL}" \
+ tail \
+ "${END} theirs"
+
+printf '%s\n' > file.expected \
+ head
+
+(
+ interactive_marker "${HEAD} ours"
+ interactive_side head
+ interactive_marker "${TAIL}"
+ interactive_side tail
+ interactive_marker "${END} theirs"
+ interactive_prompt 1 1
+ interactive_help 2
+ interactive_prompt 1 1
+) > output.expected
+
+printf '%s' '?h' > input
+normal --interactive file 8<input 9>output
+diff -u -- output.expected output >&2
diff --git a/t/interactive-multiple-commands b/t/interactive-multiple-commands
new file mode 100755
index 0000000..a279541
--- /dev/null
+++ b/t/interactive-multiple-commands
@@ -0,0 +1,113 @@
+#!/bin/sh -e
+. t/common.sh
+interactive
+
+printf '%s\n' > file \
+ "${HEAD} ours" \
+ head \
+ "${PARENT} base" \
+ base \
+ "${TAIL}" \
+ tail \
+ "${END} theirs" \
+ "${HEAD} ours" \
+ head \
+ "${PARENT} base" \
+ base \
+ "${TAIL}" \
+ tail \
+ "${END} theirs" \
+ "${HEAD} ours" \
+ head \
+ "${PARENT} base" \
+ base \
+ "${TAIL}" \
+ tail \
+ "${END} theirs" \
+ "${HEAD} ours" \
+ first \
+ "${PARENT} base" \
+ not first \
+ "${TAIL}" \
+ not first \
+ "${END} theirs" \
+ "${HEAD} ours" \
+ ours \
+ common \
+ "${PARENT} base" \
+ base \
+ common \
+ "${TAIL}" \
+ theirs \
+ common \
+ "${END} theirs"
+
+printf '%s\n' > file.expected \
+ head \
+ tail \
+ "${HEAD} ours" \
+ head \
+ "${TAIL}" \
+ tail \
+ "${END} theirs" \
+ first \
+ "${HEAD} ours" \
+ ours \
+ "${PARENT} base" \
+ base \
+ "${TAIL}" \
+ theirs \
+ "${END} theirs" \
+ common
+
+(
+ interactive_marker "${HEAD} ours"
+ interactive_side head
+ interactive_marker "${PARENT} base"
+ interactive_base base
+ interactive_marker "${TAIL}"
+ interactive_side tail
+ interactive_marker "${END} theirs"
+ interactive_prompt 1 5
+ interactive_marker "${HEAD} ours"
+ interactive_side head
+ interactive_marker "${PARENT} base"
+ interactive_base base
+ interactive_marker "${TAIL}"
+ interactive_side tail
+ interactive_marker "${END} theirs"
+ interactive_prompt 2 5
+ interactive_marker "${HEAD} ours"
+ interactive_side head
+ interactive_marker "${PARENT} base"
+ interactive_base base
+ interactive_marker "${TAIL}"
+ interactive_side tail
+ interactive_marker "${END} theirs"
+ interactive_prompt 3 5
+ interactive_marker "${HEAD} ours"
+ interactive_side first
+ interactive_marker "${PARENT} base"
+ interactive_base not
+ interactive_base first
+ interactive_marker "${TAIL}"
+ interactive_side not
+ interactive_side first
+ interactive_marker "${END} theirs"
+ interactive_prompt 4 5
+ interactive_marker "${HEAD} ours"
+ interactive_side ours
+ interactive_side common
+ interactive_marker "${PARENT} base"
+ interactive_base base
+ interactive_base common
+ interactive_marker "${TAIL}"
+ interactive_side theirs
+ interactive_side common
+ interactive_marker "${END} theirs"
+ interactive_prompt 5 5
+) > output.expected
+
+printf '%s' htbmr > input
+conflict --interactive file 8<input 9>output
+diff -u -- output.expected output >&2
diff --git a/t/interactive-short-option b/t/interactive-short-option
new file mode 100755
index 0000000..904f7e2
--- /dev/null
+++ b/t/interactive-short-option
@@ -0,0 +1,46 @@
+#!/bin/sh -e
+. t/common.sh
+interactive
+
+printf '%s\n' > file \
+ "${HEAD} ours" \
+ ours-1 \
+ "${PARENT} base" \
+ base-1 \
+ "${TAIL}" \
+ theirs-1 \
+ "${END} theirs" \
+ "${HEAD} ours" \
+ ours-2 \
+ "${PARENT} base" \
+ base-2 \
+ "${TAIL}" \
+ theirs-2 \
+ "${END} theirs"
+
+printf '%s\n' > file.expected \
+ theirs-1 \
+ ours-2
+
+(
+ interactive_marker "${HEAD} ours"
+ interactive_side ours-1
+ interactive_marker "${PARENT} base"
+ interactive_base base-1
+ interactive_marker "${TAIL}"
+ interactive_side theirs-1
+ interactive_marker "${END} theirs"
+ interactive_prompt 1 2
+ interactive_marker "${HEAD} ours"
+ interactive_side ours-2
+ interactive_marker "${PARENT} base"
+ interactive_base base-2
+ interactive_marker "${TAIL}"
+ interactive_side theirs-2
+ interactive_marker "${END} theirs"
+ interactive_prompt 2 2
+) > output.expected
+
+printf '%s' th > input
+normal -i file 8<input 9>output
+diff -u -- output.expected output >&2
diff --git a/t/interactive-skip b/t/interactive-skip
new file mode 100755
index 0000000..baf9e1b
--- /dev/null
+++ b/t/interactive-skip
@@ -0,0 +1,30 @@
+#!/bin/sh -e
+. t/common.sh
+interactive
+
+printf '%s\n' > file \
+ "${HEAD} ours" \
+ head \
+ "${TAIL}" \
+ tail \
+ "${END} theirs"
+
+printf '%s\n' > file.expected \
+ "${HEAD} ours" \
+ head \
+ "${TAIL}" \
+ tail \
+ "${END} theirs"
+
+(
+ interactive_marker "${HEAD} ours"
+ interactive_side head
+ interactive_marker "${TAIL}"
+ interactive_side tail
+ interactive_marker "${END} theirs"
+ interactive_prompt 1 1
+) > output.expected
+
+printf '%s' s > input
+conflict --interactive file 8<input 9>output
+diff -u -- output.expected output >&2
diff --git a/t/interactive-two-files b/t/interactive-two-files
new file mode 100755
index 0000000..c2c3f02
--- /dev/null
+++ b/t/interactive-two-files
@@ -0,0 +1,44 @@
+#!/bin/sh -e
+. t/common.sh
+interactive
+
+printf '%s\n' > file \
+ "${HEAD} ours" \
+ ours-1 \
+ "${TAIL}" \
+ theirs-1 \
+ "${END} theirs"
+
+printf '%s\n' > file.expected \
+ theirs-1
+
+printf '%s\n' > file2 \
+ "${HEAD} ours" \
+ ours-2 \
+ "${TAIL}" \
+ theirs-2 \
+ "${END} theirs"
+
+printf '%s\n' > file2.expected \
+ ours-2
+
+(
+ interactive_filename file
+ interactive_marker "${HEAD} ours"
+ interactive_side ours-1
+ interactive_marker "${TAIL}"
+ interactive_side theirs-1
+ interactive_marker "${END} theirs"
+ interactive_prompt 1 1
+ interactive_filename file2
+ interactive_marker "${HEAD} ours"
+ interactive_side ours-2
+ interactive_marker "${TAIL}"
+ interactive_side theirs-2
+ interactive_marker "${END} theirs"
+ interactive_prompt 1 1
+) > output.expected
+
+printf '%s' th > input
+normal --interactive file file2 8<input 9>output
+diff -u -- output.expected output >&2
diff --git a/t/invalid-option-combinations b/t/invalid-option-combinations
new file mode 100755
index 0000000..527ce63
--- /dev/null
+++ b/t/invalid-option-combinations
@@ -0,0 +1,20 @@
+#!/bin/sh -e
+. t/common.sh
+
+printf '%s\n' text > file
+cp -- file file.expected
+
+error --merge --select-head file
+error --select-head --merge file
+error --merge --select-tail file
+error --select-tail --merge file
+error --select-head --select-tail file
+error --select-tail --select-head file
+error --merge --select-head --select-tail file
+error --merge --select-tail --select-head file
+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 --unknown-option file
+error '-$' file
diff --git a/t/marker-size-10 b/t/marker-size-10
new file mode 100755
index 0000000..38fdc8d
--- /dev/null
+++ b/t/marker-size-10
@@ -0,0 +1,18 @@
+#!/bin/sh -e
+. t/common.sh
+
+set_conflict_marker_size 10 file
+
+printf '%s\n' > file \
+ "${HEAD} ours" \
+ same \
+ "${TAIL}" \
+ same \
+ "${END} theirs"
+
+printf '%s\n' > file.expected \
+ same
+
+normal file
+
+also_interactive r
diff --git a/t/marker-size-3 b/t/marker-size-3
new file mode 100755
index 0000000..1cab4dd
--- /dev/null
+++ b/t/marker-size-3
@@ -0,0 +1,18 @@
+#!/bin/sh -e
+. t/common.sh
+
+set_conflict_marker_size 3 file
+
+printf '%s\n' > file \
+ "${HEAD} ours" \
+ same \
+ "${TAIL}" \
+ same \
+ "${END} theirs'"
+
+printf '%s\n' > file.expected \
+ same
+
+normal file
+
+also_interactive r
diff --git a/t/marker-size-longer-ignored b/t/marker-size-longer-ignored
new file mode 100755
index 0000000..4453d7f
--- /dev/null
+++ b/t/marker-size-longer-ignored
@@ -0,0 +1,16 @@
+#!/bin/sh -e
+. t/common.sh
+
+set_conflict_marker_size 3 file
+
+printf '%s\n' > file \
+ '<<<< not a marker' \
+ '|||| not a marker' \
+ '==== not a marker' \
+ '>>>> not a marker'
+
+cp -- file file.expected
+
+normal file
+
+also_interactive ''
diff --git a/t/marker-size-shorter-ignored b/t/marker-size-shorter-ignored
new file mode 100755
index 0000000..cf16be7
--- /dev/null
+++ b/t/marker-size-shorter-ignored
@@ -0,0 +1,16 @@
+#!/bin/sh -e
+. t/common.sh
+
+set_conflict_marker_size 3 file
+
+printf '%s\n' > file \
+ '<< not a marker' \
+ '| not a marker' \
+ '== not a marker' \
+ '> not a marker'
+
+cp -- file file.expected
+
+normal file
+
+also_interactive ''
diff --git a/t/merge-1 b/t/merge-1
new file mode 100755
index 0000000..5967f4e
--- /dev/null
+++ b/t/merge-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 --merge --no-reduce file
diff --git a/t/merge-2-different b/t/merge-2-different
new file mode 100755
index 0000000..b27a88e
--- /dev/null
+++ b/t/merge-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 --merge --no-reduce file
+
+also_interactive m
diff --git a/t/merge-2-identical b/t/merge-2-identical
new file mode 100755
index 0000000..eafc4d1
--- /dev/null
+++ b/t/merge-2-identical
@@ -0,0 +1,15 @@
+#!/bin/sh -e
+. t/common.sh
+
+printf '%s\n' > file \
+ "${HEAD} ours" \
+ same \
+ "${TAIL}" \
+ same \
+ "${END} theirs"
+
+cp -- file file.expected
+
+conflict --merge --no-reduce file
+
+also_interactive m
diff --git a/t/merge-base b/t/merge-base
new file mode 100755
index 0000000..8b7969e
--- /dev/null
+++ b/t/merge-base
@@ -0,0 +1,17 @@
+#!/bin/sh -e
+. t/common.sh
+
+printf '%s\n' > file \
+ "${HEAD} a" \
+ same \
+ "${PARENT} b" \
+ different \
+ "${TAIL}" \
+ same \
+ "${END} c"
+
+cp -- file file.expected
+
+conflict --merge --no-reduce file
+
+also_interactive m
diff --git a/t/merge-empty-base b/t/merge-empty-base
new file mode 100755
index 0000000..6298768
--- /dev/null
+++ b/t/merge-empty-base
@@ -0,0 +1,16 @@
+#!/bin/sh -e
+. t/common.sh
+
+printf '%s\n' > file \
+ "${HEAD} ours" \
+ same \
+ "${PARENT} base" \
+ "${TAIL}" \
+ same \
+ "${END} theirs"
+
+cp -- file file.expected
+
+conflict --merge --no-reduce file
+
+also_interactive m
diff --git a/t/merge-empty-head b/t/merge-empty-head
new file mode 100755
index 0000000..257df23
--- /dev/null
+++ b/t/merge-empty-head
@@ -0,0 +1,16 @@
+#!/bin/sh -e
+. t/common.sh
+
+printf '%s\n' > file \
+ "${HEAD} ours" \
+ "${PARENT} base" \
+ same \
+ "${TAIL}" \
+ same \
+ "${END} theirs"
+
+: > file.expected
+
+normal --merge --no-reduce file
+
+also_interactive m
diff --git a/t/merge-empty-tail b/t/merge-empty-tail
new file mode 100755
index 0000000..0574dfc
--- /dev/null
+++ b/t/merge-empty-tail
@@ -0,0 +1,16 @@
+#!/bin/sh -e
+. t/common.sh
+
+printf '%s\n' > file \
+ "${HEAD} ours" \
+ same \
+ "${PARENT} base" \
+ same \
+ "${TAIL}" \
+ "${END} theirs"
+
+: > file.expected
+
+normal --merge --no-reduce file
+
+also_interactive m
diff --git a/t/merge-head b/t/merge-head
new file mode 100755
index 0000000..a87492e
--- /dev/null
+++ b/t/merge-head
@@ -0,0 +1,18 @@
+#!/bin/sh -e
+. t/common.sh
+
+printf '%s\n' > file \
+ "${HEAD} a" \
+ different \
+ "${PARENT} b" \
+ same \
+ "${TAIL}" \
+ same \
+ "${END} c"
+
+printf '%s\n' > file.expected \
+ different
+
+normal --merge --no-reduce file
+
+also_interactive m
diff --git a/t/merge-head-4 b/t/merge-head-4
new file mode 100755
index 0000000..288ac66
--- /dev/null
+++ b/t/merge-head-4
@@ -0,0 +1,20 @@
+#!/bin/sh -e
+. t/common.sh
+
+printf '%s\n' > file \
+ "${HEAD} 1" \
+ different \
+ "${PARENT} 2" \
+ same \
+ "${PARENT} 3" \
+ same \
+ "${TAIL}" \
+ same \
+ "${END} 4"
+
+printf '%s\n' > file.expected \
+ different
+
+normal --merge --no-reduce file
+
+also_interactive m
diff --git a/t/merge-lower-middle-4 b/t/merge-lower-middle-4
new file mode 100755
index 0000000..a378bcc
--- /dev/null
+++ b/t/merge-lower-middle-4
@@ -0,0 +1,28 @@
+#!/bin/sh -e
+. t/common.sh
+
+printf '%s\n' > file \
+ "${HEAD} 1" \
+ same \
+ "${PARENT} 2" \
+ same \
+ "${PARENT} 3" \
+ different \
+ "${TAIL}" \
+ same \
+ "${END} 4"
+
+printf '%s\n' > file.expected \
+ "${HEAD} 1" \
+ same \
+ "${PARENT} 2" \
+ same \
+ "${PARENT} 3" \
+ different \
+ "${TAIL}" \
+ same \
+ "${END} 4"
+
+conflict --merge --no-reduce file
+
+also_interactive m
diff --git a/t/merge-multiple-conflicts b/t/merge-multiple-conflicts
new file mode 100755
index 0000000..7aaccbc
--- /dev/null
+++ b/t/merge-multiple-conflicts
@@ -0,0 +1,34 @@
+#!/bin/sh -e
+. t/common.sh
+
+printf '%s\n' > file \
+ before \
+ "${HEAD} ours" \
+ same \
+ "${PARENT} base" \
+ same \
+ "${TAIL}" \
+ different-three \
+ "${END} theirs" \
+ between \
+ "${HEAD} ours" \
+ different-four \
+ "${PARENT} base" \
+ same \
+ "${PARENT} other" \
+ same \
+ "${TAIL}" \
+ same \
+ "${END} theirs" \
+ after
+
+printf '%s\n' > file.expected \
+ before \
+ different-three \
+ between \
+ different-four \
+ after
+
+normal --merge --no-reduce file
+
+also_interactive mm
diff --git a/t/merge-multiple-files b/t/merge-multiple-files
new file mode 100755
index 0000000..23a8bb5
--- /dev/null
+++ b/t/merge-multiple-files
@@ -0,0 +1,30 @@
+#!/bin/sh -e
+. t/common.sh
+
+printf '%s\n' > first \
+ "${HEAD} ours" \
+ same \
+ "${PARENT} base" \
+ same \
+ "${TAIL}" \
+ first-different \
+ "${END} theirs"
+
+printf '%s\n' > first.expected \
+ first-different
+
+printf '%s\n' > second \
+ "${HEAD} ours" \
+ second-different \
+ "${PARENT} base" \
+ same \
+ "${TAIL}" \
+ same \
+ "${END} theirs"
+
+printf '%s\n' > second.expected \
+ second-different
+
+normal --merge --no-reduce first second
+
+also_interactive mm
diff --git a/t/merge-no-conflicts b/t/merge-no-conflicts
new file mode 100755
index 0000000..5dc5804
--- /dev/null
+++ b/t/merge-no-conflicts
@@ -0,0 +1,11 @@
+#!/bin/sh -e
+. t/common.sh
+
+printf '%s\n' > file \
+ no conflicts
+
+cp -- file file.expected
+
+normal --merge --no-reduce file
+
+also_interactive ''
diff --git a/t/merge-short-option b/t/merge-short-option
new file mode 100755
index 0000000..3a68ee0
--- /dev/null
+++ b/t/merge-short-option
@@ -0,0 +1,18 @@
+#!/bin/sh -e
+. t/common.sh
+
+printf '%s\n' > file \
+ "${HEAD} ours" \
+ same \
+ "${PARENT} base" \
+ same \
+ "${TAIL}" \
+ different \
+ "${END} theirs"
+
+printf '%s\n' > file.expected \
+ different
+
+normal -m --no-reduce file
+
+also_interactive s
diff --git a/t/merge-tail b/t/merge-tail
new file mode 100755
index 0000000..c3949c0
--- /dev/null
+++ b/t/merge-tail
@@ -0,0 +1,18 @@
+#!/bin/sh -e
+. t/common.sh
+
+printf '%s\n' > file \
+ "${HEAD} a" \
+ same \
+ "${PARENT} b" \
+ same \
+ "${TAIL}" \
+ different \
+ "${END} c"
+
+printf '%s\n' > file.expected \
+ different
+
+normal --merge --no-reduce file
+
+also_interactive m
diff --git a/t/merge-tail-4 b/t/merge-tail-4
new file mode 100755
index 0000000..ecfff16
--- /dev/null
+++ b/t/merge-tail-4
@@ -0,0 +1,20 @@
+#!/bin/sh -e
+. t/common.sh
+
+printf '%s\n' > file \
+ "${HEAD} 1" \
+ same \
+ "${PARENT} 2" \
+ same \
+ "${PARENT} 3" \
+ same \
+ "${TAIL}" \
+ different \
+ "${END} 4"
+
+printf '%s\n' > file.expected \
+ different
+
+normal --merge --no-reduce file
+
+also_interactive m
diff --git a/t/merge-upper-middle-4 b/t/merge-upper-middle-4
new file mode 100755
index 0000000..c113241
--- /dev/null
+++ b/t/merge-upper-middle-4
@@ -0,0 +1,28 @@
+#!/bin/sh -e
+. t/common.sh
+
+printf '%s\n' > file \
+ "${HEAD} 1" \
+ same \
+ "${PARENT} 2" \
+ different \
+ "${PARENT} 3" \
+ same \
+ "${TAIL}" \
+ same \
+ "${END} 4"
+
+printf '%s\n' > file.expected \
+ "${HEAD} 1" \
+ same \
+ "${PARENT} 2" \
+ different \
+ "${PARENT} 3" \
+ same \
+ "${TAIL}" \
+ same \
+ "${END} 4"
+
+conflict --merge --no-reduce file
+
+also_interactive m
diff --git a/t/missing-newline-non-conflict b/t/missing-newline-non-conflict
new file mode 100755
index 0000000..a89640a
--- /dev/null
+++ b/t/missing-newline-non-conflict
@@ -0,0 +1,10 @@
+#!/bin/sh -e
+. t/common.sh
+
+printf '%s' 'non-conflict without newline' > file
+
+cp -- file file.expected
+
+normal file
+
+also_interactive ''
diff --git a/t/mixed-marker-sizes b/t/mixed-marker-sizes
new file mode 100755
index 0000000..8cd186b
--- /dev/null
+++ b/t/mixed-marker-sizes
@@ -0,0 +1,30 @@
+#!/bin/sh -e
+. t/common.sh
+
+set_conflict_marker_size 3 small
+
+printf '%s\n' > small \
+ "${HEAD} small-head" \
+ shared \
+ "${TAIL}" \
+ shared \
+ "${END} small-tail"
+
+printf '%s\n' > small.expected \
+ shared
+
+set_conflict_marker_size 10 large
+
+printf '%s\n' > large \
+ "${HEAD} large-head" \
+ shared \
+ "${TAIL}" \
+ shared \
+ "${END} large-tail"
+
+printf '%s\n' > large.expected \
+ shared
+
+normal small large
+
+also_interactive rr
diff --git a/t/no-option-interactive b/t/no-option-interactive
new file mode 100755
index 0000000..5609f68
--- /dev/null
+++ b/t/no-option-interactive
@@ -0,0 +1,30 @@
+#!/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 \
+ "${HEAD} ours" \
+ ours \
+ "${PARENT} base" \
+ base \
+ "${TAIL}" \
+ theirs \
+ "${END} theirs"
+
+printf '%s' q > input
+
+conflict --no-interactive file 8<input 9>output
+test ! -s output
diff --git a/t/no-option-interactive-no-yes b/t/no-option-interactive-no-yes
new file mode 100755
index 0000000..7417667
--- /dev/null
+++ b/t/no-option-interactive-no-yes
@@ -0,0 +1,20 @@
+#!/bin/sh -e
+. t/common.sh
+
+interactive
+
+printf '%s\n' > file \
+ "${HEAD} ours" \
+ ours \
+ "${PARENT} base" \
+ base \
+ "${TAIL}" \
+ theirs \
+ "${END} theirs"
+
+printf '%s\n' > file.expected \
+ ours
+
+printf '%s' h > input
+
+normal --no-interactive --interactive --no-reduce file 8<input 9>output
diff --git a/t/no-option-interactive-yes-no b/t/no-option-interactive-yes-no
new file mode 100755
index 0000000..2a3b0c4
--- /dev/null
+++ b/t/no-option-interactive-yes-no
@@ -0,0 +1,19 @@
+#!/bin/sh -e
+. t/common.sh
+
+interactive
+
+printf '%s\n' > file \
+ "${HEAD} ours" \
+ ours \
+ "${PARENT} base" \
+ base \
+ "${TAIL}" \
+ theirs \
+ "${END} theirs"
+
+cp -- file file.expected
+
+printf '%s' q > input
+
+conflict --interactive --no-interactive --no-reduce file 8<input 9>output
diff --git a/t/no-option-merge b/t/no-option-merge
new file mode 100755
index 0000000..5f55d74
--- /dev/null
+++ b/t/no-option-merge
@@ -0,0 +1,28 @@
+#!/bin/sh -e
+. t/common.sh
+
+printf '%s\n' > file \
+ "${HEAD} ours" \
+ same \
+ ours \
+ "${PARENT} base" \
+ same \
+ base \
+ "${TAIL}" \
+ same \
+ theirs \
+ "${END} theirs"
+
+printf '%s\n' > file.expected \
+ same \
+ "${HEAD} ours" \
+ ours \
+ "${PARENT} base" \
+ base \
+ "${TAIL}" \
+ theirs \
+ "${END} theirs"
+
+conflict --no-merge file
+
+also_interactive rr
diff --git a/t/no-option-merge-no-yes b/t/no-option-merge-no-yes
new file mode 100755
index 0000000..8ae2bc4
--- /dev/null
+++ b/t/no-option-merge-no-yes
@@ -0,0 +1,18 @@
+#!/bin/sh -e
+. t/common.sh
+
+printf '%s\n' > file \
+ "${HEAD} ours" \
+ old \
+ "${PARENT} base" \
+ old \
+ "${TAIL}" \
+ new \
+ "${END} theirs"
+
+printf '%s\n' > file.expected \
+ new
+
+normal --no-merge --merge --no-reduce file
+
+also_interactive m
diff --git a/t/no-option-merge-yes-no b/t/no-option-merge-yes-no
new file mode 100755
index 0000000..f351e7c
--- /dev/null
+++ b/t/no-option-merge-yes-no
@@ -0,0 +1,17 @@
+#!/bin/sh -e
+. t/common.sh
+
+printf '%s\n' > file \
+ "${HEAD} ours" \
+ ours \
+ "${PARENT} base" \
+ base \
+ "${TAIL}" \
+ theirs \
+ "${END} theirs"
+
+cp -- file file.expected
+
+conflict --merge --no-merge file
+
+also_interactive r
diff --git a/t/no-option-reduce b/t/no-option-reduce
new file mode 100755
index 0000000..3c345b2
--- /dev/null
+++ b/t/no-option-reduce
@@ -0,0 +1,20 @@
+#!/bin/sh -e
+. t/common.sh
+
+printf '%s\n' > file \
+ "${HEAD} ours" \
+ same \
+ ours \
+ "${PARENT} base" \
+ same \
+ base \
+ "${TAIL}" \
+ same \
+ theirs \
+ "${END} theirs"
+
+cp -- file file.expected
+
+conflict --no-reduce file
+
+also_interactive s
diff --git a/t/no-option-reduce-no-yes b/t/no-option-reduce-no-yes
new file mode 100755
index 0000000..e6a7d8c
--- /dev/null
+++ b/t/no-option-reduce-no-yes
@@ -0,0 +1,17 @@
+#!/bin/sh -e
+. t/common.sh
+
+printf '%s\n' > file \
+ "${HEAD} ours" \
+ ours \
+ "${PARENT} base" \
+ base \
+ "${TAIL}" \
+ theirs \
+ "${END} theirs"
+
+cp -- file file.expected
+
+conflict --no-reduce --reduce file
+
+also_interactive r
diff --git a/t/no-option-reduce-yes-no b/t/no-option-reduce-yes-no
new file mode 100755
index 0000000..82fa7bd
--- /dev/null
+++ b/t/no-option-reduce-yes-no
@@ -0,0 +1,17 @@
+#!/bin/sh -e
+. t/common.sh
+
+printf '%s\n' > file \
+ "${HEAD} ours" \
+ ours \
+ "${PARENT} base" \
+ base \
+ "${TAIL}" \
+ theirs \
+ "${END} theirs"
+
+cp -- file file.expected
+
+conflict --reduce --no-reduce file
+
+also_interactive s
diff --git a/t/no-option-remove-base b/t/no-option-remove-base
new file mode 100755
index 0000000..fbb8385
--- /dev/null
+++ b/t/no-option-remove-base
@@ -0,0 +1,28 @@
+#!/bin/sh -e
+. t/common.sh
+
+printf '%s\n' > file \
+ "${HEAD} ours" \
+ same \
+ ours \
+ "${PARENT} base" \
+ same \
+ base \
+ "${TAIL}" \
+ same \
+ theirs \
+ "${END} theirs"
+
+printf '%s\n' > file.expected \
+ same \
+ "${HEAD} ours" \
+ ours \
+ "${PARENT} base" \
+ base \
+ "${TAIL}" \
+ theirs \
+ "${END} theirs"
+
+conflict --no-remove-base file
+
+also_interactive rr
diff --git a/t/no-option-remove-base-no-yes b/t/no-option-remove-base-no-yes
new file mode 100755
index 0000000..9f6fed8
--- /dev/null
+++ b/t/no-option-remove-base-no-yes
@@ -0,0 +1,22 @@
+#!/bin/sh -e
+. t/common.sh
+
+printf '%s\n' > file \
+ "${HEAD} ours" \
+ ours \
+ "${PARENT} base" \
+ base \
+ "${TAIL}" \
+ theirs \
+ "${END} theirs"
+
+printf '%s\n' > file.expected \
+ "${HEAD} ours" \
+ ours \
+ "${TAIL}" \
+ theirs \
+ "${END} theirs"
+
+conflict --no-remove-base --remove-base --no-reduce file
+
+also_interactive bb
diff --git a/t/no-option-remove-base-yes-no b/t/no-option-remove-base-yes-no
new file mode 100755
index 0000000..e93b8ee
--- /dev/null
+++ b/t/no-option-remove-base-yes-no
@@ -0,0 +1,24 @@
+#!/bin/sh -e
+. t/common.sh
+
+printf '%s\n' > file \
+ "${HEAD} ours" \
+ ours \
+ "${PARENT} base" \
+ base \
+ "${TAIL}" \
+ theirs \
+ "${END} theirs"
+
+printf '%s\n' > file.expected \
+ "${HEAD} ours" \
+ ours \
+ "${PARENT} base" \
+ base \
+ "${TAIL}" \
+ theirs \
+ "${END} theirs"
+
+conflict --remove-base --no-remove-base --no-reduce file
+
+also_interactive ss
diff --git a/t/no-option-select-head b/t/no-option-select-head
new file mode 100755
index 0000000..d882b14
--- /dev/null
+++ b/t/no-option-select-head
@@ -0,0 +1,28 @@
+#!/bin/sh -e
+. t/common.sh
+
+printf '%s\n' > file \
+ "${HEAD} ours" \
+ same \
+ ours \
+ "${PARENT} base" \
+ same \
+ base \
+ "${TAIL}" \
+ same \
+ theirs \
+ "${END} theirs"
+
+printf '%s\n' > file.expected \
+ same \
+ "${HEAD} ours" \
+ ours \
+ "${PARENT} base" \
+ base \
+ "${TAIL}" \
+ theirs \
+ "${END} theirs"
+
+conflict --no-select-head file
+
+also_interactive rr
diff --git a/t/no-option-select-head-no-yes b/t/no-option-select-head-no-yes
new file mode 100755
index 0000000..1496892
--- /dev/null
+++ b/t/no-option-select-head-no-yes
@@ -0,0 +1,18 @@
+#!/bin/sh -e
+. t/common.sh
+
+printf '%s\n' > file \
+ "${HEAD} ours" \
+ ours \
+ "${PARENT} base" \
+ base \
+ "${TAIL}" \
+ theirs \
+ "${END} theirs"
+
+printf '%s\n' > file.expected \
+ ours
+
+normal --no-select-head --select-head --no-reduce file
+
+also_interactive h
diff --git a/t/no-option-select-head-yes-no b/t/no-option-select-head-yes-no
new file mode 100755
index 0000000..3093de5
--- /dev/null
+++ b/t/no-option-select-head-yes-no
@@ -0,0 +1,17 @@
+#!/bin/sh -e
+. t/common.sh
+
+printf '%s\n' > file \
+ "${HEAD} ours" \
+ ours \
+ "${PARENT} base" \
+ base \
+ "${TAIL}" \
+ theirs \
+ "${END} theirs"
+
+cp -- file file.expected
+
+conflict --select-head --no-select-head --no-reduce file
+
+also_interactive s
diff --git a/t/no-option-select-head-yes-then-select-tail-yes-no b/t/no-option-select-head-yes-then-select-tail-yes-no
new file mode 100755
index 0000000..7bc5924
--- /dev/null
+++ b/t/no-option-select-head-yes-then-select-tail-yes-no
@@ -0,0 +1,18 @@
+#!/bin/sh -e
+. t/common.sh
+
+printf '%s\n' > file \
+ "${HEAD} ours" \
+ ours \
+ "${PARENT} base" \
+ base \
+ "${TAIL}" \
+ theirs \
+ "${END} theirs"
+
+printf '%s\n' > file.expected \
+ ours
+
+normal --select-head --select-tail --no-select-tail --no-reduce file
+
+also_interactive h
diff --git a/t/no-option-select-tail b/t/no-option-select-tail
new file mode 100755
index 0000000..8c30d7e
--- /dev/null
+++ b/t/no-option-select-tail
@@ -0,0 +1,28 @@
+#!/bin/sh -e
+. t/common.sh
+
+printf '%s\n' > file \
+ "${HEAD} ours" \
+ same \
+ ours \
+ "${PARENT} base" \
+ same \
+ base \
+ "${TAIL}" \
+ same \
+ theirs \
+ "${END} theirs"
+
+printf '%s\n' > file.expected \
+ same \
+ "${HEAD} ours" \
+ ours \
+ "${PARENT} base" \
+ base \
+ "${TAIL}" \
+ theirs \
+ "${END} theirs"
+
+conflict --no-select-tail file
+
+also_interactive rr
diff --git a/t/no-option-select-tail-no-yes b/t/no-option-select-tail-no-yes
new file mode 100755
index 0000000..9984272
--- /dev/null
+++ b/t/no-option-select-tail-no-yes
@@ -0,0 +1,18 @@
+#!/bin/sh -e
+. t/common.sh
+
+printf '%s\n' > file \
+ "${HEAD} ours" \
+ ours \
+ "${PARENT} base" \
+ base \
+ "${TAIL}" \
+ theirs \
+ "${END} theirs"
+
+printf '%s\n' > file.expected \
+ theirs
+
+normal --no-select-tail --select-tail --no-reduce file
+
+also_interactive t
diff --git a/t/no-option-select-tail-yes-no b/t/no-option-select-tail-yes-no
new file mode 100755
index 0000000..b2c3cc9
--- /dev/null
+++ b/t/no-option-select-tail-yes-no
@@ -0,0 +1,17 @@
+#!/bin/sh -e
+. t/common.sh
+
+printf '%s\n' > file \
+ "${HEAD} ours" \
+ ours \
+ "${PARENT} base" \
+ base \
+ "${TAIL}" \
+ theirs \
+ "${END} theirs"
+
+cp -- file file.expected
+
+conflict --select-tail --no-select-tail --no-reduce file
+
+also_interactive s
diff --git a/t/no-option-select-tail-yes-then-select-head-yes-no b/t/no-option-select-tail-yes-then-select-head-yes-no
new file mode 100755
index 0000000..a46efa4
--- /dev/null
+++ b/t/no-option-select-tail-yes-then-select-head-yes-no
@@ -0,0 +1,18 @@
+#!/bin/sh -e
+. t/common.sh
+
+printf '%s\n' > file \
+ "${HEAD} ours" \
+ ours \
+ "${PARENT} base" \
+ base \
+ "${TAIL}" \
+ theirs \
+ "${END} theirs"
+
+printf '%s\n' > file.expected \
+ theirs
+
+normal --select-tail --select-head --no-select-head --no-reduce file
+
+also_interactive t
diff --git a/t/no-reduce b/t/no-reduce
new file mode 100755
index 0000000..878eeea
--- /dev/null
+++ b/t/no-reduce
@@ -0,0 +1,17 @@
+#!/bin/sh -e
+. t/common.sh
+
+printf '%s\n' > file \
+ "${HEAD} HEAD" \
+ same \
+ head \
+ "${TAIL}" \
+ same \
+ tail \
+ "${END} branch"
+
+cp -- file file.expected
+
+conflict --no-reduce file
+
+also_interactive s
diff --git a/t/no-reduce-1 b/t/no-reduce-1
new file mode 100755
index 0000000..8f549eb
--- /dev/null
+++ b/t/no-reduce-1
@@ -0,0 +1,11 @@
+#!/bin/sh -e
+. t/common.sh
+
+printf '%s\n' > file \
+ "${HEAD} 1" \
+ same \
+ "${END} 1"
+
+cp -- file file.expected
+
+error --no-reduce file
diff --git a/t/no-reduce-2-files b/t/no-reduce-2-files
new file mode 100755
index 0000000..29d7ea3
--- /dev/null
+++ b/t/no-reduce-2-files
@@ -0,0 +1,24 @@
+#!/bin/sh -e
+. t/common.sh
+
+printf '%s\n' > file \
+ "${HEAD} ours" \
+ first \
+ "${TAIL}" \
+ first \
+ "${END} theirs"
+
+cp -- file file.expected
+
+printf '%s\n' > file2 \
+ "${HEAD} ours" \
+ second \
+ "${TAIL}" \
+ second \
+ "${END} theirs"
+
+cp -- file2 file2.expected
+
+conflict --no-reduce file file2
+
+also_interactive ss
diff --git a/t/no-reduce-2-full b/t/no-reduce-2-full
new file mode 100755
index 0000000..20b4c19
--- /dev/null
+++ b/t/no-reduce-2-full
@@ -0,0 +1,19 @@
+#!/bin/sh -e
+. t/common.sh
+
+printf '%s\n' > file \
+ "${HEAD} 1" \
+ common-first \
+ common-second \
+ common-third \
+ "${TAIL} 2" \
+ common-first \
+ common-second \
+ common-third \
+ "${END} 2"
+
+cp -- file file.expected
+
+conflict --no-reduce file
+
+also_interactive s
diff --git a/t/no-reduce-2-head b/t/no-reduce-2-head
new file mode 100755
index 0000000..34029cd
--- /dev/null
+++ b/t/no-reduce-2-head
@@ -0,0 +1,21 @@
+#!/bin/sh -e
+. t/common.sh
+
+printf '%s\n' > file \
+ "${HEAD} 1" \
+ head-common-first \
+ head-common-second \
+ branch-1-first \
+ branch-1-second \
+ "${TAIL} 2" \
+ head-common-first \
+ head-common-second \
+ branch-2-first \
+ branch-2-second \
+ "${END} 2"
+
+cp -- file file.expected
+
+conflict --no-reduce file
+
+also_interactive s
diff --git a/t/no-reduce-2-hunks b/t/no-reduce-2-hunks
new file mode 100755
index 0000000..bc9c550
--- /dev/null
+++ b/t/no-reduce-2-hunks
@@ -0,0 +1,21 @@
+#!/bin/sh -e
+. t/common.sh
+
+printf '%s\n' > file \
+ "${HEAD} ours" \
+ first \
+ "${TAIL}" \
+ first \
+ "${END} theirs" \
+ between \
+ "${HEAD} ours" \
+ second \
+ "${TAIL}" \
+ second \
+ "${END} theirs"
+
+cp -- file file.expected
+
+conflict --no-reduce file
+
+also_interactive ss
diff --git a/t/no-reduce-2-middle b/t/no-reduce-2-middle
new file mode 100755
index 0000000..5409cbd
--- /dev/null
+++ b/t/no-reduce-2-middle
@@ -0,0 +1,25 @@
+#!/bin/sh -e
+. t/common.sh
+
+printf '%s\n' > file \
+ "${HEAD} 1" \
+ branch-1-before-first \
+ branch-1-before-second \
+ middle-common-first \
+ middle-common-second \
+ branch-1-after-first \
+ branch-1-after-second \
+ "${TAIL} 2" \
+ branch-2-before-first \
+ branch-2-before-second \
+ middle-common-first \
+ middle-common-second \
+ branch-2-after-first \
+ branch-2-after-second \
+ "${END} 2"
+
+cp -- file file.expected
+
+conflict --no-reduce file
+
+also_interactive s
diff --git a/t/no-reduce-2-multiple-partial b/t/no-reduce-2-multiple-partial
new file mode 100755
index 0000000..bf25762
--- /dev/null
+++ b/t/no-reduce-2-multiple-partial
@@ -0,0 +1,33 @@
+#!/bin/sh -e
+. t/common.sh
+
+printf '%s\n' > file \
+ "${HEAD} 1" \
+ head-common-first \
+ head-common-second \
+ branch-1-first-a \
+ branch-1-first-b \
+ middle-common-first \
+ middle-common-second \
+ branch-1-second-a \
+ branch-1-second-b \
+ tail-common-first \
+ tail-common-second \
+ "${TAIL} 2" \
+ head-common-first \
+ head-common-second \
+ branch-2-first-a \
+ branch-2-first-b \
+ middle-common-first \
+ middle-common-second \
+ branch-2-second-a \
+ branch-2-second-b \
+ tail-common-first \
+ tail-common-second \
+ "${END} 2"
+
+cp -- file file.expected
+
+conflict --no-reduce file
+
+also_interactive s
diff --git a/t/no-reduce-2-non-aligned b/t/no-reduce-2-non-aligned
new file mode 100755
index 0000000..542435f
--- /dev/null
+++ b/t/no-reduce-2-non-aligned
@@ -0,0 +1,27 @@
+#!/bin/sh -e
+. t/common.sh
+
+printf '%s\n' > file \
+ "${HEAD} 1" \
+ before-common-first \
+ before-common-second \
+ head-only-first \
+ head-only-second \
+ middle-common-first \
+ middle-common-second \
+ head-last \
+ "${TAIL} 2" \
+ before-common-first \
+ before-common-second \
+ tail-only \
+ middle-common-first \
+ middle-common-second \
+ tail-last-first \
+ tail-last-second \
+ "${END} 2"
+
+cp -- file file.expected
+
+conflict --no-reduce file
+
+also_interactive s
diff --git a/t/no-reduce-2-none b/t/no-reduce-2-none
new file mode 100755
index 0000000..d09b857
--- /dev/null
+++ b/t/no-reduce-2-none
@@ -0,0 +1,17 @@
+#!/bin/sh -e
+. t/common.sh
+
+printf '%s\n' > file \
+ "${HEAD} 1" \
+ branch-1-first \
+ branch-1-second \
+ "${TAIL} 2" \
+ branch-2-first \
+ branch-2-second \
+ "${END} 2"
+
+cp -- file file.expected
+
+conflict --no-reduce file
+
+also_interactive s
diff --git a/t/no-reduce-2-tail b/t/no-reduce-2-tail
new file mode 100755
index 0000000..4eb8bd8
--- /dev/null
+++ b/t/no-reduce-2-tail
@@ -0,0 +1,21 @@
+#!/bin/sh -e
+. t/common.sh
+
+printf '%s\n' > file \
+ "${HEAD} 1" \
+ branch-1-first \
+ branch-1-second \
+ tail-common-first \
+ tail-common-second \
+ "${TAIL} 2" \
+ branch-2-first \
+ branch-2-second \
+ tail-common-first \
+ tail-common-second \
+ "${END} 2"
+
+cp -- file file.expected
+
+conflict --no-reduce file
+
+also_interactive s
diff --git a/t/no-reduce-3-full b/t/no-reduce-3-full
new file mode 100755
index 0000000..8e36d3e
--- /dev/null
+++ b/t/no-reduce-3-full
@@ -0,0 +1,23 @@
+#!/bin/sh -e
+. t/common.sh
+
+printf '%s\n' > file \
+ "${HEAD} 1" \
+ common-first \
+ common-second \
+ common-third \
+ "${PARENT} 2" \
+ common-first \
+ common-second \
+ common-third \
+ "${TAIL} 3" \
+ common-first \
+ common-second \
+ common-third \
+ "${END} 3"
+
+cp -- file file.expected
+
+conflict --no-reduce file
+
+also_interactive s
diff --git a/t/no-reduce-3-head b/t/no-reduce-3-head
new file mode 100755
index 0000000..3d2475c
--- /dev/null
+++ b/t/no-reduce-3-head
@@ -0,0 +1,26 @@
+#!/bin/sh -e
+. t/common.sh
+
+printf '%s\n' > file \
+ "${HEAD} 1" \
+ head-common-first \
+ head-common-second \
+ branch-1-first \
+ branch-1-second \
+ "${PARENT} 2" \
+ head-common-first \
+ head-common-second \
+ branch-2-first \
+ branch-2-second \
+ "${TAIL} 3" \
+ head-common-first \
+ head-common-second \
+ branch-3-first \
+ branch-3-second \
+ "${END} 3"
+
+cp -- file file.expected
+
+conflict --no-reduce file
+
+also_interactive s
diff --git a/t/no-reduce-3-middle b/t/no-reduce-3-middle
new file mode 100755
index 0000000..da3eb25
--- /dev/null
+++ b/t/no-reduce-3-middle
@@ -0,0 +1,32 @@
+#!/bin/sh -e
+. t/common.sh
+
+printf '%s\n' > file \
+ "${HEAD} 1" \
+ branch-1-before-first \
+ branch-1-before-second \
+ middle-common-first \
+ middle-common-second \
+ branch-1-after-first \
+ branch-1-after-second \
+ "${PARENT} 2" \
+ branch-2-before-first \
+ branch-2-before-second \
+ middle-common-first \
+ middle-common-second \
+ branch-2-after-first \
+ branch-2-after-second \
+ "${TAIL} 3" \
+ branch-3-before-first \
+ branch-3-before-second \
+ middle-common-first \
+ middle-common-second \
+ branch-3-after-first \
+ branch-3-after-second \
+ "${END} 3"
+
+cp -- file file.expected
+
+conflict --no-reduce file
+
+also_interactive s
diff --git a/t/no-reduce-3-multiple-partial b/t/no-reduce-3-multiple-partial
new file mode 100755
index 0000000..232afd0
--- /dev/null
+++ b/t/no-reduce-3-multiple-partial
@@ -0,0 +1,44 @@
+#!/bin/sh -e
+. t/common.sh
+
+printf '%s\n' > file \
+ "${HEAD} 1" \
+ head-common-first \
+ head-common-second \
+ branch-1-first-a \
+ branch-1-first-b \
+ middle-common-first \
+ middle-common-second \
+ branch-1-second-a \
+ branch-1-second-b \
+ tail-common-first \
+ tail-common-second \
+ "${PARENT} 2" \
+ head-common-first \
+ head-common-second \
+ branch-2-first-a \
+ branch-2-first-b \
+ middle-common-first \
+ middle-common-second \
+ branch-2-second-a \
+ branch-2-second-b \
+ tail-common-first \
+ tail-common-second \
+ "${TAIL} 3" \
+ head-common-first \
+ head-common-second \
+ branch-3-first-a \
+ branch-3-first-b \
+ middle-common-first \
+ middle-common-second \
+ branch-3-second-a \
+ branch-3-second-b \
+ tail-common-first \
+ tail-common-second \
+ "${END} 3"
+
+cp -- file file.expected
+
+conflict --no-reduce file
+
+also_interactive s
diff --git a/t/no-reduce-3-none b/t/no-reduce-3-none
new file mode 100755
index 0000000..c5c05bf
--- /dev/null
+++ b/t/no-reduce-3-none
@@ -0,0 +1,20 @@
+#!/bin/sh -e
+. t/common.sh
+
+printf '%s\n' > file \
+ "${HEAD} 1" \
+ branch-1-first \
+ branch-1-second \
+ "${PARENT} 2" \
+ branch-2-first \
+ branch-2-second \
+ "${TAIL} 3" \
+ branch-3-first \
+ branch-3-second \
+ "${END} 3"
+
+cp -- file file.expected
+
+conflict --no-reduce file
+
+also_interactive s
diff --git a/t/no-reduce-3-tail b/t/no-reduce-3-tail
new file mode 100755
index 0000000..776eebd
--- /dev/null
+++ b/t/no-reduce-3-tail
@@ -0,0 +1,26 @@
+#!/bin/sh -e
+. t/common.sh
+
+printf '%s\n' > file \
+ "${HEAD} 1" \
+ branch-1-first \
+ branch-1-second \
+ tail-common-first \
+ tail-common-second \
+ "${PARENT} 2" \
+ branch-2-first \
+ branch-2-second \
+ tail-common-first \
+ tail-common-second \
+ "${TAIL} 3" \
+ branch-3-first \
+ branch-3-second \
+ tail-common-first \
+ tail-common-second \
+ "${END} 3"
+
+cp -- file file.expected
+
+conflict --no-reduce file
+
+also_interactive s
diff --git a/t/no-reduce-4-full b/t/no-reduce-4-full
new file mode 100755
index 0000000..49f47c2
--- /dev/null
+++ b/t/no-reduce-4-full
@@ -0,0 +1,27 @@
+#!/bin/sh -e
+. t/common.sh
+
+printf '%s\n' > file \
+ "${HEAD} 1" \
+ common-first \
+ common-second \
+ common-third \
+ "${PARENT} 2" \
+ common-first \
+ common-second \
+ common-third \
+ "${PARENT} 3" \
+ common-first \
+ common-second \
+ common-third \
+ "${TAIL} 4" \
+ common-first \
+ common-second \
+ common-third \
+ "${END} 4"
+
+cp -- file file.expected
+
+conflict --no-reduce file
+
+also_interactive s
diff --git a/t/no-reduce-4-head b/t/no-reduce-4-head
new file mode 100755
index 0000000..68425c6
--- /dev/null
+++ b/t/no-reduce-4-head
@@ -0,0 +1,31 @@
+#!/bin/sh -e
+. t/common.sh
+
+printf '%s\n' > file \
+ "${HEAD} 1" \
+ head-common-first \
+ head-common-second \
+ branch-1-first \
+ branch-1-second \
+ "${PARENT} 2" \
+ head-common-first \
+ head-common-second \
+ branch-2-first \
+ branch-2-second \
+ "${PARENT} 3" \
+ head-common-first \
+ head-common-second \
+ branch-3-first \
+ branch-3-second \
+ "${TAIL} 4" \
+ head-common-first \
+ head-common-second \
+ branch-4-first \
+ branch-4-second \
+ "${END} 4"
+
+cp -- file file.expected
+
+conflict --no-reduce file
+
+also_interactive s
diff --git a/t/no-reduce-4-middle b/t/no-reduce-4-middle
new file mode 100755
index 0000000..b91f36e
--- /dev/null
+++ b/t/no-reduce-4-middle
@@ -0,0 +1,39 @@
+#!/bin/sh -e
+. t/common.sh
+
+printf '%s\n' > file \
+ "${HEAD} 1" \
+ branch-1-before-first \
+ branch-1-before-second \
+ middle-common-first \
+ middle-common-second \
+ branch-1-after-first \
+ branch-1-after-second \
+ "${PARENT} 2" \
+ branch-2-before-first \
+ branch-2-before-second \
+ middle-common-first \
+ middle-common-second \
+ branch-2-after-first \
+ branch-2-after-second \
+ "${PARENT} 3" \
+ branch-3-before-first \
+ branch-3-before-second \
+ middle-common-first \
+ middle-common-second \
+ branch-3-after-first \
+ branch-3-after-second \
+ "${TAIL} 4" \
+ branch-4-before-first \
+ branch-4-before-second \
+ middle-common-first \
+ middle-common-second \
+ branch-4-after-first \
+ branch-4-after-second \
+ "${END} 4"
+
+cp -- file file.expected
+
+conflict --no-reduce file
+
+also_interactive s
diff --git a/t/no-reduce-4-multiple-partial b/t/no-reduce-4-multiple-partial
new file mode 100755
index 0000000..3ae2034
--- /dev/null
+++ b/t/no-reduce-4-multiple-partial
@@ -0,0 +1,55 @@
+#!/bin/sh -e
+. t/common.sh
+
+printf '%s\n' > file \
+ "${HEAD} 1" \
+ head-common-first \
+ head-common-second \
+ branch-1-first-a \
+ branch-1-first-b \
+ middle-common-first \
+ middle-common-second \
+ branch-1-second-a \
+ branch-1-second-b \
+ tail-common-first \
+ tail-common-second \
+ "${PARENT} 2" \
+ head-common-first \
+ head-common-second \
+ branch-2-first-a \
+ branch-2-first-b \
+ middle-common-first \
+ middle-common-second \
+ branch-2-second-a \
+ branch-2-second-b \
+ tail-common-first \
+ tail-common-second \
+ "${PARENT} 3" \
+ head-common-first \
+ head-common-second \
+ branch-3-first-a \
+ branch-3-first-b \
+ middle-common-first \
+ middle-common-second \
+ branch-3-second-a \
+ branch-3-second-b \
+ tail-common-first \
+ tail-common-second \
+ "${TAIL} 4" \
+ head-common-first \
+ head-common-second \
+ branch-4-first-a \
+ branch-4-first-b \
+ middle-common-first \
+ middle-common-second \
+ branch-4-second-a \
+ branch-4-second-b \
+ tail-common-first \
+ tail-common-second \
+ "${END} 4"
+
+cp -- file file.expected
+
+conflict --no-reduce file
+
+also_interactive s
diff --git a/t/no-reduce-4-none b/t/no-reduce-4-none
new file mode 100755
index 0000000..cdb5a0f
--- /dev/null
+++ b/t/no-reduce-4-none
@@ -0,0 +1,23 @@
+#!/bin/sh -e
+. t/common.sh
+
+printf '%s\n' > file \
+ "${HEAD} 1" \
+ branch-1-first \
+ branch-1-second \
+ "${PARENT} 2" \
+ branch-2-first \
+ branch-2-second \
+ "${PARENT} 3" \
+ branch-3-first \
+ branch-3-second \
+ "${TAIL} 4" \
+ branch-4-first \
+ branch-4-second \
+ "${END} 4"
+
+cp -- file file.expected
+
+conflict --no-reduce file
+
+also_interactive s
diff --git a/t/no-reduce-4-tail b/t/no-reduce-4-tail
new file mode 100755
index 0000000..a9edec7
--- /dev/null
+++ b/t/no-reduce-4-tail
@@ -0,0 +1,31 @@
+#!/bin/sh -e
+. t/common.sh
+
+printf '%s\n' > file \
+ "${HEAD} 1" \
+ branch-1-first \
+ branch-1-second \
+ tail-common-first \
+ tail-common-second \
+ "${PARENT} 2" \
+ branch-2-first \
+ branch-2-second \
+ tail-common-first \
+ tail-common-second \
+ "${PARENT} 3" \
+ branch-3-first \
+ branch-3-second \
+ tail-common-first \
+ tail-common-second \
+ "${TAIL} 4" \
+ branch-4-first \
+ branch-4-second \
+ tail-common-first \
+ tail-common-second \
+ "${END} 4"
+
+cp -- file file.expected
+
+conflict --no-reduce file
+
+also_interactive s
diff --git a/t/no-reduce-5-full b/t/no-reduce-5-full
new file mode 100755
index 0000000..d735484
--- /dev/null
+++ b/t/no-reduce-5-full
@@ -0,0 +1,31 @@
+#!/bin/sh -e
+. t/common.sh
+
+printf '%s\n' > file \
+ "${HEAD} 1" \
+ common-first \
+ common-second \
+ common-third \
+ "${PARENT} 2" \
+ common-first \
+ common-second \
+ common-third \
+ "${PARENT} 3" \
+ common-first \
+ common-second \
+ common-third \
+ "${PARENT} 4" \
+ common-first \
+ common-second \
+ common-third \
+ "${TAIL} 5" \
+ common-first \
+ common-second \
+ common-third \
+ "${END} 5"
+
+cp -- file file.expected
+
+conflict --no-reduce file
+
+also_interactive s
diff --git a/t/no-reduce-5-head b/t/no-reduce-5-head
new file mode 100755
index 0000000..883240c
--- /dev/null
+++ b/t/no-reduce-5-head
@@ -0,0 +1,36 @@
+#!/bin/sh -e
+. t/common.sh
+
+printf '%s\n' > file \
+ "${HEAD} 1" \
+ head-common-first \
+ head-common-second \
+ branch-1-first \
+ branch-1-second \
+ "${PARENT} 2" \
+ head-common-first \
+ head-common-second \
+ branch-2-first \
+ branch-2-second \
+ "${PARENT} 3" \
+ head-common-first \
+ head-common-second \
+ branch-3-first \
+ branch-3-second \
+ "${PARENT} 4" \
+ head-common-first \
+ head-common-second \
+ branch-4-first \
+ branch-4-second \
+ "${TAIL} 5" \
+ head-common-first \
+ head-common-second \
+ branch-5-first \
+ branch-5-second \
+ "${END} 5"
+
+cp -- file file.expected
+
+conflict --no-reduce file
+
+also_interactive s
diff --git a/t/no-reduce-5-middle b/t/no-reduce-5-middle
new file mode 100755
index 0000000..3826bc6
--- /dev/null
+++ b/t/no-reduce-5-middle
@@ -0,0 +1,46 @@
+#!/bin/sh -e
+. t/common.sh
+
+printf '%s\n' > file \
+ "${HEAD} 1" \
+ branch-1-before-first \
+ branch-1-before-second \
+ middle-common-first \
+ middle-common-second \
+ branch-1-after-first \
+ branch-1-after-second \
+ "${PARENT} 2" \
+ branch-2-before-first \
+ branch-2-before-second \
+ middle-common-first \
+ middle-common-second \
+ branch-2-after-first \
+ branch-2-after-second \
+ "${PARENT} 3" \
+ branch-3-before-first \
+ branch-3-before-second \
+ middle-common-first \
+ middle-common-second \
+ branch-3-after-first \
+ branch-3-after-second \
+ "${PARENT} 4" \
+ branch-4-before-first \
+ branch-4-before-second \
+ middle-common-first \
+ middle-common-second \
+ branch-4-after-first \
+ branch-4-after-second \
+ "${TAIL} 5" \
+ branch-5-before-first \
+ branch-5-before-second \
+ middle-common-first \
+ middle-common-second \
+ branch-5-after-first \
+ branch-5-after-second \
+ "${END} 5"
+
+cp -- file file.expected
+
+conflict --no-reduce file
+
+also_interactive s
diff --git a/t/no-reduce-5-multiple-partial b/t/no-reduce-5-multiple-partial
new file mode 100755
index 0000000..802b414
--- /dev/null
+++ b/t/no-reduce-5-multiple-partial
@@ -0,0 +1,66 @@
+#!/bin/sh -e
+. t/common.sh
+
+printf '%s\n' > file \
+ "${HEAD} 1" \
+ head-common-first \
+ head-common-second \
+ branch-1-first-a \
+ branch-1-first-b \
+ middle-common-first \
+ middle-common-second \
+ branch-1-second-a \
+ branch-1-second-b \
+ tail-common-first \
+ tail-common-second \
+ "${PARENT} 2" \
+ head-common-first \
+ head-common-second \
+ branch-2-first-a \
+ branch-2-first-b \
+ middle-common-first \
+ middle-common-second \
+ branch-2-second-a \
+ branch-2-second-b \
+ tail-common-first \
+ tail-common-second \
+ "${PARENT} 3" \
+ head-common-first \
+ head-common-second \
+ branch-3-first-a \
+ branch-3-first-b \
+ middle-common-first \
+ middle-common-second \
+ branch-3-second-a \
+ branch-3-second-b \
+ tail-common-first \
+ tail-common-second \
+ "${PARENT} 4" \
+ head-common-first \
+ head-common-second \
+ branch-4-first-a \
+ branch-4-first-b \
+ middle-common-first \
+ middle-common-second \
+ branch-4-second-a \
+ branch-4-second-b \
+ tail-common-first \
+ tail-common-second \
+ "${TAIL} 5" \
+ head-common-first \
+ head-common-second \
+ branch-5-first-a \
+ branch-5-first-b \
+ middle-common-first \
+ middle-common-second \
+ branch-5-second-a \
+ branch-5-second-b \
+ tail-common-first \
+ tail-common-second \
+ "${END} 5"
+
+cp -- file file.expected
+
+conflict --no-reduce file
+
+also_interactive s
diff --git a/t/no-reduce-5-none b/t/no-reduce-5-none
new file mode 100755
index 0000000..fbd2808
--- /dev/null
+++ b/t/no-reduce-5-none
@@ -0,0 +1,26 @@
+#!/bin/sh -e
+. t/common.sh
+
+printf '%s\n' > file \
+ "${HEAD} 1" \
+ branch-1-first \
+ branch-1-second \
+ "${PARENT} 2" \
+ branch-2-first \
+ branch-2-second \
+ "${PARENT} 3" \
+ branch-3-first \
+ branch-3-second \
+ "${PARENT} 4" \
+ branch-4-first \
+ branch-4-second \
+ "${TAIL} 5" \
+ branch-5-first \
+ branch-5-second \
+ "${END} 5"
+
+cp -- file file.expected
+
+conflict --no-reduce file
+
+also_interactive s
diff --git a/t/no-reduce-5-tail b/t/no-reduce-5-tail
new file mode 100755
index 0000000..8799582
--- /dev/null
+++ b/t/no-reduce-5-tail
@@ -0,0 +1,36 @@
+#!/bin/sh -e
+. t/common.sh
+
+printf '%s\n' > file \
+ "${HEAD} 1" \
+ branch-1-first \
+ branch-1-second \
+ tail-common-first \
+ tail-common-second \
+ "${PARENT} 2" \
+ branch-2-first \
+ branch-2-second \
+ tail-common-first \
+ tail-common-second \
+ "${PARENT} 3" \
+ branch-3-first \
+ branch-3-second \
+ tail-common-first \
+ tail-common-second \
+ "${PARENT} 4" \
+ branch-4-first \
+ branch-4-second \
+ tail-common-first \
+ tail-common-second \
+ "${TAIL} 5" \
+ branch-5-first \
+ branch-5-second \
+ tail-common-first \
+ tail-common-second \
+ "${END} 5"
+
+cp -- file file.expected
+
+conflict --no-reduce file
+
+also_interactive s
diff --git a/t/no-reduce-dashdash-dashprefix b/t/no-reduce-dashdash-dashprefix
new file mode 100755
index 0000000..5ddaaed
--- /dev/null
+++ b/t/no-reduce-dashdash-dashprefix
@@ -0,0 +1,17 @@
+#!/bin/sh -e
+. t/common.sh
+
+printf '%s\n' > -file \
+ "${HEAD} HEAD" \
+ same \
+ head \
+ "${TAIL}" \
+ same \
+ tail \
+ "${END} branch"
+
+cp -- -file -file.expected
+
+conflict --no-reduce -- -file
+
+also_interactive s
diff --git a/t/no-reduce-empty-base b/t/no-reduce-empty-base
new file mode 100755
index 0000000..2cc942f
--- /dev/null
+++ b/t/no-reduce-empty-base
@@ -0,0 +1,16 @@
+#!/bin/sh -e
+. t/common.sh
+
+printf '%s\n' > file \
+ "${HEAD} ours" \
+ common \
+ "${PARENT} base" \
+ "${TAIL}" \
+ common \
+ "${END} theirs"
+
+cp -- file file.expected
+
+conflict --no-reduce file
+
+also_interactive s
diff --git a/t/no-reduce-empty-head b/t/no-reduce-empty-head
new file mode 100755
index 0000000..95c0338
--- /dev/null
+++ b/t/no-reduce-empty-head
@@ -0,0 +1,16 @@
+#!/bin/sh -e
+. t/common.sh
+
+printf '%s\n' > file \
+ "${HEAD} ours" \
+ "${PARENT} base" \
+ common \
+ "${TAIL}" \
+ common \
+ "${END} theirs"
+
+cp -- file file.expected
+
+conflict --no-reduce file
+
+also_interactive s
diff --git a/t/no-reduce-empty-tail b/t/no-reduce-empty-tail
new file mode 100755
index 0000000..b8888ae
--- /dev/null
+++ b/t/no-reduce-empty-tail
@@ -0,0 +1,16 @@
+#!/bin/sh -e
+. t/common.sh
+
+printf '%s\n' > file \
+ "${HEAD} ours" \
+ common \
+ "${PARENT} base" \
+ common \
+ "${TAIL}" \
+ "${END} theirs"
+
+cp -- file file.expected
+
+conflict --no-reduce file
+
+also_interactive s
diff --git a/t/no-reduce-no-conflicts b/t/no-reduce-no-conflicts
new file mode 100755
index 0000000..ac818e6
--- /dev/null
+++ b/t/no-reduce-no-conflicts
@@ -0,0 +1,11 @@
+#!/bin/sh -e
+. t/common.sh
+
+printf '%s\n' > file \
+ no conflicts
+
+cp -- file file.expected
+
+normal --no-reduce file
+
+also_interactive ''
diff --git a/t/null-bytes b/t/null-bytes
new file mode 100755
index 0000000..9a7ee7f
--- /dev/null
+++ b/t/null-bytes
@@ -0,0 +1,15 @@
+#!/bin/sh -e
+. t/common.sh
+
+(
+ printf '%s\n' "${HEAD} ours"
+ printf 'same\000text\n'
+ printf '%s\n' "${TAIL}"
+ printf 'same\000text\n'
+ printf '%s\n' "${END} theirs"
+) > file
+
+printf 'same\000text\n' > file.expected
+
+"$command" --reduce file
+cmp -s -- file file.expected
diff --git a/t/reduce-1 b/t/reduce-1
new file mode 100755
index 0000000..4f96968
--- /dev/null
+++ b/t/reduce-1
@@ -0,0 +1,9 @@
+#!/bin/sh -e
+. t/common.sh
+
+printf '%s\n' > file \
+ "${HEAD} 1" \
+ same \
+ "${END} 1"
+
+error --reduce file
diff --git a/t/reduce-2-files b/t/reduce-2-files
new file mode 100755
index 0000000..d28e142
--- /dev/null
+++ b/t/reduce-2-files
@@ -0,0 +1,26 @@
+#!/bin/sh -e
+. t/common.sh
+
+printf '%s\n' > file \
+ "${HEAD} ours" \
+ first \
+ "${TAIL}" \
+ first \
+ "${END} theirs"
+
+printf '%s\n' > file.expected \
+ first
+
+printf '%s\n' > file2 \
+ "${HEAD} ours" \
+ second \
+ "${TAIL}" \
+ second \
+ "${END} theirs"
+
+printf '%s\n' > file2.expected \
+ second
+
+normal --reduce file file2
+
+also_interactive rr
diff --git a/t/reduce-2-full b/t/reduce-2-full
new file mode 100755
index 0000000..59a1b5e
--- /dev/null
+++ b/t/reduce-2-full
@@ -0,0 +1,22 @@
+#!/bin/sh -e
+. t/common.sh
+
+printf '%s\n' > file \
+ "${HEAD} 1" \
+ common-first \
+ common-second \
+ common-third \
+ "${TAIL} 2" \
+ common-first \
+ common-second \
+ common-third \
+ "${END} 2"
+
+printf '%s\n' > file.expected \
+ common-first \
+ common-second \
+ common-third
+
+normal --reduce file
+
+also_interactive r
diff --git a/t/reduce-2-head b/t/reduce-2-head
new file mode 100755
index 0000000..d4bcada
--- /dev/null
+++ b/t/reduce-2-head
@@ -0,0 +1,30 @@
+#!/bin/sh -e
+. t/common.sh
+
+printf '%s\n' > file \
+ "${HEAD} 1" \
+ head-common-first \
+ head-common-second \
+ branch-1-first \
+ branch-1-second \
+ "${TAIL} 2" \
+ head-common-first \
+ head-common-second \
+ branch-2-first \
+ branch-2-second \
+ "${END} 2"
+
+printf '%s\n' > file.expected \
+ head-common-first \
+ head-common-second \
+ "${HEAD} 1" \
+ branch-1-first \
+ branch-1-second \
+ "${TAIL} 2" \
+ branch-2-first \
+ branch-2-second \
+ "${END} 2"
+
+conflict --reduce file
+
+also_interactive rr
diff --git a/t/reduce-2-hunks b/t/reduce-2-hunks
new file mode 100755
index 0000000..22146e6
--- /dev/null
+++ b/t/reduce-2-hunks
@@ -0,0 +1,24 @@
+#!/bin/sh -e
+. t/common.sh
+
+printf '%s\n' > file \
+ "${HEAD} ours" \
+ first \
+ "${TAIL}" \
+ first \
+ "${END} theirs" \
+ between \
+ "${HEAD} ours" \
+ second \
+ "${TAIL}" \
+ second \
+ "${END} theirs"
+
+printf '%s\n' > file.expected \
+ first \
+ between \
+ second
+
+normal --reduce file
+
+also_interactive rr
diff --git a/t/reduce-2-middle b/t/reduce-2-middle
new file mode 100755
index 0000000..f2ab8d3
--- /dev/null
+++ b/t/reduce-2-middle
@@ -0,0 +1,41 @@
+#!/bin/sh -e
+. t/common.sh
+
+printf '%s\n' > file \
+ "${HEAD} 1" \
+ branch-1-before-first \
+ branch-1-before-second \
+ middle-common-first \
+ middle-common-second \
+ branch-1-after-first \
+ branch-1-after-second \
+ "${TAIL} 2" \
+ branch-2-before-first \
+ branch-2-before-second \
+ middle-common-first \
+ middle-common-second \
+ branch-2-after-first \
+ branch-2-after-second \
+ "${END} 2"
+
+printf '%s\n' > file.expected \
+ "${HEAD} 1" \
+ branch-1-before-first \
+ branch-1-before-second \
+ "${TAIL} 2" \
+ branch-2-before-first \
+ branch-2-before-second \
+ "${END} 2" \
+ middle-common-first \
+ middle-common-second \
+ "${HEAD} 1" \
+ branch-1-after-first \
+ branch-1-after-second \
+ "${TAIL} 2" \
+ branch-2-after-first \
+ branch-2-after-second \
+ "${END} 2"
+
+conflict --reduce file
+
+also_interactive rrr
diff --git a/t/reduce-2-multiple-partial b/t/reduce-2-multiple-partial
new file mode 100755
index 0000000..eaf4ba5
--- /dev/null
+++ b/t/reduce-2-multiple-partial
@@ -0,0 +1,53 @@
+#!/bin/sh -e
+. t/common.sh
+
+printf '%s\n' > file \
+ "${HEAD} 1" \
+ head-common-first \
+ head-common-second \
+ branch-1-first-a \
+ branch-1-first-b \
+ middle-common-first \
+ middle-common-second \
+ branch-1-second-a \
+ branch-1-second-b \
+ tail-common-first \
+ tail-common-second \
+ "${TAIL} 2" \
+ head-common-first \
+ head-common-second \
+ branch-2-first-a \
+ branch-2-first-b \
+ middle-common-first \
+ middle-common-second \
+ branch-2-second-a \
+ branch-2-second-b \
+ tail-common-first \
+ tail-common-second \
+ "${END} 2"
+
+printf '%s\n' > file.expected \
+ head-common-first \
+ head-common-second \
+ "${HEAD} 1" \
+ branch-1-first-a \
+ branch-1-first-b \
+ "${TAIL} 2" \
+ branch-2-first-a \
+ branch-2-first-b \
+ "${END} 2" \
+ middle-common-first \
+ middle-common-second \
+ "${HEAD} 1" \
+ branch-1-second-a \
+ branch-1-second-b \
+ "${TAIL} 2" \
+ branch-2-second-a \
+ branch-2-second-b \
+ "${END} 2" \
+ tail-common-first \
+ tail-common-second
+
+conflict --reduce file
+
+also_interactive rrr
diff --git a/t/reduce-2-non-aligned b/t/reduce-2-non-aligned
new file mode 100755
index 0000000..58202d1
--- /dev/null
+++ b/t/reduce-2-non-aligned
@@ -0,0 +1,43 @@
+#!/bin/sh -e
+. t/common.sh
+
+printf '%s\n' > file \
+ "${HEAD} 1" \
+ before-common-first \
+ before-common-second \
+ head-only-first \
+ head-only-second \
+ middle-common-first \
+ middle-common-second \
+ head-last \
+ "${TAIL} 2" \
+ before-common-first \
+ before-common-second \
+ tail-only \
+ middle-common-first \
+ middle-common-second \
+ tail-last-first \
+ tail-last-second \
+ "${END} 2"
+
+printf '%s\n' > file.expected \
+ before-common-first \
+ before-common-second \
+ "${HEAD} 1" \
+ head-only-first \
+ head-only-second \
+ "${TAIL} 2" \
+ tail-only \
+ "${END} 2" \
+ middle-common-first \
+ middle-common-second \
+ "${HEAD} 1" \
+ head-last \
+ "${TAIL} 2" \
+ tail-last-first \
+ tail-last-second \
+ "${END} 2"
+
+conflict --reduce file
+
+also_interactive rrr
diff --git a/t/reduce-2-none b/t/reduce-2-none
new file mode 100755
index 0000000..63efabc
--- /dev/null
+++ b/t/reduce-2-none
@@ -0,0 +1,24 @@
+#!/bin/sh -e
+. t/common.sh
+
+printf '%s\n' > file \
+ "${HEAD} 1" \
+ branch-1-first \
+ branch-1-second \
+ "${TAIL} 2" \
+ branch-2-first \
+ branch-2-second \
+ "${END} 2"
+
+printf '%s\n' > file.expected \
+ "${HEAD} 1" \
+ branch-1-first \
+ branch-1-second \
+ "${TAIL} 2" \
+ branch-2-first \
+ branch-2-second \
+ "${END} 2"
+
+conflict --reduce file
+
+also_interactive rr
diff --git a/t/reduce-2-tail b/t/reduce-2-tail
new file mode 100755
index 0000000..fb125f8
--- /dev/null
+++ b/t/reduce-2-tail
@@ -0,0 +1,30 @@
+#!/bin/sh -e
+. t/common.sh
+
+printf '%s\n' > file \
+ "${HEAD} 1" \
+ branch-1-first \
+ branch-1-second \
+ tail-common-first \
+ tail-common-second \
+ "${TAIL} 2" \
+ branch-2-first \
+ branch-2-second \
+ tail-common-first \
+ tail-common-second \
+ "${END} 2"
+
+printf '%s\n' > file.expected \
+ "${HEAD} 1" \
+ branch-1-first \
+ branch-1-second \
+ "${TAIL} 2" \
+ branch-2-first \
+ branch-2-second \
+ "${END} 2" \
+ tail-common-first \
+ tail-common-second
+
+conflict --reduce file
+
+also_interactive rr
diff --git a/t/reduce-3-full b/t/reduce-3-full
new file mode 100755
index 0000000..72cabeb
--- /dev/null
+++ b/t/reduce-3-full
@@ -0,0 +1,26 @@
+#!/bin/sh -e
+. t/common.sh
+
+printf '%s\n' > file \
+ "${HEAD} 1" \
+ common-first \
+ common-second \
+ common-third \
+ "${PARENT} 2" \
+ common-first \
+ common-second \
+ common-third \
+ "${TAIL} 3" \
+ common-first \
+ common-second \
+ common-third \
+ "${END} 3"
+
+printf '%s\n' > file.expected \
+ common-first \
+ common-second \
+ common-third
+
+normal --reduce file
+
+also_interactive r
diff --git a/t/reduce-3-head b/t/reduce-3-head
new file mode 100755
index 0000000..abc705c
--- /dev/null
+++ b/t/reduce-3-head
@@ -0,0 +1,38 @@
+#!/bin/sh -e
+. t/common.sh
+
+printf '%s\n' > file \
+ "${HEAD} 1" \
+ head-common-first \
+ head-common-second \
+ branch-1-first \
+ branch-1-second \
+ "${PARENT} 2" \
+ head-common-first \
+ head-common-second \
+ branch-2-first \
+ branch-2-second \
+ "${TAIL} 3" \
+ head-common-first \
+ head-common-second \
+ branch-3-first \
+ branch-3-second \
+ "${END} 3"
+
+printf '%s\n' > file.expected \
+ head-common-first \
+ head-common-second \
+ "${HEAD} 1" \
+ branch-1-first \
+ branch-1-second \
+ "${PARENT} 2" \
+ branch-2-first \
+ branch-2-second \
+ "${TAIL} 3" \
+ branch-3-first \
+ branch-3-second \
+ "${END} 3"
+
+conflict --reduce file
+
+also_interactive rr
diff --git a/t/reduce-3-middle b/t/reduce-3-middle
new file mode 100755
index 0000000..25caf2f
--- /dev/null
+++ b/t/reduce-3-middle
@@ -0,0 +1,54 @@
+#!/bin/sh -e
+. t/common.sh
+
+printf '%s\n' > file \
+ "${HEAD} 1" \
+ branch-1-before-first \
+ branch-1-before-second \
+ middle-common-first \
+ middle-common-second \
+ branch-1-after-first \
+ branch-1-after-second \
+ "${PARENT} 2" \
+ branch-2-before-first \
+ branch-2-before-second \
+ middle-common-first \
+ middle-common-second \
+ branch-2-after-first \
+ branch-2-after-second \
+ "${TAIL} 3" \
+ branch-3-before-first \
+ branch-3-before-second \
+ middle-common-first \
+ middle-common-second \
+ branch-3-after-first \
+ branch-3-after-second \
+ "${END} 3"
+
+printf '%s\n' > file.expected \
+ "${HEAD} 1" \
+ branch-1-before-first \
+ branch-1-before-second \
+ "${PARENT} 2" \
+ branch-2-before-first \
+ branch-2-before-second \
+ "${TAIL} 3" \
+ branch-3-before-first \
+ branch-3-before-second \
+ "${END} 3" \
+ middle-common-first \
+ middle-common-second \
+ "${HEAD} 1" \
+ branch-1-after-first \
+ branch-1-after-second \
+ "${PARENT} 2" \
+ branch-2-after-first \
+ branch-2-after-second \
+ "${TAIL} 3" \
+ branch-3-after-first \
+ branch-3-after-second \
+ "${END} 3"
+
+conflict --reduce file
+
+also_interactive rrr
diff --git a/t/reduce-3-multiple-partial b/t/reduce-3-multiple-partial
new file mode 100755
index 0000000..ec8cab7
--- /dev/null
+++ b/t/reduce-3-multiple-partial
@@ -0,0 +1,70 @@
+#!/bin/sh -e
+. t/common.sh
+
+printf '%s\n' > file \
+ "${HEAD} 1" \
+ head-common-first \
+ head-common-second \
+ branch-1-first-a \
+ branch-1-first-b \
+ middle-common-first \
+ middle-common-second \
+ branch-1-second-a \
+ branch-1-second-b \
+ tail-common-first \
+ tail-common-second \
+ "${PARENT} 2" \
+ head-common-first \
+ head-common-second \
+ branch-2-first-a \
+ branch-2-first-b \
+ middle-common-first \
+ middle-common-second \
+ branch-2-second-a \
+ branch-2-second-b \
+ tail-common-first \
+ tail-common-second \
+ "${TAIL} 3" \
+ head-common-first \
+ head-common-second \
+ branch-3-first-a \
+ branch-3-first-b \
+ middle-common-first \
+ middle-common-second \
+ branch-3-second-a \
+ branch-3-second-b \
+ tail-common-first \
+ tail-common-second \
+ "${END} 3"
+
+printf '%s\n' > file.expected \
+ head-common-first \
+ head-common-second \
+ "${HEAD} 1" \
+ branch-1-first-a \
+ branch-1-first-b \
+ "${PARENT} 2" \
+ branch-2-first-a \
+ branch-2-first-b \
+ "${TAIL} 3" \
+ branch-3-first-a \
+ branch-3-first-b \
+ "${END} 3" \
+ middle-common-first \
+ middle-common-second \
+ "${HEAD} 1" \
+ branch-1-second-a \
+ branch-1-second-b \
+ "${PARENT} 2" \
+ branch-2-second-a \
+ branch-2-second-b \
+ "${TAIL} 3" \
+ branch-3-second-a \
+ branch-3-second-b \
+ "${END} 3" \
+ tail-common-first \
+ tail-common-second
+
+conflict --reduce file
+
+also_interactive rrr
diff --git a/t/reduce-3-none b/t/reduce-3-none
new file mode 100755
index 0000000..9083c88
--- /dev/null
+++ b/t/reduce-3-none
@@ -0,0 +1,30 @@
+#!/bin/sh -e
+. t/common.sh
+
+printf '%s\n' > file \
+ "${HEAD} 1" \
+ branch-1-first \
+ branch-1-second \
+ "${PARENT} 2" \
+ branch-2-first \
+ branch-2-second \
+ "${TAIL} 3" \
+ branch-3-first \
+ branch-3-second \
+ "${END} 3"
+
+printf '%s\n' > file.expected \
+ "${HEAD} 1" \
+ branch-1-first \
+ branch-1-second \
+ "${PARENT} 2" \
+ branch-2-first \
+ branch-2-second \
+ "${TAIL} 3" \
+ branch-3-first \
+ branch-3-second \
+ "${END} 3"
+
+conflict --reduce file
+
+also_interactive rr
diff --git a/t/reduce-3-tail b/t/reduce-3-tail
new file mode 100755
index 0000000..5291c57
--- /dev/null
+++ b/t/reduce-3-tail
@@ -0,0 +1,38 @@
+#!/bin/sh -e
+. t/common.sh
+
+printf '%s\n' > file \
+ "${HEAD} 1" \
+ branch-1-first \
+ branch-1-second \
+ tail-common-first \
+ tail-common-second \
+ "${PARENT} 2" \
+ branch-2-first \
+ branch-2-second \
+ tail-common-first \
+ tail-common-second \
+ "${TAIL} 3" \
+ branch-3-first \
+ branch-3-second \
+ tail-common-first \
+ tail-common-second \
+ "${END} 3"
+
+printf '%s\n' > file.expected \
+ "${HEAD} 1" \
+ branch-1-first \
+ branch-1-second \
+ "${PARENT} 2" \
+ branch-2-first \
+ branch-2-second \
+ "${TAIL} 3" \
+ branch-3-first \
+ branch-3-second \
+ "${END} 3" \
+ tail-common-first \
+ tail-common-second
+
+conflict --reduce file
+
+also_interactive rr
diff --git a/t/reduce-4-full b/t/reduce-4-full
new file mode 100755
index 0000000..e29c089
--- /dev/null
+++ b/t/reduce-4-full
@@ -0,0 +1,30 @@
+#!/bin/sh -e
+. t/common.sh
+
+printf '%s\n' > file \
+ "${HEAD} 1" \
+ common-first \
+ common-second \
+ common-third \
+ "${PARENT} 2" \
+ common-first \
+ common-second \
+ common-third \
+ "${PARENT} 3" \
+ common-first \
+ common-second \
+ common-third \
+ "${TAIL} 4" \
+ common-first \
+ common-second \
+ common-third \
+ "${END} 4"
+
+printf '%s\n' > file.expected \
+ common-first \
+ common-second \
+ common-third
+
+normal --reduce file
+
+also_interactive r
diff --git a/t/reduce-4-head b/t/reduce-4-head
new file mode 100755
index 0000000..9d36033
--- /dev/null
+++ b/t/reduce-4-head
@@ -0,0 +1,46 @@
+#!/bin/sh -e
+. t/common.sh
+
+printf '%s\n' > file \
+ "${HEAD} 1" \
+ head-common-first \
+ head-common-second \
+ branch-1-first \
+ branch-1-second \
+ "${PARENT} 2" \
+ head-common-first \
+ head-common-second \
+ branch-2-first \
+ branch-2-second \
+ "${PARENT} 3" \
+ head-common-first \
+ head-common-second \
+ branch-3-first \
+ branch-3-second \
+ "${TAIL} 4" \
+ head-common-first \
+ head-common-second \
+ branch-4-first \
+ branch-4-second \
+ "${END} 4"
+
+printf '%s\n' > file.expected \
+ head-common-first \
+ head-common-second \
+ "${HEAD} 1" \
+ branch-1-first \
+ branch-1-second \
+ "${PARENT} 2" \
+ branch-2-first \
+ branch-2-second \
+ "${PARENT} 3" \
+ branch-3-first \
+ branch-3-second \
+ "${TAIL} 4" \
+ branch-4-first \
+ branch-4-second \
+ "${END} 4"
+
+conflict --reduce file
+
+also_interactive rr
diff --git a/t/reduce-4-middle b/t/reduce-4-middle
new file mode 100755
index 0000000..c333b0f
--- /dev/null
+++ b/t/reduce-4-middle
@@ -0,0 +1,67 @@
+#!/bin/sh -e
+. t/common.sh
+
+printf '%s\n' > file \
+ "${HEAD} 1" \
+ branch-1-before-first \
+ branch-1-before-second \
+ middle-common-first \
+ middle-common-second \
+ branch-1-after-first \
+ branch-1-after-second \
+ "${PARENT} 2" \
+ branch-2-before-first \
+ branch-2-before-second \
+ middle-common-first \
+ middle-common-second \
+ branch-2-after-first \
+ branch-2-after-second \
+ "${PARENT} 3" \
+ branch-3-before-first \
+ branch-3-before-second \
+ middle-common-first \
+ middle-common-second \
+ branch-3-after-first \
+ branch-3-after-second \
+ "${TAIL} 4" \
+ branch-4-before-first \
+ branch-4-before-second \
+ middle-common-first \
+ middle-common-second \
+ branch-4-after-first \
+ branch-4-after-second \
+ "${END} 4"
+
+printf '%s\n' > file.expected \
+ "${HEAD} 1" \
+ branch-1-before-first \
+ branch-1-before-second \
+ "${PARENT} 2" \
+ branch-2-before-first \
+ branch-2-before-second \
+ "${PARENT} 3" \
+ branch-3-before-first \
+ branch-3-before-second \
+ "${TAIL} 4" \
+ branch-4-before-first \
+ branch-4-before-second \
+ "${END} 4" \
+ middle-common-first \
+ middle-common-second \
+ "${HEAD} 1" \
+ branch-1-after-first \
+ branch-1-after-second \
+ "${PARENT} 2" \
+ branch-2-after-first \
+ branch-2-after-second \
+ "${PARENT} 3" \
+ branch-3-after-first \
+ branch-3-after-second \
+ "${TAIL} 4" \
+ branch-4-after-first \
+ branch-4-after-second \
+ "${END} 4"
+
+conflict --reduce file
+
+also_interactive rrr
diff --git a/t/reduce-4-multiple-partial b/t/reduce-4-multiple-partial
new file mode 100755
index 0000000..bf23792
--- /dev/null
+++ b/t/reduce-4-multiple-partial
@@ -0,0 +1,87 @@
+#!/bin/sh -e
+. t/common.sh
+
+printf '%s\n' > file \
+ "${HEAD} 1" \
+ head-common-first \
+ head-common-second \
+ branch-1-first-a \
+ branch-1-first-b \
+ middle-common-first \
+ middle-common-second \
+ branch-1-second-a \
+ branch-1-second-b \
+ tail-common-first \
+ tail-common-second \
+ "${PARENT} 2" \
+ head-common-first \
+ head-common-second \
+ branch-2-first-a \
+ branch-2-first-b \
+ middle-common-first \
+ middle-common-second \
+ branch-2-second-a \
+ branch-2-second-b \
+ tail-common-first \
+ tail-common-second \
+ "${PARENT} 3" \
+ head-common-first \
+ head-common-second \
+ branch-3-first-a \
+ branch-3-first-b \
+ middle-common-first \
+ middle-common-second \
+ branch-3-second-a \
+ branch-3-second-b \
+ tail-common-first \
+ tail-common-second \
+ "${TAIL} 4" \
+ head-common-first \
+ head-common-second \
+ branch-4-first-a \
+ branch-4-first-b \
+ middle-common-first \
+ middle-common-second \
+ branch-4-second-a \
+ branch-4-second-b \
+ tail-common-first \
+ tail-common-second \
+ "${END} 4"
+
+printf '%s\n' > file.expected \
+ head-common-first \
+ head-common-second \
+ "${HEAD} 1" \
+ branch-1-first-a \
+ branch-1-first-b \
+ "${PARENT} 2" \
+ branch-2-first-a \
+ branch-2-first-b \
+ "${PARENT} 3" \
+ branch-3-first-a \
+ branch-3-first-b \
+ "${TAIL} 4" \
+ branch-4-first-a \
+ branch-4-first-b \
+ "${END} 4" \
+ middle-common-first \
+ middle-common-second \
+ "${HEAD} 1" \
+ branch-1-second-a \
+ branch-1-second-b \
+ "${PARENT} 2" \
+ branch-2-second-a \
+ branch-2-second-b \
+ "${PARENT} 3" \
+ branch-3-second-a \
+ branch-3-second-b \
+ "${TAIL} 4" \
+ branch-4-second-a \
+ branch-4-second-b \
+ "${END} 4" \
+ tail-common-first \
+ tail-common-second
+
+conflict --reduce file
+
+also_interactive rrr
diff --git a/t/reduce-4-none b/t/reduce-4-none
new file mode 100755
index 0000000..75c95c3
--- /dev/null
+++ b/t/reduce-4-none
@@ -0,0 +1,36 @@
+#!/bin/sh -e
+. t/common.sh
+
+printf '%s\n' > file \
+ "${HEAD} 1" \
+ branch-1-first \
+ branch-1-second \
+ "${PARENT} 2" \
+ branch-2-first \
+ branch-2-second \
+ "${PARENT} 3" \
+ branch-3-first \
+ branch-3-second \
+ "${TAIL} 4" \
+ branch-4-first \
+ branch-4-second \
+ "${END} 4"
+
+printf '%s\n' > file.expected \
+ "${HEAD} 1" \
+ branch-1-first \
+ branch-1-second \
+ "${PARENT} 2" \
+ branch-2-first \
+ branch-2-second \
+ "${PARENT} 3" \
+ branch-3-first \
+ branch-3-second \
+ "${TAIL} 4" \
+ branch-4-first \
+ branch-4-second \
+ "${END} 4"
+
+conflict --reduce file
+
+also_interactive rr
diff --git a/t/reduce-4-tail b/t/reduce-4-tail
new file mode 100755
index 0000000..752f680
--- /dev/null
+++ b/t/reduce-4-tail
@@ -0,0 +1,46 @@
+#!/bin/sh -e
+. t/common.sh
+
+printf '%s\n' > file \
+ "${HEAD} 1" \
+ branch-1-first \
+ branch-1-second \
+ tail-common-first \
+ tail-common-second \
+ "${PARENT} 2" \
+ branch-2-first \
+ branch-2-second \
+ tail-common-first \
+ tail-common-second \
+ "${PARENT} 3" \
+ branch-3-first \
+ branch-3-second \
+ tail-common-first \
+ tail-common-second \
+ "${TAIL} 4" \
+ branch-4-first \
+ branch-4-second \
+ tail-common-first \
+ tail-common-second \
+ "${END} 4"
+
+printf '%s\n' > file.expected \
+ "${HEAD} 1" \
+ branch-1-first \
+ branch-1-second \
+ "${PARENT} 2" \
+ branch-2-first \
+ branch-2-second \
+ "${PARENT} 3" \
+ branch-3-first \
+ branch-3-second \
+ "${TAIL} 4" \
+ branch-4-first \
+ branch-4-second \
+ "${END} 4" \
+ tail-common-first \
+ tail-common-second
+
+conflict --reduce file
+
+also_interactive rr
diff --git a/t/reduce-5-full b/t/reduce-5-full
new file mode 100755
index 0000000..439a510
--- /dev/null
+++ b/t/reduce-5-full
@@ -0,0 +1,34 @@
+#!/bin/sh -e
+. t/common.sh
+
+printf '%s\n' > file \
+ "${HEAD} 1" \
+ common-first \
+ common-second \
+ common-third \
+ "${PARENT} 2" \
+ common-first \
+ common-second \
+ common-third \
+ "${PARENT} 3" \
+ common-first \
+ common-second \
+ common-third \
+ "${PARENT} 4" \
+ common-first \
+ common-second \
+ common-third \
+ "${TAIL} 5" \
+ common-first \
+ common-second \
+ common-third \
+ "${END} 5"
+
+printf '%s\n' > file.expected \
+ common-first \
+ common-second \
+ common-third
+
+normal --reduce file
+
+also_interactive r
diff --git a/t/reduce-5-head b/t/reduce-5-head
new file mode 100755
index 0000000..a85ca8c
--- /dev/null
+++ b/t/reduce-5-head
@@ -0,0 +1,54 @@
+#!/bin/sh -e
+. t/common.sh
+
+printf '%s\n' > file \
+ "${HEAD} 1" \
+ head-common-first \
+ head-common-second \
+ branch-1-first \
+ branch-1-second \
+ "${PARENT} 2" \
+ head-common-first \
+ head-common-second \
+ branch-2-first \
+ branch-2-second \
+ "${PARENT} 3" \
+ head-common-first \
+ head-common-second \
+ branch-3-first \
+ branch-3-second \
+ "${PARENT} 4" \
+ head-common-first \
+ head-common-second \
+ branch-4-first \
+ branch-4-second \
+ "${TAIL} 5" \
+ head-common-first \
+ head-common-second \
+ branch-5-first \
+ branch-5-second \
+ "${END} 5"
+
+printf '%s\n' > file.expected \
+ head-common-first \
+ head-common-second \
+ "${HEAD} 1" \
+ branch-1-first \
+ branch-1-second \
+ "${PARENT} 2" \
+ branch-2-first \
+ branch-2-second \
+ "${PARENT} 3" \
+ branch-3-first \
+ branch-3-second \
+ "${PARENT} 4" \
+ branch-4-first \
+ branch-4-second \
+ "${TAIL} 5" \
+ branch-5-first \
+ branch-5-second \
+ "${END} 5"
+
+conflict --reduce file
+
+also_interactive rr
diff --git a/t/reduce-5-middle b/t/reduce-5-middle
new file mode 100755
index 0000000..1a4cd73
--- /dev/null
+++ b/t/reduce-5-middle
@@ -0,0 +1,80 @@
+#!/bin/sh -e
+. t/common.sh
+
+printf '%s\n' > file \
+ "${HEAD} 1" \
+ branch-1-before-first \
+ branch-1-before-second \
+ middle-common-first \
+ middle-common-second \
+ branch-1-after-first \
+ branch-1-after-second \
+ "${PARENT} 2" \
+ branch-2-before-first \
+ branch-2-before-second \
+ middle-common-first \
+ middle-common-second \
+ branch-2-after-first \
+ branch-2-after-second \
+ "${PARENT} 3" \
+ branch-3-before-first \
+ branch-3-before-second \
+ middle-common-first \
+ middle-common-second \
+ branch-3-after-first \
+ branch-3-after-second \
+ "${PARENT} 4" \
+ branch-4-before-first \
+ branch-4-before-second \
+ middle-common-first \
+ middle-common-second \
+ branch-4-after-first \
+ branch-4-after-second \
+ "${TAIL} 5" \
+ branch-5-before-first \
+ branch-5-before-second \
+ middle-common-first \
+ middle-common-second \
+ branch-5-after-first \
+ branch-5-after-second \
+ "${END} 5"
+
+printf '%s\n' > file.expected \
+ "${HEAD} 1" \
+ branch-1-before-first \
+ branch-1-before-second \
+ "${PARENT} 2" \
+ branch-2-before-first \
+ branch-2-before-second \
+ "${PARENT} 3" \
+ branch-3-before-first \
+ branch-3-before-second \
+ "${PARENT} 4" \
+ branch-4-before-first \
+ branch-4-before-second \
+ "${TAIL} 5" \
+ branch-5-before-first \
+ branch-5-before-second \
+ "${END} 5" \
+ middle-common-first \
+ middle-common-second \
+ "${HEAD} 1" \
+ branch-1-after-first \
+ branch-1-after-second \
+ "${PARENT} 2" \
+ branch-2-after-first \
+ branch-2-after-second \
+ "${PARENT} 3" \
+ branch-3-after-first \
+ branch-3-after-second \
+ "${PARENT} 4" \
+ branch-4-after-first \
+ branch-4-after-second \
+ "${TAIL} 5" \
+ branch-5-after-first \
+ branch-5-after-second \
+ "${END} 5"
+
+conflict --reduce file
+
+also_interactive rrr
diff --git a/t/reduce-5-multiple-partial b/t/reduce-5-multiple-partial
new file mode 100755
index 0000000..e95805f
--- /dev/null
+++ b/t/reduce-5-multiple-partial
@@ -0,0 +1,104 @@
+#!/bin/sh -e
+. t/common.sh
+
+printf '%s\n' > file \
+ "${HEAD} 1" \
+ head-common-first \
+ head-common-second \
+ branch-1-first-a \
+ branch-1-first-b \
+ middle-common-first \
+ middle-common-second \
+ branch-1-second-a \
+ branch-1-second-b \
+ tail-common-first \
+ tail-common-second \
+ "${PARENT} 2" \
+ head-common-first \
+ head-common-second \
+ branch-2-first-a \
+ branch-2-first-b \
+ middle-common-first \
+ middle-common-second \
+ branch-2-second-a \
+ branch-2-second-b \
+ tail-common-first \
+ tail-common-second \
+ "${PARENT} 3" \
+ head-common-first \
+ head-common-second \
+ branch-3-first-a \
+ branch-3-first-b \
+ middle-common-first \
+ middle-common-second \
+ branch-3-second-a \
+ branch-3-second-b \
+ tail-common-first \
+ tail-common-second \
+ "${PARENT} 4" \
+ head-common-first \
+ head-common-second \
+ branch-4-first-a \
+ branch-4-first-b \
+ middle-common-first \
+ middle-common-second \
+ branch-4-second-a \
+ branch-4-second-b \
+ tail-common-first \
+ tail-common-second \
+ "${TAIL} 5" \
+ head-common-first \
+ head-common-second \
+ branch-5-first-a \
+ branch-5-first-b \
+ middle-common-first \
+ middle-common-second \
+ branch-5-second-a \
+ branch-5-second-b \
+ tail-common-first \
+ tail-common-second \
+ "${END} 5"
+
+printf '%s\n' > file.expected \
+ head-common-first \
+ head-common-second \
+ "${HEAD} 1" \
+ branch-1-first-a \
+ branch-1-first-b \
+ "${PARENT} 2" \
+ branch-2-first-a \
+ branch-2-first-b \
+ "${PARENT} 3" \
+ branch-3-first-a \
+ branch-3-first-b \
+ "${PARENT} 4" \
+ branch-4-first-a \
+ branch-4-first-b \
+ "${TAIL} 5" \
+ branch-5-first-a \
+ branch-5-first-b \
+ "${END} 5" \
+ middle-common-first \
+ middle-common-second \
+ "${HEAD} 1" \
+ branch-1-second-a \
+ branch-1-second-b \
+ "${PARENT} 2" \
+ branch-2-second-a \
+ branch-2-second-b \
+ "${PARENT} 3" \
+ branch-3-second-a \
+ branch-3-second-b \
+ "${PARENT} 4" \
+ branch-4-second-a \
+ branch-4-second-b \
+ "${TAIL} 5" \
+ branch-5-second-a \
+ branch-5-second-b \
+ "${END} 5" \
+ tail-common-first \
+ tail-common-second
+
+conflict --reduce file
+
+also_interactive rrr
diff --git a/t/reduce-5-none b/t/reduce-5-none
new file mode 100755
index 0000000..d22f1e5
--- /dev/null
+++ b/t/reduce-5-none
@@ -0,0 +1,42 @@
+#!/bin/sh -e
+. t/common.sh
+
+printf '%s\n' > file \
+ "${HEAD} 1" \
+ branch-1-first \
+ branch-1-second \
+ "${PARENT} 2" \
+ branch-2-first \
+ branch-2-second \
+ "${PARENT} 3" \
+ branch-3-first \
+ branch-3-second \
+ "${PARENT} 4" \
+ branch-4-first \
+ branch-4-second \
+ "${TAIL} 5" \
+ branch-5-first \
+ branch-5-second \
+ "${END} 5"
+
+printf '%s\n' > file.expected \
+ "${HEAD} 1" \
+ branch-1-first \
+ branch-1-second \
+ "${PARENT} 2" \
+ branch-2-first \
+ branch-2-second \
+ "${PARENT} 3" \
+ branch-3-first \
+ branch-3-second \
+ "${PARENT} 4" \
+ branch-4-first \
+ branch-4-second \
+ "${TAIL} 5" \
+ branch-5-first \
+ branch-5-second \
+ "${END} 5"
+
+conflict --reduce file
+
+also_interactive rr
diff --git a/t/reduce-5-tail b/t/reduce-5-tail
new file mode 100755
index 0000000..a003cd3
--- /dev/null
+++ b/t/reduce-5-tail
@@ -0,0 +1,54 @@
+#!/bin/sh -e
+. t/common.sh
+
+printf '%s\n' > file \
+ "${HEAD} 1" \
+ branch-1-first \
+ branch-1-second \
+ tail-common-first \
+ tail-common-second \
+ "${PARENT} 2" \
+ branch-2-first \
+ branch-2-second \
+ tail-common-first \
+ tail-common-second \
+ "${PARENT} 3" \
+ branch-3-first \
+ branch-3-second \
+ tail-common-first \
+ tail-common-second \
+ "${PARENT} 4" \
+ branch-4-first \
+ branch-4-second \
+ tail-common-first \
+ tail-common-second \
+ "${TAIL} 5" \
+ branch-5-first \
+ branch-5-second \
+ tail-common-first \
+ tail-common-second \
+ "${END} 5"
+
+printf '%s\n' > file.expected \
+ "${HEAD} 1" \
+ branch-1-first \
+ branch-1-second \
+ "${PARENT} 2" \
+ branch-2-first \
+ branch-2-second \
+ "${PARENT} 3" \
+ branch-3-first \
+ branch-3-second \
+ "${PARENT} 4" \
+ branch-4-first \
+ branch-4-second \
+ "${TAIL} 5" \
+ branch-5-first \
+ branch-5-second \
+ "${END} 5" \
+ tail-common-first \
+ tail-common-second
+
+conflict --reduce file
+
+also_interactive rr
diff --git a/t/reduce-empty-base b/t/reduce-empty-base
new file mode 100755
index 0000000..09e80cc
--- /dev/null
+++ b/t/reduce-empty-base
@@ -0,0 +1,16 @@
+#!/bin/sh -e
+. t/common.sh
+
+printf '%s\n' > file \
+ "${HEAD} ours" \
+ common \
+ "${PARENT} base" \
+ "${TAIL}" \
+ common \
+ "${END} theirs"
+
+cp -- file file.expected
+
+conflict --reduce file
+
+also_interactive r
diff --git a/t/reduce-empty-head b/t/reduce-empty-head
new file mode 100755
index 0000000..6d082aa
--- /dev/null
+++ b/t/reduce-empty-head
@@ -0,0 +1,16 @@
+#!/bin/sh -e
+. t/common.sh
+
+printf '%s\n' > file \
+ "${HEAD} ours" \
+ "${PARENT} base" \
+ common \
+ "${TAIL}" \
+ common \
+ "${END} theirs"
+
+cp -- file file.expected
+
+conflict --reduce file
+
+also_interactive r
diff --git a/t/reduce-empty-tail b/t/reduce-empty-tail
new file mode 100755
index 0000000..a1ce3df
--- /dev/null
+++ b/t/reduce-empty-tail
@@ -0,0 +1,16 @@
+#!/bin/sh -e
+. t/common.sh
+
+printf '%s\n' > file \
+ "${HEAD} ours" \
+ common \
+ "${PARENT} base" \
+ common \
+ "${TAIL}" \
+ "${END} theirs"
+
+cp -- file file.expected
+
+conflict --reduce file
+
+also_interactive r
diff --git a/t/reduce-is-default b/t/reduce-is-default
new file mode 100755
index 0000000..c5f7dd8
--- /dev/null
+++ b/t/reduce-is-default
@@ -0,0 +1,23 @@
+#!/bin/sh -e
+. t/common.sh
+
+printf '%s\n' > file \
+ "${HEAD} HEAD" \
+ same \
+ head \
+ "${TAIL}" \
+ same \
+ tail \
+ "${END} branch"
+
+printf '%s\n' > file.expected \
+ same \
+ "${HEAD} HEAD" \
+ head \
+ "${TAIL}" \
+ tail \
+ "${END} branch"
+
+conflict file
+
+also_interactive rr
diff --git a/t/reduce-no-conflicts b/t/reduce-no-conflicts
new file mode 100755
index 0000000..41dbae0
--- /dev/null
+++ b/t/reduce-no-conflicts
@@ -0,0 +1,11 @@
+#!/bin/sh -e
+. t/common.sh
+
+printf '%s\n' > file \
+ no conflicts
+
+cp -- file file.expected
+
+normal --reduce file
+
+also_interactive ''
diff --git a/t/remove-base-1 b/t/remove-base-1
new file mode 100755
index 0000000..b497fe6
--- /dev/null
+++ b/t/remove-base-1
@@ -0,0 +1,12 @@
+#!/bin/sh -e
+. t/common.sh
+
+printf '%s\n' > file \
+ "${HEAD} Head" \
+ head \
+ "${END} Head"
+
+printf '%s\n' > file.expected \
+ head
+
+error --remove-base --no-reduce file
diff --git a/t/remove-base-2 b/t/remove-base-2
new file mode 100755
index 0000000..c4a4fb6
--- /dev/null
+++ b/t/remove-base-2
@@ -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 --remove-base --no-reduce file
+
+also_interactive b
diff --git a/t/remove-base-3 b/t/remove-base-3
new file mode 100755
index 0000000..b0be37f
--- /dev/null
+++ b/t/remove-base-3
@@ -0,0 +1,22 @@
+#!/bin/sh -e
+. t/common.sh
+
+printf '%s\n' > file \
+ "${HEAD} ours" \
+ ours \
+ "${PARENT} base" \
+ base \
+ "${TAIL}" \
+ theirs \
+ "${END} theirs"
+
+printf '%s\n' > file.expected \
+ "${HEAD} ours" \
+ ours \
+ "${TAIL}" \
+ theirs \
+ "${END} theirs"
+
+conflict --remove-base --no-reduce file
+
+also_interactive bb
diff --git a/t/remove-base-3-empty-base b/t/remove-base-3-empty-base
new file mode 100755
index 0000000..b3e009d
--- /dev/null
+++ b/t/remove-base-3-empty-base
@@ -0,0 +1,21 @@
+#!/bin/sh -e
+. t/common.sh
+
+printf '%s\n' > file \
+ "${HEAD} ours" \
+ ours \
+ "${PARENT} base" \
+ "${TAIL}" \
+ theirs \
+ "${END} theirs"
+
+printf '%s\n' > file.expected \
+ "${HEAD} ours" \
+ ours \
+ "${TAIL}" \
+ theirs \
+ "${END} theirs"
+
+conflict --remove-base --no-reduce file
+
+also_interactive bb
diff --git a/t/remove-base-3-empty-head b/t/remove-base-3-empty-head
new file mode 100755
index 0000000..af4f9e9
--- /dev/null
+++ b/t/remove-base-3-empty-head
@@ -0,0 +1,20 @@
+#!/bin/sh -e
+. t/common.sh
+
+printf '%s\n' > file \
+ "${HEAD} ours" \
+ "${PARENT} base" \
+ base \
+ "${TAIL}" \
+ theirs \
+ "${END} theirs"
+
+printf '%s\n' > file.expected \
+ "${HEAD} ours" \
+ "${TAIL}" \
+ theirs \
+ "${END} theirs"
+
+conflict --remove-base --no-reduce file
+
+also_interactive bb
diff --git a/t/remove-base-3-empty-tail b/t/remove-base-3-empty-tail
new file mode 100755
index 0000000..bf0302b
--- /dev/null
+++ b/t/remove-base-3-empty-tail
@@ -0,0 +1,20 @@
+#!/bin/sh -e
+. t/common.sh
+
+printf '%s\n' > file \
+ "${HEAD} ours" \
+ ours \
+ "${PARENT} base" \
+ base \
+ "${TAIL}" \
+ "${END} theirs"
+
+printf '%s\n' > file.expected \
+ "${HEAD} ours" \
+ ours \
+ "${TAIL}" \
+ "${END} theirs"
+
+conflict --remove-base --no-reduce file
+
+also_interactive bb
diff --git a/t/remove-base-4 b/t/remove-base-4
new file mode 100755
index 0000000..4e77685
--- /dev/null
+++ b/t/remove-base-4
@@ -0,0 +1,19 @@
+#!/bin/sh -e
+. t/common.sh
+
+printf '%s\n' > file \
+ "${HEAD} head" \
+ head \
+ "${PARENT} 2" \
+ second \
+ "${PARENT} 3" \
+ third \
+ "${TAIL}" \
+ tail \
+ "${END} tail"
+
+cp -- file file.expected
+
+conflict --remove-base --no-reduce file
+
+also_interactive b
diff --git a/t/remove-base-5 b/t/remove-base-5
new file mode 100755
index 0000000..fd50087
--- /dev/null
+++ b/t/remove-base-5
@@ -0,0 +1,21 @@
+#!/bin/sh -e
+. t/common.sh
+
+printf '%s\n' > file \
+ "${HEAD} head" \
+ head \
+ "${PARENT} 2" \
+ second \
+ "${PARENT} 3" \
+ third \
+ "${PARENT} 4" \
+ fourth \
+ "${TAIL}" \
+ tail \
+ "${END} tail"
+
+cp -- file file.expected
+
+conflict --remove-base --no-reduce file
+
+also_interactive b
diff --git a/t/remove-base-multiple-files b/t/remove-base-multiple-files
new file mode 100755
index 0000000..e3ad177
--- /dev/null
+++ b/t/remove-base-multiple-files
@@ -0,0 +1,33 @@
+#!/bin/sh -e
+. t/common.sh
+
+printf '%s\n' > first \
+ "${HEAD} first" \
+ first-head \
+ "${TAIL}" \
+ first-tail \
+ "${END}"
+
+cp -- first first.expected
+
+printf '%s\n' > second \
+ before \
+ "${HEAD} second" \
+ second-head \
+ "${PARENT} middle" \
+ second-middle \
+ "${TAIL}" \
+ second-tail \
+ "${END}"
+
+printf '%s\n' > second.expected \
+ before \
+ "${HEAD} second" \
+ second-head \
+ "${TAIL}" \
+ second-tail \
+ "${END}"
+
+conflict --remove-base --no-reduce first second
+
+also_interactive bbb
diff --git a/t/remove-base-multiple-hunks b/t/remove-base-multiple-hunks
new file mode 100755
index 0000000..c3fdc0f
--- /dev/null
+++ b/t/remove-base-multiple-hunks
@@ -0,0 +1,36 @@
+#!/bin/sh -e
+. t/common.sh
+
+printf '%s\n' > file \
+ "${HEAD} first" \
+ first-head \
+ "${TAIL}" \
+ first-tail \
+ "${END}" \
+ between \
+ "${HEAD} second" \
+ second-head \
+ "${PARENT} middle" \
+ second-middle \
+ "${TAIL}" \
+ second-tail \
+ "${END}" \
+ after
+
+printf '%s\n' > file.expected \
+ "${HEAD} first" \
+ first-head \
+ "${TAIL}" \
+ first-tail \
+ "${END}" \
+ between \
+ "${HEAD} second" \
+ second-head \
+ "${TAIL}" \
+ second-tail \
+ "${END}" \
+ after
+
+conflict --remove-base --no-reduce file
+
+also_interactive bbbb
diff --git a/t/remove-base-no-conflicts b/t/remove-base-no-conflicts
new file mode 100755
index 0000000..9c36985
--- /dev/null
+++ b/t/remove-base-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 --remove-base --no-reduce file
+
+also_interactive ''
diff --git a/t/select-head-1 b/t/select-head-1
new file mode 100755
index 0000000..f803cf2
--- /dev/null
+++ b/t/select-head-1
@@ -0,0 +1,12 @@
+#!/bin/sh -e
+. t/common.sh
+
+printf '%s\n' > file \
+ "${HEAD} Head" \
+ head \
+ "${END} Head"
+
+printf '%s\n' > file.expected \
+ head
+
+error --select-head --no-reduce file
diff --git a/t/select-head-2 b/t/select-head-2
new file mode 100755
index 0000000..e050f9f
--- /dev/null
+++ b/t/select-head-2
@@ -0,0 +1,16 @@
+#!/bin/sh -e
+. t/common.sh
+
+printf '%s\n' > file \
+ "${HEAD} head" \
+ head \
+ "${TAIL}" \
+ tail \
+ "${END} tail"
+
+printf '%s\n' > file.expected \
+ head
+
+normal --select-head --no-reduce file
+
+also_interactive h
diff --git a/t/select-head-3 b/t/select-head-3
new file mode 100755
index 0000000..98e5b63
--- /dev/null
+++ b/t/select-head-3
@@ -0,0 +1,18 @@
+#!/bin/sh -e
+. t/common.sh
+
+printf '%s\n' > file \
+ "${HEAD} ours" \
+ ours \
+ "${PARENT} base" \
+ base \
+ "${TAIL}" \
+ theirs \
+ "${END} theirs"
+
+printf '%s\n' > file.expected \
+ ours
+
+normal --select-head --no-reduce file
+
+also_interactive h
diff --git a/t/select-head-3-empty-base b/t/select-head-3-empty-base
new file mode 100755
index 0000000..0bfa110
--- /dev/null
+++ b/t/select-head-3-empty-base
@@ -0,0 +1,17 @@
+#!/bin/sh -e
+. t/common.sh
+
+printf '%s\n' > file \
+ "${HEAD}" \
+ ours \
+ "${PARENT}" \
+ "${TAIL}" \
+ theirs \
+ "${END}"
+
+printf '%s\n' > file.expected \
+ ours
+
+normal --select-head --no-reduce file
+
+also_interactive h
diff --git a/t/select-head-3-empty-head b/t/select-head-3-empty-head
new file mode 100755
index 0000000..f1ebd7c
--- /dev/null
+++ b/t/select-head-3-empty-head
@@ -0,0 +1,16 @@
+#!/bin/sh -e
+. t/common.sh
+
+printf '%s\n' > file \
+ "${HEAD} ours" \
+ "${PARENT} base" \
+ base \
+ "${TAIL} theirs" \
+ theirs \
+ "${END} end of conflict"
+
+: > file.expected
+
+normal --select-head --no-reduce file
+
+also_interactive h
diff --git a/t/select-head-3-empty-tail b/t/select-head-3-empty-tail
new file mode 100755
index 0000000..8041a15
--- /dev/null
+++ b/t/select-head-3-empty-tail
@@ -0,0 +1,17 @@
+#!/bin/sh -e
+. t/common.sh
+
+printf '%s\n' > file \
+ "${HEAD} o" \
+ ours \
+ "${PARENT} b" \
+ base \
+ "${TAIL}" \
+ "${END} t"
+
+printf '%s\n' > file.expected \
+ ours
+
+normal --select-head --no-reduce file
+
+also_interactive h
diff --git a/t/select-head-4 b/t/select-head-4
new file mode 100755
index 0000000..6c9c9dd
--- /dev/null
+++ b/t/select-head-4
@@ -0,0 +1,22 @@
+#!/bin/sh -e
+. t/common.sh
+
+printf '%s\n' > file \
+ "${HEAD} head" \
+ head \
+ "${PARENT} 2" \
+ second \
+ "${PARENT} 3" \
+ third \
+ "${PARENT} 4" \
+ fourth \
+ "${TAIL}" \
+ tail \
+ "${END} tail"
+
+printf '%s\n' > file.expected \
+ head
+
+normal --select-head --no-reduce file
+
+also_interactive h
diff --git a/t/select-head-multiple-files b/t/select-head-multiple-files
new file mode 100755
index 0000000..ba103a1
--- /dev/null
+++ b/t/select-head-multiple-files
@@ -0,0 +1,30 @@
+#!/bin/sh -e
+. t/common.sh
+
+printf '%s\n' > first \
+ "${HEAD} first" \
+ first-head \
+ "${TAIL}" \
+ first-tail \
+ "${END}"
+
+printf '%s\n' > first.expected \
+ first-head
+
+printf '%s\n' > second \
+ before \
+ "${HEAD} second" \
+ second-head \
+ "${PARENT} middle" \
+ second-middle \
+ "${TAIL}" \
+ second-tail \
+ "${END}"
+
+printf '%s\n' > second.expected \
+ before \
+ second-head
+
+normal --select-head --no-reduce first second
+
+also_interactive hh
diff --git a/t/select-head-multiple-hunks b/t/select-head-multiple-hunks
new file mode 100755
index 0000000..f0bad13
--- /dev/null
+++ b/t/select-head-multiple-hunks
@@ -0,0 +1,28 @@
+#!/bin/sh -e
+. t/common.sh
+
+printf '%s\n' > file \
+ "${HEAD} first" \
+ first-head \
+ "${TAIL}" \
+ first-tail \
+ "${END}" \
+ between \
+ "${HEAD} second" \
+ second-head \
+ "${PARENT} middle" \
+ second-middle \
+ "${TAIL}" \
+ second-tail \
+ "${END}" \
+ after
+
+printf '%s\n' > file.expected \
+ first-head \
+ between \
+ second-head \
+ after
+
+normal --select-head --no-reduce file
+
+also_interactive hh
diff --git a/t/select-head-no-conflicts b/t/select-head-no-conflicts
new file mode 100755
index 0000000..d996675
--- /dev/null
+++ b/t/select-head-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 --select-head --no-reduce file
+
+also_interactive ''
diff --git a/t/select-tail-1 b/t/select-tail-1
new file mode 100755
index 0000000..b11e471
--- /dev/null
+++ b/t/select-tail-1
@@ -0,0 +1,12 @@
+#!/bin/sh -e
+. t/common.sh
+
+printf '%s\n' > file \
+ "${HEAD} Tail" \
+ tail \
+ "${END} Tail"
+
+printf '%s\n' > file.expected \
+ tail
+
+error --select-tail --no-reduce file
diff --git a/t/select-tail-2 b/t/select-tail-2
new file mode 100755
index 0000000..a6d1c0f
--- /dev/null
+++ b/t/select-tail-2
@@ -0,0 +1,16 @@
+#!/bin/sh -e
+. t/common.sh
+
+printf '%s\n' > file \
+ "${HEAD} head" \
+ head \
+ "${TAIL}" \
+ tail \
+ "${END} tail"
+
+printf '%s\n' > file.expected \
+ tail
+
+normal --select-tail --no-reduce file
+
+also_interactive t
diff --git a/t/select-tail-3 b/t/select-tail-3
new file mode 100755
index 0000000..7a7962a
--- /dev/null
+++ b/t/select-tail-3
@@ -0,0 +1,18 @@
+#!/bin/sh -e
+. t/common.sh
+
+printf '%s\n' > file \
+ "${HEAD} ours" \
+ ours \
+ "${PARENT} base" \
+ base \
+ "${TAIL}" \
+ theirs \
+ "${END} theirs"
+
+printf '%s\n' > file.expected \
+ theirs
+
+normal --select-tail --no-reduce file
+
+also_interactive t
diff --git a/t/select-tail-3-empty-base b/t/select-tail-3-empty-base
new file mode 100755
index 0000000..6e88956
--- /dev/null
+++ b/t/select-tail-3-empty-base
@@ -0,0 +1,17 @@
+#!/bin/sh -e
+. t/common.sh
+
+printf '%s\n' > file \
+ "${HEAD} head" \
+ ours \
+ "${PARENT} parent" \
+ "${TAIL}" \
+ theirs \
+ "${END} tail"
+
+printf '%s\n' > file.expected \
+ theirs
+
+normal --select-tail --no-reduce file
+
+also_interactive t
diff --git a/t/select-tail-3-empty-head b/t/select-tail-3-empty-head
new file mode 100755
index 0000000..424c73b
--- /dev/null
+++ b/t/select-tail-3-empty-head
@@ -0,0 +1,17 @@
+#!/bin/sh -e
+. t/common.sh
+
+printf '%s\n' > file \
+ "${HEAD} OURS" \
+ "${PARENT} BASE" \
+ base \
+ "${TAIL}" \
+ theirs \
+ "${END} THEIRS"
+
+printf '%s\n' > file.expected \
+ theirs
+
+normal --select-tail --no-reduce file
+
+also_interactive t
diff --git a/t/select-tail-3-empty-tail b/t/select-tail-3-empty-tail
new file mode 100755
index 0000000..d625068
--- /dev/null
+++ b/t/select-tail-3-empty-tail
@@ -0,0 +1,16 @@
+#!/bin/sh -e
+. t/common.sh
+
+printf '%s\n' > file \
+ "${HEAD} ours" \
+ ours \
+ "${PARENT} base" \
+ base \
+ "${TAIL}" \
+ "${END} theirs"
+
+: > file.expected
+
+normal --select-tail --no-reduce file
+
+also_interactive t
diff --git a/t/select-tail-4 b/t/select-tail-4
new file mode 100755
index 0000000..45111db
--- /dev/null
+++ b/t/select-tail-4
@@ -0,0 +1,22 @@
+#!/bin/sh -e
+. t/common.sh
+
+printf '%s\n' > file \
+ "${HEAD} head" \
+ head \
+ "${PARENT} 2" \
+ second \
+ "${PARENT} 3" \
+ third \
+ "${PARENT} 4" \
+ fourth \
+ "${TAIL}" \
+ tail \
+ "${END} tail"
+
+printf '%s\n' > file.expected \
+ tail
+
+normal --select-tail --no-reduce file
+
+also_interactive t
diff --git a/t/select-tail-multiple-files b/t/select-tail-multiple-files
new file mode 100755
index 0000000..f2c8ae8
--- /dev/null
+++ b/t/select-tail-multiple-files
@@ -0,0 +1,30 @@
+#!/bin/sh -e
+. t/common.sh
+
+printf '%s\n' > first \
+ "${HEAD} first" \
+ first-head \
+ "${TAIL}" \
+ first-tail \
+ "${END}"
+
+printf '%s\n' > first.expected \
+ first-tail
+
+printf '%s\n' > second \
+ before \
+ "${HEAD} second" \
+ second-head \
+ "${PARENT} middle" \
+ second-middle \
+ "${TAIL}" \
+ second-tail \
+ "${END}"
+
+printf '%s\n' > second.expected \
+ before \
+ second-tail
+
+normal --select-tail --no-reduce first second
+
+also_interactive tt
diff --git a/t/select-tail-multiple-hunks b/t/select-tail-multiple-hunks
new file mode 100755
index 0000000..eaa80f1
--- /dev/null
+++ b/t/select-tail-multiple-hunks
@@ -0,0 +1,28 @@
+#!/bin/sh -e
+. t/common.sh
+
+printf '%s\n' > file \
+ "${HEAD} first" \
+ first-head \
+ "${TAIL}" \
+ first-tail \
+ "${END}" \
+ between \
+ "${HEAD} second" \
+ second-head \
+ "${PARENT} middle" \
+ second-middle \
+ "${TAIL}" \
+ second-tail \
+ "${END}" \
+ after
+
+printf '%s\n' > file.expected \
+ first-tail \
+ between \
+ second-tail \
+ after
+
+normal --select-tail --no-reduce file
+
+also_interactive tt
diff --git a/t/select-tail-no-conflicts b/t/select-tail-no-conflicts
new file mode 100755
index 0000000..ba98700
--- /dev/null
+++ b/t/select-tail-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 --select-tail --no-reduce file
+
+also_interactive ''
diff --git a/t/stdin-dash-operand b/t/stdin-dash-operand
new file mode 100755
index 0000000..42d960a
--- /dev/null
+++ b/t/stdin-dash-operand
@@ -0,0 +1,21 @@
+#!/bin/sh -e
+. t/common.sh
+
+printf '%s\n' > file \
+ "${HEAD} HEAD" \
+ same \
+ "${TAIL}" \
+ same \
+ "${END} branch"
+
+printf '%s\n' > file.expected \
+ same
+
+cp -- file input
+normal --reduce - < input > output
+cmp -s -- file input
+diff -u -- output file.expected >&2
+
+also_interactive r < input > output
+cmp -s -- file input
+diff -u -- output file.expected >&2
diff --git a/t/stdin-dashdash-dash-operand b/t/stdin-dashdash-dash-operand
new file mode 100755
index 0000000..f9e41c6
--- /dev/null
+++ b/t/stdin-dashdash-dash-operand
@@ -0,0 +1,21 @@
+#!/bin/sh -e
+. t/common.sh
+
+printf '%s\n' > file \
+ "${HEAD} HEAD" \
+ same \
+ "${TAIL}" \
+ same \
+ "${END} branch"
+
+printf '%s\n' > file.expected \
+ same
+
+cp -- file input
+normal --reduce -- - < input > output
+cmp -s -- file input
+diff -u -- output file.expected >&2
+
+also_interactive r < input > output
+cmp -s -- file input
+diff -u -- output file.expected >&2
diff --git a/t/stdin-dashdash-no-operand b/t/stdin-dashdash-no-operand
new file mode 100755
index 0000000..1e9c6c6
--- /dev/null
+++ b/t/stdin-dashdash-no-operand
@@ -0,0 +1,21 @@
+#!/bin/sh -e
+. t/common.sh
+
+printf '%s\n' > file \
+ "${HEAD} HEAD" \
+ same \
+ "${TAIL}" \
+ same \
+ "${END} branch"
+
+printf '%s\n' > file.expected \
+ same
+
+cp -- file input
+normal --reduce -- < input > output
+cmp -s -- file input
+diff -u -- output file.expected >&2
+
+also_interactive r < input > output
+cmp -s -- file input
+diff -u -- output file.expected >&2
diff --git a/t/stdin-no-operand b/t/stdin-no-operand
new file mode 100755
index 0000000..54a3cb4
--- /dev/null
+++ b/t/stdin-no-operand
@@ -0,0 +1,21 @@
+#!/bin/sh -e
+. t/common.sh
+
+printf '%s\n' > file \
+ "${HEAD} HEAD" \
+ same \
+ "${TAIL}" \
+ same \
+ "${END} branch"
+
+printf '%s\n' > file.expected \
+ same
+
+cp -- file input
+normal --reduce < input > output
+cmp -s -- file input
+diff -u -- output file.expected >&2
+
+also_interactive r < input > output
+cmp -s -- file input
+diff -u -- output file.expected >&2
diff --git a/t/unclosed-conflict-head b/t/unclosed-conflict-head
new file mode 100755
index 0000000..4a7f4a2
--- /dev/null
+++ b/t/unclosed-conflict-head
@@ -0,0 +1,12 @@
+#!/bin/sh -e
+. t/common.sh
+
+printf '%s\n' > file \
+ "${HEAD} a" \
+ broken
+
+cp -- file file.original
+
+error file
+
+diff -u -- file file.original >&2
diff --git a/t/unclosed-conflict-head-parent b/t/unclosed-conflict-head-parent
new file mode 100755
index 0000000..ae4c501
--- /dev/null
+++ b/t/unclosed-conflict-head-parent
@@ -0,0 +1,14 @@
+#!/bin/sh -e
+. t/common.sh
+
+printf '%s\n' > file \
+ "${HEAD} a" \
+ first \
+ "${PARENT} b" \
+ broken
+
+cp -- file file.original
+
+error file
+
+diff -u -- file file.original >&2
diff --git a/t/unclosed-conflict-head-parent-tail b/t/unclosed-conflict-head-parent-tail
new file mode 100755
index 0000000..c05ca5d
--- /dev/null
+++ b/t/unclosed-conflict-head-parent-tail
@@ -0,0 +1,16 @@
+#!/bin/sh -e
+. t/common.sh
+
+printf '%s\n' > file \
+ "${HEAD} a" \
+ first \
+ "${PARENT} b" \
+ middle \
+ "${TAIL}" \
+ broken
+
+cp -- file file.original
+
+error file
+
+diff -u -- file file.original >&2
diff --git a/t/unclosed-conflict-head-tail b/t/unclosed-conflict-head-tail
new file mode 100755
index 0000000..b84755e
--- /dev/null
+++ b/t/unclosed-conflict-head-tail
@@ -0,0 +1,14 @@
+#!/bin/sh -e
+. t/common.sh
+
+printf '%s\n' > file \
+ "${HEAD} a" \
+ first \
+ "${TAIL}" \
+ broken
+
+cp -- file file.original
+
+error file
+
+diff -u -- file file.original >&2
diff --git a/t/unexpected-separator-end b/t/unexpected-separator-end
new file mode 100755
index 0000000..1f81488
--- /dev/null
+++ b/t/unexpected-separator-end
@@ -0,0 +1,11 @@
+#!/bin/sh -e
+. t/common.sh
+
+printf '%s\n' > file \
+ "${END}"
+
+cp -- file file.original
+
+error file
+
+diff -u -- file file.original >&2
diff --git a/t/unexpected-separator-parent b/t/unexpected-separator-parent
new file mode 100755
index 0000000..1b86d70
--- /dev/null
+++ b/t/unexpected-separator-parent
@@ -0,0 +1,11 @@
+#!/bin/sh -e
+. t/common.sh
+
+printf '%s\n' > file \
+ "${PARENT} base"
+
+cp -- file file.original
+
+error file
+
+diff -u -- file file.original >&2
diff --git a/t/unexpected-separator-tail b/t/unexpected-separator-tail
new file mode 100755
index 0000000..e8f8ad6
--- /dev/null
+++ b/t/unexpected-separator-tail
@@ -0,0 +1,11 @@
+#!/bin/sh -e
+. t/common.sh
+
+printf '%s\n' > file \
+ "${TAIL}"
+
+cp -- file file.original
+
+error file
+
+diff -u -- file file.original >&2