apply: support --ours, --theirs, and --union for three-way merges

--ours, --theirs, and --union are already supported in `git merge-file` for automatically resolving conflicts in favor of one version or the other, instead of leaving conflict markers in the file. Support them in `git apply -3` as well because the two commands do the same kind of file-level merges. In case in the future --ours, --theirs, and --union gain a meaning outside of three-way-merges, they do not imply --3way but rather must be specified alongside it. Signed-off-by: Alex Henrie <alexhenrie24@gmail.com> Signed-off-by: Junio C Hamano <gitster@pobox.com>

Alex Henrie committed Sep 9, 2024 at 08:10 UTC 57f583c748fdc65c16d1c24a7060a758850cb4a2
4 files changed +67 -3
Documentation/git-apply.txt
+8 -1
@@ -9,7 +9,8 @@ git-apply - Apply a patch to files and/or to the index
9 SYNOPSIS
10 --------
11 [verse]
12 -'git apply' [--stat] [--numstat] [--summary] [--check] [--index | --intent-to-add] [--3way]
12 +'git apply' [--stat] [--numstat] [--summary] [--check]
13 + [--index | --intent-to-add] [--3way] [--ours | --theirs | --union]
14 [--apply] [--no-add] [--build-fake-ancestor=<file>] [-R | --reverse]
15 [--allow-binary-replacement | --binary] [--reject] [-z]
16 [-p<n>] [-C<n>] [--inaccurate-eof] [--recount] [--cached]
@@ -92,6 +93,12 @@ OPTIONS
93 When used with the `--cached` option, any conflicts are left at higher stages
94 in the cache.
95
96 +--ours::
97 +--theirs::
98 +--union::
99 + Instead of leaving conflicts in the file, resolve conflicts favouring
100 + our (or their or both) side of the lines. Requires --3way.
101 +
102 --build-fake-ancestor=<file>::
103 Newer 'git diff' output has embedded 'index information'
104 for each blob to help identify the original version that
apply.c
+18 -2
@@ -3561,6 +3561,7 @@ static int three_way_merge(struct apply_state *state,
3561 const struct object_id *theirs)
3562 {
3563 mmfile_t base_file, our_file, their_file;
3564 + struct ll_merge_options merge_opts = LL_MERGE_OPTIONS_INIT;
3565 mmbuffer_t result = { NULL };
3566 enum ll_merge_result status;
3567
@@ -3573,12 +3574,13 @@ static int three_way_merge(struct apply_state *state,
3574 read_mmblob(&base_file, base);
3575 read_mmblob(&our_file, ours);
3576 read_mmblob(&their_file, theirs);
3577 + merge_opts.variant = state->merge_variant;
3578 status = ll_merge(&result, path,
3579 &base_file, "base",
3580 &our_file, "ours",
3581 &their_file, "theirs",
3582 state->repo->index,
3581 - NULL);
3583 + &merge_opts);
3584 if (status == LL_MERGE_BINARY_CONFLICT)
3585 warning("Cannot merge binary files: %s (%s vs. %s)",
3586 path, "ours", "theirs");
@@ -5151,6 +5153,15 @@ int apply_parse_options(int argc, const char **argv,
5153 N_("also apply the patch (use with --stat/--summary/--check)")),
5154 OPT_BOOL('3', "3way", &state->threeway,
5155 N_( "attempt three-way merge, fall back on normal patch if that fails")),
5156 + OPT_SET_INT_F(0, "ours", &state->merge_variant,
5157 + N_("for conflicts, use our version"),
5158 + XDL_MERGE_FAVOR_OURS, PARSE_OPT_NONEG),
5159 + OPT_SET_INT_F(0, "theirs", &state->merge_variant,
5160 + N_("for conflicts, use their version"),
5161 + XDL_MERGE_FAVOR_THEIRS, PARSE_OPT_NONEG),
5162 + OPT_SET_INT_F(0, "union", &state->merge_variant,
5163 + N_("for conflicts, use a union version"),
5164 + XDL_MERGE_FAVOR_UNION, PARSE_OPT_NONEG),
5165 OPT_FILENAME(0, "build-fake-ancestor", &state->fake_ancestor,
5166 N_("build a temporary index based on embedded index information")),
5167 /* Think twice before adding "--nul" synonym to this */
@@ -5190,5 +5201,10 @@ int apply_parse_options(int argc, const char **argv,
5201 OPT_END()
5202 };
5203
5193 - return parse_options(argc, argv, state->prefix, builtin_apply_options, apply_usage, 0);
5204 + argc = parse_options(argc, argv, state->prefix, builtin_apply_options, apply_usage, 0);
5205 +
5206 + if (state->merge_variant && !state->threeway)
5207 + die(_("--ours, --theirs, and --union require --3way"));
5208 +
5209 + return argc;
5210 }
apply.h
+1
@@ -59,6 +59,7 @@ struct apply_state {
59 struct repository *repo;
60 const char *index_file;
61 enum apply_verbosity apply_verbosity;
62 + int merge_variant;
63 char *fake_ancestor;
64 const char *patch_input_file;
65 int line_termination;
t/t4108-apply-threeway.sh
+40
@@ -82,6 +82,46 @@ test_expect_success 'apply with --3way with merge.conflictStyle = diff3' '
82 test_apply_with_3way
83 '
84
85 +test_apply_with_3way_favoritism () {
86 + apply_arg=$1
87 + merge_arg=$2
88 +
89 + # Merging side should be similar to applying this patch
90 + git diff ...side >P.diff &&
91 +
92 + # The corresponding conflicted merge
93 + git reset --hard &&
94 + git checkout main^0 &&
95 + git merge --no-commit $merge_arg side &&
96 + git ls-files -s >expect.ls &&
97 + print_sanitized_conflicted_diff >expect.diff &&
98 +
99 + # should apply successfully
100 + git reset --hard &&
101 + git checkout main^0 &&
102 + git apply --index --3way $apply_arg P.diff &&
103 + git ls-files -s >actual.ls &&
104 + print_sanitized_conflicted_diff >actual.diff &&
105 +
106 + # The result should resemble the corresponding merge
107 + test_cmp expect.ls actual.ls &&
108 + test_cmp expect.diff actual.diff
109 +}
110 +
111 +test_expect_success 'apply with --3way --ours' '
112 + test_apply_with_3way_favoritism --ours -Xours
113 +'
114 +
115 +test_expect_success 'apply with --3way --theirs' '
116 + test_apply_with_3way_favoritism --theirs -Xtheirs
117 +'
118 +
119 +test_expect_success 'apply with --3way --union' '
120 + echo "* merge=union" >.gitattributes &&
121 + test_apply_with_3way_favoritism --union &&
122 + rm .gitattributes
123 +'
124 +
125 test_expect_success 'apply with --3way with rerere enabled' '
126 test_config rerere.enabled true &&
127