Skip to content

merge-tree: add new --quiet option #1920

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions Documentation/git-merge-tree.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,12 @@ OPTIONS
default is to include these messages if there are merge
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

On the Git mailing list, "Kristoffer Haugsbakk" wrote (reply to this):

On Fri, May 16, 2025, at 22:04, Elijah Newren via GitGitGadget wrote:
> From: Elijah Newren <[email protected]>
> +	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");

I’ve been using git-merge-tree(1) for some scripting but only today
tried out `--stdin` for printing refs that conflict.

```
# Pipe in pairs
merge_pairs=$(mktemp)
tee $merge_pairs \
    | git merge-tree --stdin --no-messages \
    | tr '\0' '\n' \
    | grep --extended-regexp '^(1|0)$' \
    | paste -d' ' - $merge_pairs \
    | grep '^0' \
    | cut -d' ' -f2-
```

(Previously I called the command in a loop)

I could imagine a `--format` option to just keep one of the arguments,
which means the tee(1) (for cross-referencing the ref) and all the other
things are gone:

```
git merge-tree --format='%(if)%(conflicted)%(then)oid2%(end)' --stdin
```

(But imagined options aside)

`--stdin` is presumably for efficiency and `--quiet`/`--dry-run`
definitely is.  But `--quiet` can only be used in the mode where you can
only do a single merge, not in the `--stdin` batch mode.

`--quiet`/`--dry-run` with informational output (c.f. the above
die-for-incompatible) would “break” the documented output format since
conflicts haven’t been computed all the way and there are no OIDs for
successful merges.  But the user is opting into a new mode here, never
seen before.  Can’t they opt into a new informational mode where
`--stdin --quiet` can co-exist?  Then you can have dry-run batch mode.

-- 
Kristoffer Haugsbakk

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

On the Git mailing list, "Kristoffer Haugsbakk" wrote (reply to this):

On Sat, May 17, 2025, at 21:52, Kristoffer Haugsbakk wrote:
> Can’t they opt into a new informational mode where
> `--stdin --quiet` can co-exist?

Yes, I now immediately see the contradiction in the
literal text: “informational mode” and `--quiet`. 
But I shall not weigh in on the naming matter.

-- 
Kristoffer Haugsbakk

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
Expand Down
18 changes: 18 additions & 0 deletions builtin/merge-tree.c
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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 */
Expand All @@ -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] [<options>] <branch1> <branch2>"),
Expand All @@ -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",
Expand Down Expand Up @@ -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++)
Expand Down
38 changes: 31 additions & 7 deletions merge-ort.c
Original file line number Diff line number Diff line change
Expand Up @@ -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)
{
/*
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -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;
}
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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)) {
Expand Down Expand Up @@ -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);
Expand All @@ -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:
Expand Down Expand Up @@ -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);

Expand Down Expand Up @@ -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);
}
Expand Down
1 change: 1 addition & 0 deletions merge-ort.h
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down
38 changes: 38 additions & 0 deletions t/t4301-merge-tree-write-tree.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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 &&
Expand All @@ -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 &&
Expand Down
Loading