Merge branch 'en/merge-tree-check'

"git merge-tree" learned an option to see if it resolves cleanly
without actually creating a result.

* en/merge-tree-check:
  merge-tree: add a new --quiet flag
  merge-ort: add a new mergeability_only option
This commit is contained in:
Junio C Hamano
2025-05-27 13:59:08 -07:00
5 changed files with 94 additions and 7 deletions

View File

@@ -65,6 +65,12 @@ OPTIONS
default is to include these messages if there are merge default is to include these messages if there are merge
conflicts, and to omit them otherwise. conflicts, and to omit them otherwise.
--quiet::
Disable all output from the program. Useful when you are only
interested in the exit status. Allows merge-tree to exit
early when it finds a conflict, and allows it to avoid writing
most objects created by merges.
--allow-unrelated-histories:: --allow-unrelated-histories::
merge-tree will by default error out if the two branches specified merge-tree will by default error out if the two branches specified
share no common history. This flag can be given to override that share no common history. This flag can be given to override that

View File

@@ -490,6 +490,9 @@ static int real_merge(struct merge_tree_options *o,
if (result.clean < 0) if (result.clean < 0)
die(_("failure to merge")); die(_("failure to merge"));
if (o->merge_options.mergeability_only)
goto cleanup;
if (show_messages == -1) if (show_messages == -1)
show_messages = !result.clean; show_messages = !result.clean;
@@ -522,6 +525,8 @@ static int real_merge(struct merge_tree_options *o,
} }
if (o->use_stdin) if (o->use_stdin)
putchar(line_termination); putchar(line_termination);
cleanup:
merge_finalize(&opt, &result); merge_finalize(&opt, &result);
clear_merge_options(&opt); clear_merge_options(&opt);
return !result.clean; /* result.clean < 0 handled above */ return !result.clean; /* result.clean < 0 handled above */
@@ -538,6 +543,7 @@ int cmd_merge_tree(int argc,
int original_argc; int original_argc;
const char *merge_base = NULL; const char *merge_base = NULL;
int ret; int ret;
int quiet = 0;
const char * const merge_tree_usage[] = { const char * const merge_tree_usage[] = {
N_("git merge-tree [--write-tree] [<options>] <branch1> <branch2>"), N_("git merge-tree [--write-tree] [<options>] <branch1> <branch2>"),
@@ -552,6 +558,10 @@ int cmd_merge_tree(int argc,
N_("do a trivial merge only"), MODE_TRIVIAL), N_("do a trivial merge only"), MODE_TRIVIAL),
OPT_BOOL(0, "messages", &o.show_messages, OPT_BOOL(0, "messages", &o.show_messages,
N_("also show informational/conflict messages")), N_("also show informational/conflict messages")),
OPT_BOOL_F(0, "quiet",
&quiet,
N_("suppress all output; only exit status wanted"),
PARSE_OPT_NONEG),
OPT_SET_INT('z', NULL, &line_termination, OPT_SET_INT('z', NULL, &line_termination,
N_("separate paths with the NUL character"), '\0'), N_("separate paths with the NUL character"), '\0'),
OPT_BOOL_F(0, "name-only", OPT_BOOL_F(0, "name-only",
@@ -583,6 +593,14 @@ int cmd_merge_tree(int argc,
argc = parse_options(argc, argv, prefix, mt_options, argc = parse_options(argc, argv, prefix, mt_options,
merge_tree_usage, PARSE_OPT_STOP_AT_NON_OPTION); merge_tree_usage, PARSE_OPT_STOP_AT_NON_OPTION);
if (quiet && o.show_messages == -1)
o.show_messages = 0;
o.merge_options.mergeability_only = quiet;
die_for_incompatible_opt2(quiet, "--quiet", o.show_messages, "--messages");
die_for_incompatible_opt2(quiet, "--quiet", o.name_only, "--name-only");
die_for_incompatible_opt2(quiet, "--quiet", o.use_stdin, "--stdin");
die_for_incompatible_opt2(quiet, "--quiet", !line_termination, "-z");
if (xopts.nr && o.mode == MODE_TRIVIAL) if (xopts.nr && o.mode == MODE_TRIVIAL)
die(_("--trivial-merge is incompatible with all other options")); die(_("--trivial-merge is incompatible with all other options"));
for (size_t x = 0; x < xopts.nr; x++) for (size_t x = 0; x < xopts.nr; x++)

View File

@@ -2127,6 +2127,7 @@ static int handle_content_merge(struct merge_options *opt,
const struct version_info *b, const struct version_info *b,
const char *pathnames[3], const char *pathnames[3],
const int extra_marker_size, const int extra_marker_size,
const int record_object,
struct version_info *result) struct version_info *result)
{ {
/* /*
@@ -2214,7 +2215,7 @@ static int handle_content_merge(struct merge_options *opt,
ret = -1; ret = -1;
} }
if (!ret && if (!ret && record_object &&
write_object_file(result_buf.ptr, result_buf.size, write_object_file(result_buf.ptr, result_buf.size,
OBJ_BLOB, &result->oid)) { OBJ_BLOB, &result->oid)) {
path_msg(opt, ERROR_OBJECT_WRITE_FAILED, 0, path_msg(opt, ERROR_OBJECT_WRITE_FAILED, 0,
@@ -2897,6 +2898,7 @@ static int process_renames(struct merge_options *opt,
struct version_info merged; struct version_info merged;
struct conflict_info *base, *side1, *side2; struct conflict_info *base, *side1, *side2;
unsigned was_binary_blob = 0; unsigned was_binary_blob = 0;
const int record_object = true;
pathnames[0] = oldpath; pathnames[0] = oldpath;
pathnames[1] = newpath; pathnames[1] = newpath;
@@ -2947,6 +2949,7 @@ static int process_renames(struct merge_options *opt,
&side2->stages[2], &side2->stages[2],
pathnames, pathnames,
1 + 2 * opt->priv->call_depth, 1 + 2 * opt->priv->call_depth,
record_object,
&merged); &merged);
if (clean_merge < 0) if (clean_merge < 0)
return -1; return -1;
@@ -3061,6 +3064,7 @@ static int process_renames(struct merge_options *opt,
struct conflict_info *base, *side1, *side2; struct conflict_info *base, *side1, *side2;
int clean; int clean;
const int record_object = true;
pathnames[0] = oldpath; pathnames[0] = oldpath;
pathnames[other_source_index] = oldpath; pathnames[other_source_index] = oldpath;
@@ -3080,6 +3084,7 @@ static int process_renames(struct merge_options *opt,
&side2->stages[2], &side2->stages[2],
pathnames, pathnames,
1 + 2 * opt->priv->call_depth, 1 + 2 * opt->priv->call_depth,
record_object,
&merged); &merged);
if (clean < 0) if (clean < 0)
return -1; return -1;
@@ -3931,9 +3936,12 @@ static int write_completed_directory(struct merge_options *opt,
* Write out the tree to the git object directory, and also * Write out the tree to the git object directory, and also
* record the mode and oid in dir_info->result. * record the mode and oid in dir_info->result.
*/ */
int record_tree = (!opt->mergeability_only ||
opt->priv->call_depth);
dir_info->is_null = 0; dir_info->is_null = 0;
dir_info->result.mode = S_IFDIR; dir_info->result.mode = S_IFDIR;
if (write_tree(&dir_info->result.oid, &info->versions, offset, if (record_tree &&
write_tree(&dir_info->result.oid, &info->versions, offset,
opt->repo->hash_algo->rawsz) < 0) opt->repo->hash_algo->rawsz) < 0)
ret = -1; ret = -1;
} }
@@ -4231,10 +4239,13 @@ static int process_entry(struct merge_options *opt,
struct version_info *o = &ci->stages[0]; struct version_info *o = &ci->stages[0];
struct version_info *a = &ci->stages[1]; struct version_info *a = &ci->stages[1];
struct version_info *b = &ci->stages[2]; struct version_info *b = &ci->stages[2];
int record_object = (!opt->mergeability_only ||
opt->priv->call_depth);
clean_merge = handle_content_merge(opt, path, o, a, b, clean_merge = handle_content_merge(opt, path, o, a, b,
ci->pathnames, ci->pathnames,
opt->priv->call_depth * 2, opt->priv->call_depth * 2,
record_object,
&merged_file); &merged_file);
if (clean_merge < 0) if (clean_merge < 0)
return -1; return -1;
@@ -4395,6 +4406,8 @@ static int process_entries(struct merge_options *opt,
STRING_LIST_INIT_NODUP, STRING_LIST_INIT_NODUP,
NULL, 0 }; NULL, 0 };
int ret = 0; int ret = 0;
const int record_tree = (!opt->mergeability_only ||
opt->priv->call_depth);
trace2_region_enter("merge", "process_entries setup", opt->repo); trace2_region_enter("merge", "process_entries setup", opt->repo);
if (strmap_empty(&opt->priv->paths)) { if (strmap_empty(&opt->priv->paths)) {
@@ -4454,6 +4467,12 @@ static int process_entries(struct merge_options *opt,
ret = -1; ret = -1;
goto cleanup; goto cleanup;
}; };
if (!ci->merged.clean && opt->mergeability_only &&
!opt->priv->call_depth) {
ret = 0;
goto cleanup;
}
} }
} }
trace2_region_leave("merge", "processing", opt->repo); trace2_region_leave("merge", "processing", opt->repo);
@@ -4468,7 +4487,8 @@ static int process_entries(struct merge_options *opt,
fflush(stdout); fflush(stdout);
BUG("dir_metadata accounting completely off; shouldn't happen"); BUG("dir_metadata accounting completely off; shouldn't happen");
} }
if (write_tree(result_oid, &dir_metadata.versions, 0, if (record_tree &&
write_tree(result_oid, &dir_metadata.versions, 0,
opt->repo->hash_algo->rawsz) < 0) opt->repo->hash_algo->rawsz) < 0)
ret = -1; ret = -1;
cleanup: cleanup:
@@ -4715,6 +4735,8 @@ void merge_display_update_messages(struct merge_options *opt,
if (opt->record_conflict_msgs_as_headers) if (opt->record_conflict_msgs_as_headers)
BUG("Either display conflict messages or record them as headers, not both"); BUG("Either display conflict messages or record them as headers, not both");
if (opt->mergeability_only)
BUG("Displaying conflict messages incompatible with mergeability-only checks");
trace2_region_enter("merge", "display messages", opt->repo); trace2_region_enter("merge", "display messages", opt->repo);
@@ -5171,10 +5193,12 @@ redo:
result->path_messages = &opt->priv->conflicts; result->path_messages = &opt->priv->conflicts;
if (result->clean >= 0) { if (result->clean >= 0) {
result->tree = parse_tree_indirect(&working_tree_oid); if (!opt->mergeability_only) {
if (!result->tree) result->tree = parse_tree_indirect(&working_tree_oid);
die(_("unable to read tree (%s)"), if (!result->tree)
oid_to_hex(&working_tree_oid)); die(_("unable to read tree (%s)"),
oid_to_hex(&working_tree_oid));
}
/* existence of conflicted entries implies unclean */ /* existence of conflicted entries implies unclean */
result->clean &= strmap_empty(&opt->priv->conflicted); result->clean &= strmap_empty(&opt->priv->conflicted);
} }

View File

@@ -83,6 +83,7 @@ struct merge_options {
/* miscellaneous control options */ /* miscellaneous control options */
const char *subtree_shift; const char *subtree_shift;
unsigned renormalize : 1; unsigned renormalize : 1;
unsigned mergeability_only : 1; /* exit early, write fewer objects */
unsigned record_conflict_msgs_as_headers : 1; unsigned record_conflict_msgs_as_headers : 1;
const char *msg_header_prefix; const char *msg_header_prefix;

View File

@@ -54,6 +54,25 @@ test_expect_success setup '
git commit -m first-commit git commit -m first-commit
' '
test_expect_success '--quiet on clean merge' '
# Get rid of loose objects to start with
git gc &&
echo "0 objects, 0 kilobytes" >expect &&
git count-objects >actual &&
test_cmp expect actual &&
# Ensure merge is successful (exit code of 0)
git merge-tree --write-tree --quiet side1 side3 >output &&
# Ensure there is no output
test_must_be_empty output &&
# Ensure no loose objects written (all new objects written would have
# been in "outer layer" of the merge)
git count-objects >actual &&
test_cmp expect actual
'
test_expect_success 'Clean merge' ' test_expect_success 'Clean merge' '
TREE_OID=$(git merge-tree --write-tree side1 side3) && TREE_OID=$(git merge-tree --write-tree side1 side3) &&
q_to_tab <<-EOF >expect && q_to_tab <<-EOF >expect &&
@@ -72,6 +91,25 @@ test_expect_success 'Failed merge without rename detection' '
grep "CONFLICT (modify/delete): numbers deleted" out grep "CONFLICT (modify/delete): numbers deleted" out
' '
test_expect_success '--quiet on conflicted merge' '
# Get rid of loose objects to start with
git gc &&
echo "0 objects, 0 kilobytes" >expect &&
git count-objects >actual &&
test_cmp expect actual &&
# Ensure merge has conflict
test_expect_code 1 git merge-tree --write-tree --quiet side1 side2 >output &&
# Ensure there is no output
test_must_be_empty output &&
# Ensure no loose objects written (all new objects written would have
# been in "outer layer" of the merge)
git count-objects >actual &&
test_cmp expect actual
'
test_expect_success 'Content merge and a few conflicts' ' test_expect_success 'Content merge and a few conflicts' '
git checkout side1^0 && git checkout side1^0 &&
test_must_fail git merge side2 && test_must_fail git merge side2 &&