diff --git a/Documentation/git-merge-tree.adoc b/Documentation/git-merge-tree.adoc index cf0578f9b5e86d..f824eea61f1e06 100644 --- a/Documentation/git-merge-tree.adoc +++ b/Documentation/git-merge-tree.adoc @@ -65,6 +65,12 @@ OPTIONS default is to include these messages if there are merge 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:: merge-tree will by default error out if the two branches specified share no common history. This flag can be given to override that diff --git a/builtin/merge-tree.c b/builtin/merge-tree.c index 4aafa73c61559e..7f41665dfd7e67 100644 --- a/builtin/merge-tree.c +++ b/builtin/merge-tree.c @@ -490,6 +490,9 @@ static int real_merge(struct merge_tree_options *o, if (result.clean < 0) die(_("failure to merge")); + if (o->merge_options.mergeability_only) + goto cleanup; + if (show_messages == -1) show_messages = !result.clean; @@ -522,6 +525,8 @@ static int real_merge(struct merge_tree_options *o, } if (o->use_stdin) putchar(line_termination); + +cleanup: merge_finalize(&opt, &result); clear_merge_options(&opt); return !result.clean; /* result.clean < 0 handled above */ @@ -538,6 +543,7 @@ int cmd_merge_tree(int argc, int original_argc; const char *merge_base = NULL; int ret; + int quiet = 0; const char * const merge_tree_usage[] = { N_("git merge-tree [--write-tree] [] "), @@ -552,6 +558,10 @@ int cmd_merge_tree(int argc, N_("do a trivial merge only"), MODE_TRIVIAL), OPT_BOOL(0, "messages", &o.show_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, N_("separate paths with the NUL character"), '\0'), OPT_BOOL_F(0, "name-only", @@ -583,6 +593,14 @@ int cmd_merge_tree(int argc, argc = parse_options(argc, argv, prefix, mt_options, 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) die(_("--trivial-merge is incompatible with all other options")); for (size_t x = 0; x < xopts.nr; x++) diff --git a/merge-ort.c b/merge-ort.c index 77310a4a52c972..47b3d1730ece36 100644 --- a/merge-ort.c +++ b/merge-ort.c @@ -2127,6 +2127,7 @@ static int handle_content_merge(struct merge_options *opt, const struct version_info *b, const char *pathnames[3], const int extra_marker_size, + const int record_object, struct version_info *result) { /* @@ -2214,7 +2215,7 @@ static int handle_content_merge(struct merge_options *opt, ret = -1; } - if (!ret && + if (!ret && record_object && write_object_file(result_buf.ptr, result_buf.size, OBJ_BLOB, &result->oid)) { 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 conflict_info *base, *side1, *side2; unsigned was_binary_blob = 0; + const int record_object = true; pathnames[0] = oldpath; pathnames[1] = newpath; @@ -2947,6 +2949,7 @@ static int process_renames(struct merge_options *opt, &side2->stages[2], pathnames, 1 + 2 * opt->priv->call_depth, + record_object, &merged); if (clean_merge < 0) return -1; @@ -3061,6 +3064,7 @@ static int process_renames(struct merge_options *opt, struct conflict_info *base, *side1, *side2; int clean; + const int record_object = true; pathnames[0] = oldpath; pathnames[other_source_index] = oldpath; @@ -3080,6 +3084,7 @@ static int process_renames(struct merge_options *opt, &side2->stages[2], pathnames, 1 + 2 * opt->priv->call_depth, + record_object, &merged); if (clean < 0) 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 * 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->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) ret = -1; } @@ -4231,10 +4239,13 @@ static int process_entry(struct merge_options *opt, struct version_info *o = &ci->stages[0]; struct version_info *a = &ci->stages[1]; 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, ci->pathnames, opt->priv->call_depth * 2, + record_object, &merged_file); if (clean_merge < 0) return -1; @@ -4395,6 +4406,8 @@ static int process_entries(struct merge_options *opt, STRING_LIST_INIT_NODUP, NULL, 0 }; int ret = 0; + const int record_tree = (!opt->mergeability_only || + opt->priv->call_depth); trace2_region_enter("merge", "process_entries setup", opt->repo); if (strmap_empty(&opt->priv->paths)) { @@ -4454,6 +4467,12 @@ static int process_entries(struct merge_options *opt, ret = -1; goto cleanup; }; + if (!ci->merged.clean && opt->mergeability_only && + !opt->priv->call_depth) { + ret = 0; + goto cleanup; + } + } } trace2_region_leave("merge", "processing", opt->repo); @@ -4468,7 +4487,8 @@ static int process_entries(struct merge_options *opt, fflush(stdout); 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) ret = -1; cleanup: @@ -4715,6 +4735,8 @@ void merge_display_update_messages(struct merge_options *opt, if (opt->record_conflict_msgs_as_headers) 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); @@ -5171,10 +5193,12 @@ static void merge_ort_nonrecursive_internal(struct merge_options *opt, result->path_messages = &opt->priv->conflicts; if (result->clean >= 0) { - result->tree = parse_tree_indirect(&working_tree_oid); - if (!result->tree) - die(_("unable to read tree (%s)"), - oid_to_hex(&working_tree_oid)); + if (!opt->mergeability_only) { + result->tree = parse_tree_indirect(&working_tree_oid); + if (!result->tree) + die(_("unable to read tree (%s)"), + oid_to_hex(&working_tree_oid)); + } /* existence of conflicted entries implies unclean */ result->clean &= strmap_empty(&opt->priv->conflicted); } diff --git a/merge-ort.h b/merge-ort.h index 30750c03962f2c..6045579825da8b 100644 --- a/merge-ort.h +++ b/merge-ort.h @@ -83,6 +83,7 @@ struct merge_options { /* miscellaneous control options */ const char *subtree_shift; unsigned renormalize : 1; + unsigned mergeability_only : 1; /* exit early, write fewer objects */ unsigned record_conflict_msgs_as_headers : 1; const char *msg_header_prefix; diff --git a/t/t4301-merge-tree-write-tree.sh b/t/t4301-merge-tree-write-tree.sh index f9c5883a7f7cd6..6e117ee93c8b5b 100755 --- a/t/t4301-merge-tree-write-tree.sh +++ b/t/t4301-merge-tree-write-tree.sh @@ -54,6 +54,25 @@ test_expect_success setup ' 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' ' TREE_OID=$(git merge-tree --write-tree side1 side3) && q_to_tab <<-EOF >expect && @@ -72,6 +91,25 @@ test_expect_success 'Failed merge without rename detection' ' 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' ' git checkout side1^0 && test_must_fail git merge side2 &&