Skip to contentSkip to conversation

RFC: In-core git merge-tree ("Server side merges")

PATCH Superseded Elijah Newren via GitGitGadget 11 participants 240 messages Jan 22, 2022 – Jun 18, 2022 1 message not in the archive Documentation Page 2 of 3

lore
Share & tools
lore
#123 lore raw

Re: [PATCH v3 03/15] merge-tree: add option parsing and initial shell for real merge function

In reply to Elijah Newren
Quoted 33 lines · Elijah Newren
Quoted 3 lines · Ævar Arnfjörð Bjarmason

On Thu, Feb 03 2022, Elijah Newren wrote:

[...]

Quoted 19 lines
Quoted 9 lines

...also, even if we did require the --write-tree flag, we'd still have to look at argc. Since the option parsing handles both modes, someone could leave off --write-tree, but include a bunch of other options that only make sense with --write-tree. Individually checking the setting of every extra flag along with write_tree is a royal pain and I don't want to repeat that for each new option added. Simply checking argc allows you to provide an error message if the user does that.

Quoted 9 lines

(And I think it's sad that in Git we often forgot to warn and notify users of options that are only functional with certain other arguments; it makes it harder for users to figure out, and has in the past even made it harder for other developers to figure out what was meant and how things are to be used. I think I've seen multiple Git devs be confused over ls-files --directory and --no-empty-directory options, assuming they'd do something sensible for tracked files, when in fact those arguments are simply ignored because they are only modifiers for how untracked files are treated.)

Quoted 4 lines

There's a much simpler way to do what you're trying to do here which is to only parse --write-tree, and as soon as you have that pass off two one function or the other, and have those functions call parse_options().

But that makes --write-tree a mandatory argument when trying to use that mode, right? If so, that is not a simpler way to do what I'm trying to do at all; it breaks my intended usage.

Quoted 7 lines

--write-tree is a documentation-only construct that users should never have to pass.

Also, what happens if we remove the --trivial-merge flag and its whole mode after a sufficient deprecation period? Would the --write-tree parameter remain required in your model to select the only existing mode, simply due to us having gone through a transition period?

You can have your cake and eat it too by running parse_optionss() N number of times. Although perhaps in this case the end result isn't worth it.

I was hoping this could be a simpler case of a subcommand dispatch, and perhaps it can still be generalized to that.

If the "trivial" mode never takes options and always 3 argv elements, we could just run parse_options() for it with no options, after checking that we have 3 arguments, and none start with '-'.

But the below is a generalization of this I tried out just now, it passes all your tests, and means that whenever you add new options you don't need to keep saying "no, not with the trivial mode" for each one.

Basically we run parse_options() once with the full set of options, and save away argc/argv (note the lack of strvec_clear() there, that's a TODO memory leak).

Then we've got a o.mode, which along with argc is the *only* thing we pay attention to at that point.

Then we dispatch to the "trivial" or "write" functions, which do parse_options() again, this time with only their options.

It means that e.g. this now works as expected:

./git merge-tree --trivial-merge -z origin/{master,next,seen}
error: unknown switch `z'
usage: git merge-tree [--trivial-merge] <base-tree> <branch1> <branch2>

--trivial-merge do a trivial merge only

I.e. we error out, with your verison we'll just ignore the -z.

Your "--trivial-merge is incompatible with all other options" doesn't work as you expect, and is buggy whether you want to go this route or not, as the added tests show.

Basically it does nothing at all. Because if you add --foo we'll die before we get there, parse_options() will die for us.

But if you do --trivial-merge -z your argc/argv will be trimmed, because -z is a known option. So your check is doing nothing. Your tests also pass with this removal of the only option compatibily check on top:

	diff --git a/builtin/merge-tree.c b/builtin/merge-tree.c
	index 58c0ddc5a32..08f18d43334 100644
	--- a/builtin/merge-tree.c
	+++ b/builtin/merge-tree.c
	@@ -476,7 +476,6 @@ int cmd_merge_tree(int argc, const char **argv, const char *prefix)
	 {
	 	struct merge_tree_options o = { .show_messages = -1 };
	 	int expected_remaining_argc;
	-	int original_argc;
	 
	 	const char * const merge_tree_usage[] = {
	 		N_("git merge-tree [--write-tree] [<options>] <branch1> <branch2>"),
	@@ -505,7 +504,6 @@ int cmd_merge_tree(int argc, const char **argv, const char *prefix)
	 	};
	 
	 	/* Parse arguments */
	-	original_argc = argc;
	 	argc = parse_options(argc, argv, prefix, mt_options,
	 			     merge_tree_usage, PARSE_OPT_STOP_AT_NON_OPTION);
	 	if (o.mode) {
	@@ -517,8 +515,6 @@ int cmd_merge_tree(int argc, const char **argv, const char *prefix)
	 			usage_with_options(merge_tree_usage, mt_options);
	 		o.mode = (argc == 2 ? 'w' : 't');
	 	}
	-	if (o.mode == 't' && original_argc < argc)
	-		die(_("--trivial-merge is incompatible with all other options"));
	 
	 	/* Do the relevant type of merge */
	 	if (o.mode == 'w')

So, here it is in all its glory :) A bit nasty for sure, but IMO preferrable to an ever expanding list of "X isn't compatible with A".

builtin/merge-tree.c +100 -46
index 58c0ddc5a32..1d47912816d 100644
@@ -12,6 +12,7 @@
 #include "exec-cmd.h"
 #include "merge-blobs.h"
 #include "quote.h"
+#include "strvec.h"
 
 static int line_termination = '\n';
 
@@ -371,13 +372,66 @@ static void *get_tree_descriptor(struct repository *r,
 	return buf;
 }
 
-static int trivial_merge(const char *base,
-			 const char *branch1,
-			 const char *branch2)
+struct merge_tree_options {
+	int mode;
+	int allow_unrelated_histories;
+	int show_messages;
+	int exclude_modes_oids_stages;
+};
+
+#define BUILTIN_MERGE_TREE_USAGE_WRITE \
+		N_("git merge-tree [--write-tree] [<options>] <branch1> <branch2>")
+#define BUILTIN_MERGE_TREE_USAGE_TRIVIAL \
+		N_("git merge-tree [--trivial-merge] <base-tree> <branch1> <branch2>")
+
+#define BUILTIN_MERGE_TREE_OPT_CMDMODE_TRIVIAL \
+		OPT_CMDMODE(0, "trivial-merge", &o.mode, \
+			    N_("do a trivial merge only"), 't')
+
+#define BUILTIN_MERGE_TREE_OPT_CMDMODE_WRITE \
+		OPT_CMDMODE(0, "write-tree", &o.mode, \
+			    N_("do a real merge instead of a trivial merge"), \
+			    'w')
+
+#define BUILTIN_MERGE_TREE_OPT_WRITE \
+		BUILTIN_MERGE_TREE_OPT_CMDMODE_WRITE, \
+		OPT_BOOL(0, "messages", &o.show_messages, \
+			 N_("also show informational/conflict messages")), \
+		OPT_SET_INT('z', NULL, &line_termination, \
+			    N_("separate paths with the NUL character"), '\0'), \
+		OPT_BOOL_F('l', "exclude-modes-oids-stages", \
+			   &o.exclude_modes_oids_stages, \
+			   N_("list conflicted files without modes/oids/stages"), \
+			   PARSE_OPT_NONEG), \
+		OPT_BOOL_F(0, "allow-unrelated-histories", \
+			   &o.allow_unrelated_histories, \
+			   N_("allow merging unrelated histories"), \
+			   PARSE_OPT_NONEG)
+
+static int trivial_merge(int argc, const char **argv, const char *prefix)
 {
 	struct repository *r = the_repository;
 	struct tree_desc t[3];
 	void *buf1, *buf2, *buf3;
+	struct merge_tree_options o = { 0 };
+	const char * const usage[] = {
+		BUILTIN_MERGE_TREE_USAGE_TRIVIAL,
+		NULL,
+	};
+	struct option options[] = {
+		BUILTIN_MERGE_TREE_OPT_CMDMODE_TRIVIAL,
+		OPT_END()
+	};
+	const char *base, *branch1, *branch2;
+
+	argc = parse_options(argc, argv, prefix, options, usage,
+			     PARSE_OPT_STOP_AT_NON_OPTION);
+	if (argc != 3)
+		BUG("should have ensured remaining argc == 3 already! Got %d", argc);
+
+	base = argv[0];
+	branch1 = argv[1];
+	branch2 = argv[2];;
 
 	buf1 = get_tree_descriptor(r, t+0, base);
 	buf2 = get_tree_descriptor(r, t+1, branch1);
@@ -391,24 +445,34 @@ static int trivial_merge(const char *base,
 	return 0;
 }
 
-struct merge_tree_options {
-	int mode;
-	int allow_unrelated_histories;
-	int show_messages;
-	int exclude_modes_oids_stages;
-};
-
-static int real_merge(struct merge_tree_options *o,
-		      const char *branch1, const char *branch2,
-		      const char *prefix)
+static int real_merge(int argc, const char **argv, const char *prefix)
 {
 	struct commit *parent1, *parent2;
 	struct commit_list *common;
 	struct commit_list *merge_bases = NULL;
 	struct commit_list *j;
+	struct merge_tree_options o = { .show_messages = 1 };
 	struct merge_options opt;
 	struct merge_result result = { 0 };
 
+	const char * const usage[] = {
+		BUILTIN_MERGE_TREE_USAGE_WRITE,
+		NULL,
+	};
+	struct option options[] = {
+		BUILTIN_MERGE_TREE_OPT_CMDMODE_WRITE,
+		BUILTIN_MERGE_TREE_OPT_WRITE,
+		OPT_END()
+	};
+	const char *branch1, *branch2;
+
+	argc = parse_options(argc, argv, prefix, options, usage,
+			     PARSE_OPT_STOP_AT_NON_OPTION);
+	if (argc != 2)
+		BUG("should have ensured remaining argc == 2 already! Got %d", argc);
+	branch1 = argv[0];
+	branch2 = argv[1];
+
 	parent1 = get_merge_parent(branch1);
 	if (!parent1)
 		help_unknown_ref(branch1, "merge-tree",
@@ -431,7 +495,7 @@ static int real_merge(struct merge_tree_options *o,
 	 * merge_incore_recursive in merge-ort.h
 	 */
 	common = get_merge_bases(parent1, parent2);
-	if (!common && !o->allow_unrelated_histories)
+	if (!common && !o.allow_unrelated_histories)
 		die(_("refusing to merge unrelated histories"));
 	for (j = common; j; j = j->next)
 		commit_list_insert(j->item, &merge_bases);
@@ -440,8 +504,8 @@ static int real_merge(struct merge_tree_options *o,
 	if (result.clean < 0)
 		die(_("failure to merge"));
 
-	if (o->show_messages == -1)
-		o->show_messages = !result.clean;
+	if (o.show_messages == -1)
+		o.show_messages = !result.clean;
 
 	puts(oid_to_hex(&result.tree->object.oid));
 	if (!result.clean) {
@@ -453,7 +517,7 @@ static int real_merge(struct merge_tree_options *o,
 		for (i = 0; i < conflicted_files.nr; i++) {
 			const char *name = conflicted_files.items[i].string;
 			struct stage_info *c = conflicted_files.items[i].util;
-			if (!o->exclude_modes_oids_stages)
+			if (!o.exclude_modes_oids_stages)
 				printf("%06o %s %d\t",
 				       c->mode, oid_to_hex(&c->oid), c->stage);
 			else if (last && !strcmp(last, name))
@@ -464,7 +528,7 @@ static int real_merge(struct merge_tree_options *o,
 		}
 		string_list_clear(&conflicted_files, 1);
 	}
-	if (o->show_messages) {
+	if (o.show_messages) {
 		putchar(line_termination);
 		merge_display_update_messages(&opt, &result, stdout);
 	}
@@ -474,40 +538,32 @@ static int real_merge(struct merge_tree_options *o,
 
 int cmd_merge_tree(int argc, const char **argv, const char *prefix)
 {
-	struct merge_tree_options o = { .show_messages = -1 };
+	struct merge_tree_options o;
 	int expected_remaining_argc;
-	int original_argc;
-
+	int original_argc = argc;
+	struct strvec original_args = STRVEC_INIT;
 	const char * const merge_tree_usage[] = {
-		N_("git merge-tree [--write-tree] [<options>] <branch1> <branch2>"),
-		N_("git merge-tree [--trivial-merge] <base-tree> <branch1> <branch2>"),
+		BUILTIN_MERGE_TREE_USAGE_WRITE,
+		BUILTIN_MERGE_TREE_USAGE_TRIVIAL,
 		NULL
 	};
 	struct option mt_options[] = {
-		OPT_CMDMODE(0, "write-tree", &o.mode,
-			    N_("do a real merge instead of a trivial merge"),
-			    'w'),
-		OPT_CMDMODE(0, "trivial-merge", &o.mode,
-			    N_("do a trivial merge only"), 't'),
-		OPT_BOOL(0, "messages", &o.show_messages,
-			 N_("also show informational/conflict messages")),
-		OPT_SET_INT('z', NULL, &line_termination,
-			    N_("separate paths with the NUL character"), '\0'),
-		OPT_BOOL_F('l', "exclude-modes-oids-stages",
-			   &o.exclude_modes_oids_stages,
-			   N_("list conflicted files without modes/oids/stages"),
-			   PARSE_OPT_NONEG),
-		OPT_BOOL_F(0, "allow-unrelated-histories",
-			   &o.allow_unrelated_histories,
-			   N_("allow merging unrelated histories"),
-			   PARSE_OPT_NONEG),
+		BUILTIN_MERGE_TREE_OPT_CMDMODE_TRIVIAL,
+		BUILTIN_MERGE_TREE_OPT_CMDMODE_WRITE,
+		BUILTIN_MERGE_TREE_OPT_WRITE,
 		OPT_END()
 	};
 
+	/* We only care about deciding "o.mode" here */
+	o.mode = 0;
+	/*
+	 * We need our original argv, and
+	 * PARSE_OPT_KEEP_{ARGV0,UNKNOWN} would do the wrong thing
+	 */
+	strvec_pushv(&original_args, argv);
 	/* Parse arguments */
-	original_argc = argc;
 	argc = parse_options(argc, argv, prefix, mt_options,
-			     merge_tree_usage, PARSE_OPT_STOP_AT_NON_OPTION);
+			     merge_tree_usage, 0);
 	if (o.mode) {
 		expected_remaining_argc = (o.mode == 'w' ? 2 : 3);
 		if (argc != expected_remaining_argc)
@@ -517,12 +573,10 @@ int cmd_merge_tree(int argc, const char **argv, const char *prefix)
 			usage_with_options(merge_tree_usage, mt_options);
 		o.mode = (argc == 2 ? 'w' : 't');
 	}
-	if (o.mode == 't' && original_argc < argc)
-		die(_("--trivial-merge is incompatible with all other options"));
 
 	/* Do the relevant type of merge */
 	if (o.mode == 'w')
-		return real_merge(&o, argv[0], argv[1], prefix);
+		return real_merge(original_argc, original_args.v, prefix);
 	else
-		return trivial_merge(argv[0], argv[1], argv[2]);
+		return trivial_merge(original_argc, original_args.v, prefix);
 }
t/t4301-merge-tree-write-tree.sh +12 -0
index 4de089d976d..749bdb6862d 100755
@@ -92,6 +92,18 @@ test_expect_success 'Barf on too many arguments' '
 	grep "^usage: git merge-tree" expect
 '
 
+for opt in $(git merge-tree --git-completion-helper-all)
+do
+	if test $opt = "--trivial-merge" || test $opt = "--write-tree"
+	then
+		continue
+	fi
+
+	test_expect_success "usage: --trivial-merge is incompatible with $opt" '
+		test_expect_code 129 git merge-tree --trivial-merge $opt side1 side2 side3
+	'
+done
+
 test_expect_success 'test conflict notices and such' '
 	test_expect_code 1 git merge-tree --write-tree --exclude-modes-oids-stages side1 side2 >out &&
 	sed -e "s/[0-9a-f]\{40,\}/HASH/g" out >actual &&
#124 lore raw

Re: [PATCH v3 03/15] merge-tree: add option parsing and initial shell for real merge function

Quoted 3 lines · Ævar Arnfjörð Bjarmason

On Thu, Feb 03 2022, Elijah Newren wrote:

[...]

Quoted 51 lines
Quoted 11 lines

But that makes --write-tree a mandatory argument when trying to use that mode, right? If so, that is not a simpler way to do what I'm trying to do at all; it breaks my intended usage.

--write-tree is a documentation-only construct that users should never have to pass.

Also, what happens if we remove the --trivial-merge flag and its whole mode after a sufficient deprecation period? Would the --write-tree parameter remain required in your model to select the only existing mode, simply due to us having gone through a transition period?

You can have your cake and eat it too by running parse_optionss() N number of times. Although perhaps in this case the end result isn't worth it.

I was hoping this could be a simpler case of a subcommand dispatch, and perhaps it can still be generalized to that.

If the "trivial" mode never takes options and always 3 argv elements, we could just run parse_options() for it with no options, after checking that we have 3 arguments, and none start with '-'.

But the below is a generalization of this I tried out just now, it passes all your tests, and means that whenever you add new options you don't need to keep saying "no, not with the trivial mode" for each one.

Basically we run parse_options() once with the full set of options, and save away argc/argv (note the lack of strvec_clear() there, that's a TODO memory leak).

Then we've got a o.mode, which along with argc is the *only* thing we pay attention to at that point.

Then we dispatch to the "trivial" or "write" functions, which do parse_options() again, this time with only their options.

It means that e.g. this now works as expected:

./git merge-tree --trivial-merge -z origin/{master,next,seen}
error: unknown switch `z'
usage: git merge-tree [--trivial-merge] <base-tree> <branch1> <branch2>

--trivial-merge do a trivial merge only

I.e. we error out, with your verison we'll just ignore the -z.

Your "--trivial-merge is incompatible with all other options" doesn't work as you expect, and is buggy whether you want to go this route or not, as the added tests show.

Quoted 5 lines

Basically it does nothing at all. Because if you add --foo we'll die before we get there, parse_options() will die for us.

But if you do --trivial-merge -z your argc/argv will be trimmed, because -z is a known option. So your check is doing nothing.

Gah, good catch. How did I get the check reversed?? I know I tested it at one point. Anyway, this fixes it:

builtin/merge-tree.c +0 -0
index 58c0ddc5a3..fb25e3d10d 100644
@@ -505,19 +505,22 @@ int cmd_merge_tree(int argc, const char **argv, const cha

r *prefix)

        };

        /* Parse arguments */
-       original_argc = argc;
+       original_argc = argc - 1; /* ignoring program name, i.e. argv[0] */
        argc = parse_options(argc, argv, prefix, mt_options,
                             merge_tree_usage, PARSE_OPT_STOP_AT_NON_OPTION);
        if (o.mode) {
                expected_remaining_argc = (o.mode == 'w' ? 2 : 3);
                if (argc != expected_remaining_argc)
                        usage_with_options(merge_tree_usage, mt_options);
+               if (o.mode == 't')
+                       /* Removal of `--trivial-merge` is expected */
+                       original_argc_options--;
        } else {
                if (argc < 2 || argc > 3)
                        usage_with_options(merge_tree_usage, mt_options);
                o.mode = (argc == 2 ? 'w' : 't');
        }

- if (o.mode == 't' && original_argc < argc)
+ if (o.mode == 't' && argc < original_argc)
die(_("--trivial-merge is incompatible with all other
options"));

        /* Do the relevant type of merge */

and results in this error being shown when any other option is listed with --trivial-merge.

Quoted 2 lines

So, here it is in all its glory :) A bit nasty for sure, but IMO preferrable to an ever expanding list of "X isn't compatible with A".

Agreed...on both counts. :-)

Quoted 227 lines
builtin/merge-tree.c +98 -43
index 58c0ddc5a32..1d47912816d 100644
@@ -12,6 +12,7 @@
 #include "exec-cmd.h"
 #include "merge-blobs.h"
 #include "quote.h"
+#include "strvec.h"
 
 static int line_termination = '\n';
 
@@ -371,13 +372,66 @@ static void *get_tree_descriptor(struct repository *r,
        return buf;
 }
 
-static int trivial_merge(const char *base,
-                        const char *branch1,
-                        const char *branch2)
+struct merge_tree_options {
+       int mode;
+       int allow_unrelated_histories;
+       int show_messages;
+       int exclude_modes_oids_stages;
+};
+
+#define BUILTIN_MERGE_TREE_USAGE_WRITE \
+               N_("git merge-tree [--write-tree] [<options>] <branch1> <branch2>")
+#define BUILTIN_MERGE_TREE_USAGE_TRIVIAL \
+               N_("git merge-tree [--trivial-merge] <base-tree> <branch1> <branch2>")
+
+#define BUILTIN_MERGE_TREE_OPT_CMDMODE_TRIVIAL \
+               OPT_CMDMODE(0, "trivial-merge", &o.mode, \
+                           N_("do a trivial merge only"), 't')
+
+#define BUILTIN_MERGE_TREE_OPT_CMDMODE_WRITE \
+               OPT_CMDMODE(0, "write-tree", &o.mode, \
+                           N_("do a real merge instead of a trivial merge"), \
+                           'w')
+
+#define BUILTIN_MERGE_TREE_OPT_WRITE \
+               BUILTIN_MERGE_TREE_OPT_CMDMODE_WRITE, \
+               OPT_BOOL(0, "messages", &o.show_messages, \
+                        N_("also show informational/conflict messages")), \
+               OPT_SET_INT('z', NULL, &line_termination, \
+                           N_("separate paths with the NUL character"), '\0'), \
+               OPT_BOOL_F('l', "exclude-modes-oids-stages", \
+                          &o.exclude_modes_oids_stages, \
+                          N_("list conflicted files without modes/oids/stages"), \
+                          PARSE_OPT_NONEG), \
+               OPT_BOOL_F(0, "allow-unrelated-histories", \
+                          &o.allow_unrelated_histories, \
+                          N_("allow merging unrelated histories"), \
+                          PARSE_OPT_NONEG)
+
+static int trivial_merge(int argc, const char **argv, const char *prefix)
 {
        struct repository *r = the_repository;
        struct tree_desc t[3];
        void *buf1, *buf2, *buf3;
+       struct merge_tree_options o = { 0 };
+       const char * const usage[] = {
+               BUILTIN_MERGE_TREE_USAGE_TRIVIAL,
+               NULL,
+       };
+       struct option options[] = {
+               BUILTIN_MERGE_TREE_OPT_CMDMODE_TRIVIAL,
+               OPT_END()
+       };
+       const char *base, *branch1, *branch2;
+
+       argc = parse_options(argc, argv, prefix, options, usage,
+                            PARSE_OPT_STOP_AT_NON_OPTION);
+       if (argc != 3)
+               BUG("should have ensured remaining argc == 3 already! Got %d", argc);
+
+       base = argv[0];
+       branch1 = argv[1];
+       branch2 = argv[2];;
 
        buf1 = get_tree_descriptor(r, t+0, base);
        buf2 = get_tree_descriptor(r, t+1, branch1);
@@ -391,24 +445,34 @@ static int trivial_merge(const char *base,
        return 0;
 }
 
-struct merge_tree_options {
-       int mode;
-       int allow_unrelated_histories;
-       int show_messages;
-       int exclude_modes_oids_stages;
-};
-
-static int real_merge(struct merge_tree_options *o,
-                     const char *branch1, const char *branch2,
-                     const char *prefix)
+static int real_merge(int argc, const char **argv, const char *prefix)
 {
        struct commit *parent1, *parent2;
        struct commit_list *common;
        struct commit_list *merge_bases = NULL;
        struct commit_list *j;
+       struct merge_tree_options o = { .show_messages = 1 };
        struct merge_options opt;
        struct merge_result result = { 0 };
 
+       const char * const usage[] = {
+               BUILTIN_MERGE_TREE_USAGE_WRITE,
+               NULL,
+       };
+       struct option options[] = {
+               BUILTIN_MERGE_TREE_OPT_CMDMODE_WRITE,
+               BUILTIN_MERGE_TREE_OPT_WRITE,
+               OPT_END()
+       };
+       const char *branch1, *branch2;
+
+       argc = parse_options(argc, argv, prefix, options, usage,
+                            PARSE_OPT_STOP_AT_NON_OPTION);
+       if (argc != 2)
+               BUG("should have ensured remaining argc == 2 already! Got %d", argc);
+       branch1 = argv[0];
+       branch2 = argv[1];
+
        parent1 = get_merge_parent(branch1);
        if (!parent1)
                help_unknown_ref(branch1, "merge-tree",
@@ -431,7 +495,7 @@ static int real_merge(struct merge_tree_options *o,
         * merge_incore_recursive in merge-ort.h
         */
        common = get_merge_bases(parent1, parent2);
-       if (!common && !o->allow_unrelated_histories)
+       if (!common && !o.allow_unrelated_histories)
                die(_("refusing to merge unrelated histories"));
        for (j = common; j; j = j->next)
                commit_list_insert(j->item, &merge_bases);
@@ -440,8 +504,8 @@ static int real_merge(struct merge_tree_options *o,
        if (result.clean < 0)
                die(_("failure to merge"));
 
-       if (o->show_messages == -1)
-               o->show_messages = !result.clean;
+       if (o.show_messages == -1)
+               o.show_messages = !result.clean;
 
        puts(oid_to_hex(&result.tree->object.oid));
        if (!result.clean) {
@@ -453,7 +517,7 @@ static int real_merge(struct merge_tree_options *o,
                for (i = 0; i < conflicted_files.nr; i++) {
                        const char *name = conflicted_files.items[i].string;
                        struct stage_info *c = conflicted_files.items[i].util;
-                       if (!o->exclude_modes_oids_stages)
+                       if (!o.exclude_modes_oids_stages)
                                printf("%06o %s %d\t",
                                       c->mode, oid_to_hex(&c->oid), c->stage);
                        else if (last && !strcmp(last, name))
@@ -464,7 +528,7 @@ static int real_merge(struct merge_tree_options *o,
                }
                string_list_clear(&conflicted_files, 1);
        }
-       if (o->show_messages) {
+       if (o.show_messages) {
                putchar(line_termination);
                merge_display_update_messages(&opt, &result, stdout);
        }
@@ -474,40 +538,32 @@ static int real_merge(struct merge_tree_options *o,
 
 int cmd_merge_tree(int argc, const char **argv, const char *prefix)
 {
-       struct merge_tree_options o = { .show_messages = -1 };
+       struct merge_tree_options o;
        int expected_remaining_argc;
-       int original_argc;
-
+       int original_argc = argc;
+       struct strvec original_args = STRVEC_INIT;
        const char * const merge_tree_usage[] = {
-               N_("git merge-tree [--write-tree] [<options>] <branch1> <branch2>"),
-               N_("git merge-tree [--trivial-merge] <base-tree> <branch1> <branch2>"),
+               BUILTIN_MERGE_TREE_USAGE_WRITE,
+               BUILTIN_MERGE_TREE_USAGE_TRIVIAL,
                NULL
        };
        struct option mt_options[] = {
-               OPT_CMDMODE(0, "write-tree", &o.mode,
-                           N_("do a real merge instead of a trivial merge"),
-                           'w'),
-               OPT_CMDMODE(0, "trivial-merge", &o.mode,
-                           N_("do a trivial merge only"), 't'),
-               OPT_BOOL(0, "messages", &o.show_messages,
-                        N_("also show informational/conflict messages")),
-               OPT_SET_INT('z', NULL, &line_termination,
-                           N_("separate paths with the NUL character"), '\0'),
-               OPT_BOOL_F('l', "exclude-modes-oids-stages",
-                          &o.exclude_modes_oids_stages,
-                          N_("list conflicted files without modes/oids/stages"),
-                          PARSE_OPT_NONEG),
-               OPT_BOOL_F(0, "allow-unrelated-histories",
-                          &o.allow_unrelated_histories,
-                          N_("allow merging unrelated histories"),
-                          PARSE_OPT_NONEG),
+               BUILTIN_MERGE_TREE_OPT_CMDMODE_TRIVIAL,
+               BUILTIN_MERGE_TREE_OPT_CMDMODE_WRITE,
+               BUILTIN_MERGE_TREE_OPT_WRITE,
                OPT_END()
        };
 
+       /* We only care about deciding "o.mode" here */
+       o.mode = 0;
+       /*
+        * We need our original argv, and
+        * PARSE_OPT_KEEP_{ARGV0,UNKNOWN} would do the wrong thing
+        */
+       strvec_pushv(&original_args, argv);
        /* Parse arguments */
-       original_argc = argc;
        argc = parse_options(argc, argv, prefix, mt_options,
-                            merge_tree_usage, PARSE_OPT_STOP_AT_NON_OPTION);
+                            merge_tree_usage, 0);
        if (o.mode) {
                expected_remaining_argc = (o.mode == 'w' ? 2 : 3);
                if (argc != expected_remaining_argc)
@@ -517,12 +573,10 @@ int cmd_merge_tree(int argc, const char **argv, const char *prefix)
                        usage_with_options(merge_tree_usage, mt_options);
                o.mode = (argc == 2 ? 'w' : 't');
        }
-       if (o.mode == 't' && original_argc < argc)

builtin/merge-tree.c int cmd_merge_tree(int argc, const char **argv, const char *prefix) · line 582

Quoted 10 lines
builtin/merge-tree.c +2 -3
-               die(_("--trivial-merge is incompatible with all other options"));
 
        /* Do the relevant type of merge */
        if (o.mode == 'w')
-               return real_merge(&o, argv[0], argv[1], prefix);
+               return real_merge(original_argc, original_args.v, prefix);
        else
-               return trivial_merge(argv[0], argv[1], argv[2]);
+               return trivial_merge(original_argc, original_args.v, prefix);
 }

Yeah, that's waaay more complex than my code which basically just needs 6 lines to check the incompatibility; the basic patch for that part (going back to before this patch we're commenting on) was just:

+ original_argc = argc - 1; /* ignore program name, i.e. argv[0] */
...
+ if (o.mode == 't')
+ /* Removal of --trivial-merge is expected */
+ original_argc_options--;
...
+ if (o.mode == 't' && argc < original_argc)
+ die(_("--trivial-merge is incompatible with all other options"));

Quoted 9 lines
t/t4301-merge-tree-write-tree.sh +1 -0
index 4de089d976d..749bdb6862d 100755
@@ -92,6 +92,18 @@ test_expect_success 'Barf on too many arguments' '
        grep "^usage: git merge-tree" expect
 '
 
+for opt in $(git merge-tree --git-completion-helper-all)

t/t4301-merge-tree-write-tree.sh test_expect_success 'Barf on too many arguments' ' · line 105

Quoted 10 lines
t/t4301-merge-tree-write-tree.sh +10 -0
+do
+       if test $opt = "--trivial-merge" || test $opt = "--write-tree"
+       then
+               continue
+       fi
+
+       test_expect_success "usage: --trivial-merge is incompatible with $opt" '
+               test_expect_code 129 git merge-tree --trivial-merge $opt side1 side2 side3
+       '
+done

Yep, I was clearly missing an important testcase. Thanks for providing one and for pointing out the error in my patch! Very cool.

#125 lore raw

Re: [PATCH v3 03/15] merge-tree: add option parsing and initial shell for real merge function

In reply to Elijah Newren
Quoted 12 lines · Elijah Newren
Quoted 11 lines · Ævar Arnfjörð Bjarmason
Quoted 7 lines · Elijah Newren via GitGitGadget

From: Elijah Newren <[email protected]>

Let merge-tree accept a --write-tree parameter for choosing real merges instead of trivial merges, and accept an optional --trivial-merge option to get the traditional behavior. Note that these accept different numbers of arguments, though, so these names need not actually be used.

Quoted 8 lines
Quoted 6 lines

Maybe that ship has sailed, but just my 0.02: I thought this whole thing was much less confusing with your initial merge-tree-ort proposal at
https://lore.kernel.org/git/CABPp-BEeBpJoU4yXdfA6vRAYVAUbd2gRhEV6j4VEqoqcu=FGSw@mail.gmail.com/;
I.e. the end-state of merge-tree.c is that you end up reading largely unrelated code (various static functions only used by one side or another).

Christian's merge-tree-ort proposal?

Yes. I'm clearly getting the chronology wrong here. Sorry.

Quoted 57 lines
Quoted 46 lines

But maybe that's all water under the bridge etc, however...

Quoted 24 lines

int cmd_merge_tree(int argc, const char **argv, const char *prefix) {

builtin/merge-tree.c +19 -3
-     if (argc != 4)
-             usage(merge_tree_usage);
-     return trivial_merge(argc, argv);
+     struct merge_tree_options o = { 0 };
+     int expected_remaining_argc;
+
+     const char * const merge_tree_usage[] = {
+             N_("git merge-tree [--write-tree] <branch1> <branch2>"),
+             N_("git merge-tree [--trivial-merge] <base-tree> <branch1> <branch2>"),
+             NULL
+     };
+     struct option mt_options[] = {
+             OPT_CMDMODE(0, "write-tree", &o.mode,
+                         N_("do a real merge instead of a trivial merge"),
+                         'w'),
+             OPT_CMDMODE(0, "trivial-merge", &o.mode,
+                         N_("do a trivial merge only"), 't'),
+             OPT_END()
+     };
+
+     /* Parse arguments */
+     argc = parse_options(argc, argv, prefix, mt_options,

builtin/merge-tree.c static int trivial_merge(int argc, const char **argv) · line 428

Quoted 10 lines
builtin/merge-tree.c +10 -0
+                          merge_tree_usage, PARSE_OPT_STOP_AT_NON_OPTION);
+     if (o.mode) {
+             expected_remaining_argc = (o.mode == 'w' ? 2 : 3);
+             if (argc != expected_remaining_argc)
+                     usage_with_options(merge_tree_usage, mt_options);
+     } else {
+             if (argc < 2 || argc > 3)
+                     usage_with_options(merge_tree_usage, mt_options);
+             o.mode = (argc == 2 ? 'w' : 't');
+     }

Do we really need to make this interface more special-casey by auto-guessing based on argc what argument you want? I.e. instead of usage like:

        N_("git merge-tree [--write-tree] <branch1> <branch2>"),
        N_("git merge-tree [--trivial-merge] <base-tree> <branch1> <branch2>"),

Wouldn't it be simpler to just have the equivalent of:

Quoted 7 lines
        # old
        git merge-tree ...
        # new
        git merge-tree --new-thing ...

And not have to look at ... to figure out if we're dispatching to the new or old thing.

You seem to be focusing on code simplicity? Sure, that'd be simpler code, it'd just be a less useful feature.

Quoted 8 lines

I think passing --write-tree all the time would be an annoyance. I don't see why anyone would ever use the other mode. However, for as long as both exist in the manual, it makes the manual easier to explain to users, and example testcases more self-documenting by having the flag there. That's the sole purpose of the flag.

I'm never going to actually use it when I invoke it from the command line. And I suspect most would leave it off.

I think I mostly covered this in
https://lore.kernel.org/git/[email protected]/
in the side-thread.

I'm not opposed to them being in the same command, it just seems like more work for those reasons in every way.

E.g. the current manual page doesn't really describe the format the --trivial-merge emits, so we could do with a section on that, but then it would say "only for this option, not the other" etc.

#126 lore raw

Re: [PATCH v3 03/15] merge-tree: add option parsing and initial shell for real merge function

In reply to Elijah Newren via GitGitGadget
Quoted 91 lines · Elijah Newren via GitGitGadget

Let merge-tree accept a --write-tree parameter for choosing real merges instead of trivial merges, and accept an optional --trivial-merge option to get the traditional behavior. Note that these accept different numbers of arguments, though, so these names need not actually be used.

Note that real merges differ from trivial merges in that they handle:
- three way content merges
- recursive ancestor consolidation
- renames
- proper directory/file conflict handling
- etc.
Basically all the stuff you'd expect from git merge, just without updating the index and working tree. The initial shell added here does nothing more than die with "real merges are not yet implemented", but that will be fixed in subsequent commits.

---
 builtin/merge-tree.c | 61 +++++++++++++++++++++++++++++++++++++-------
 git.c                |  2 +-
 2 files changed, 53 insertions(+), 10 deletions(-)
builtin/merge-tree.c +23 -9
index 914ec960b7e..e98ec8a9f1d 100644
@@ -3,13 +3,12 @@
 #include "tree-walk.h"
 #include "xdiff-interface.h"
 #include "object-store.h"
+#include "parse-options.h"
 #include "repository.h"
 #include "blob.h"
 #include "exec-cmd.h"
 #include "merge-blobs.h"
 
-static const char merge_tree_usage[] = "git merge-tree <base-tree> <branch1> <branch2>";
-
 struct merge_list {
 	struct merge_list *next;
 	struct merge_list *link;	/* other stages for this object */
@@ -366,15 +365,17 @@ static void *get_tree_descriptor(struct repository *r,
 	return buf;
 }
 
-static int trivial_merge(int argc, const char **argv)
+static int trivial_merge(const char *base,
+			 const char *branch1,
+			 const char *branch2)
 {
 	struct repository *r = the_repository;
 	struct tree_desc t[3];
 	void *buf1, *buf2, *buf3;
 
-	buf1 = get_tree_descriptor(r, t+0, argv[1]);
-	buf2 = get_tree_descriptor(r, t+1, argv[2]);
-	buf3 = get_tree_descriptor(r, t+2, argv[3]);
+	buf1 = get_tree_descriptor(r, t+0, base);
+	buf2 = get_tree_descriptor(r, t+1, branch1);
+	buf3 = get_tree_descriptor(r, t+2, branch2);
 	trivial_merge_trees(t, "");
 	free(buf1);
 	free(buf2);
@@ -384,9 +385,51 @@ static int trivial_merge(int argc, const char **argv)
 	return 0;
 }
 
+struct merge_tree_options {
+	int mode;
+};
+
+static int real_merge(struct merge_tree_options *o,
+		      const char *branch1, const char *branch2)
+{
+	die(_("real merges are not yet implemented"));
+}
+
 int cmd_merge_tree(int argc, const char **argv, const char *prefix)
 {
-	if (argc != 4)
-		usage(merge_tree_usage);
-	return trivial_merge(argc, argv);
+	struct merge_tree_options o = { 0 };
+	int expected_remaining_argc;
+
+	const char * const merge_tree_usage[] = {
+		N_("git merge-tree [--write-tree] <branch1> <branch2>"),
+		N_("git merge-tree [--trivial-merge] <base-tree> <branch1> <branch2>"),

builtin/merge-tree.c static int trivial_merge(int argc, const char **argv) · line 415

Quoted 10 lines
builtin/merge-tree.c +10 -0
+		NULL
+	};
+	struct option mt_options[] = {
+		OPT_CMDMODE(0, "write-tree", &o.mode,
+			    N_("do a real merge instead of a trivial merge"),
+			    'w'),
+		OPT_CMDMODE(0, "trivial-merge", &o.mode,
+			    N_("do a trivial merge only"), 't'),
+		OPT_END()
+	};

In the review club last week I had mentioned I thought OPT_CMDMODE worked well with enums. I found some a reasonably nice example in builtin/replace.c:cmd_replace(), although I have some Opinions about the enum declaration placement there. Regardless, I think using an enum instead of a single character would make this more readable - otherwise I need to remember what 'w' means when I'm reasoning about how many args to expect below.

Quoted 10 lines
builtin/merge-tree.c +10 -0
+
+	/* Parse arguments */
+	argc = parse_options(argc, argv, prefix, mt_options,
+			     merge_tree_usage, PARSE_OPT_STOP_AT_NON_OPTION);
+	if (o.mode) {
+		expected_remaining_argc = (o.mode == 'w' ? 2 : 3);
+		if (argc != expected_remaining_argc)
+			usage_with_options(merge_tree_usage, mt_options);
+	} else {
+		if (argc < 2 || argc > 3)

builtin/merge-tree.c static int trivial_merge(int argc, const char **argv) · line 435

Quoted 10 lines
builtin/merge-tree.c +9 -0
+			usage_with_options(merge_tree_usage, mt_options);
+		o.mode = (argc == 2 ? 'w' : 't');
+	}
+
+	/* Do the relevant type of merge */
+	if (o.mode == 'w')
+		return real_merge(&o, argv[0], argv[1]);
+	else
+		return trivial_merge(argv[0], argv[1], argv[2]);
 }

Sorry for the slow reply.

- Emily

#127 lore raw

Re: [PATCH v3 03/15] merge-tree: add option parsing and initial shell for real merge function

Quoted 14 lines · Emily Shaffer

builtin/merge-tree.c static int trivial_merge(int argc, const char **argv) · line 415

Quoted 13 lines
builtin/merge-tree.c +13 -0
+	const char * const merge_tree_usage[] = {
+		N_("git merge-tree [--write-tree] <branch1> <branch2>"),
+		N_("git merge-tree [--trivial-merge] <base-tree> <branch1> <branch2>"),
+		NULL
+	};
+	struct option mt_options[] = {
+		OPT_CMDMODE(0, "write-tree", &o.mode,
+			    N_("do a real merge instead of a trivial merge"),
+			    'w'),
+		OPT_CMDMODE(0, "trivial-merge", &o.mode,
+			    N_("do a trivial merge only"), 't'),
+		OPT_END()
+	};
Quoted 7 lines

In the review club last week I had mentioned I thought OPT_CMDMODE worked well with enums. I found some a reasonably nice example in builtin/replace.c:cmd_replace(), although I have some Opinions about the enum declaration placement there. Regardless, I think using an enum instead of a single character would make this more readable - otherwise I need to remember what 'w' means when I'm reasoning about how many args to expect below.

I am reasonably sure whoever did the above use of OPT_CMDMODE() feature mimicked an existing one that use it to make options with a single-letter shorthand mutually exclusive. If the options are with short-hand, you wouldn't be complaining that you do not know what 'w' stands for. When using it with options without short-hand, like this case, I'd agree it would make it easier to read to use symbolic constants of some kind.

#128 lore raw

PATCH v3 4/15 merge-tree: implement real merges

In reply to Elijah Newren via GitGitGadget

From: Elijah Newren <[email protected]>

This adds the ability to perform real merges rather than just trivial merges (meaning handling three way content merges, recursive ancestor consolidation, renames, proper directory/file conflict handling, and so forth). However, unlike git merge, the working tree and index are left alone and no branch is updated.

The only output is:
- the toplevel resulting tree printed on stdout
- exit status of 0 (clean), 1 (conflicts present), anything else (merge could not be performed; unknown if clean or conflicted)

This output is meant to be used by some higher level script, perhaps in a sequence of steps like this:

   NEWTREE=$(git merge-tree --write-tree $BRANCH1 $BRANCH2)
   test $? -eq 0 || die "There were conflicts..."
   NEWCOMMIT=$(git commit-tree $NEWTREE -p $BRANCH1 -p $BRANCH2)
   git update-ref $BRANCH1 $NEWCOMMIT

Note that higher level scripts may also want to access the conflict/warning messages normally output during a merge, or have quick access to a list of files with conflicts. That is not available in this preliminary implementation, but subsequent commits will add that ability.

This also marks the traditional trivial merge of merge-tree as deprecated. The trivial merge not only had limited applicability, the output format was also difficult to work with (and its format undocumented), and will generally be less performant than real merges.

---
 Documentation/git-merge-tree.txt | 71 +++++++++++++++++++++-----
 builtin/merge-tree.c             | 44 +++++++++++++++-
 t/t4301-merge-tree-write-tree.sh | 88 ++++++++++++++++++++++++++++++++
 3 files changed, 190 insertions(+), 13 deletions(-)
 create mode 100755 t/t4301-merge-tree-write-tree.sh
Documentation/git-merge-tree.txt +59 -12
index 58731c19422..569485815a0 100644
@@ -3,26 +3,73 @@ git-merge-tree(1)
 
 NAME
 ----
-git-merge-tree - Show three-way merge without touching index
+git-merge-tree - Perform merge without touching index or working tree
 
 
 SYNOPSIS
 --------
 [verse]
-'git merge-tree' <base-tree> <branch1> <branch2>
+'git merge-tree' [--write-tree] <branch1> <branch2>
+'git merge-tree' [--trivial-merge] <base-tree> <branch1> <branch2> (deprecated)
 
 DESCRIPTION
 -----------
-Reads three tree-ish, and output trivial merge results and
-conflicting stages to the standard output.  This is similar to
-what three-way 'git read-tree -m' does, but instead of storing the
-results in the index, the command outputs the entries to the
-standard output.
-
-This is meant to be used by higher level scripts to compute
-merge results outside of the index, and stuff the results back into the
-index.  For this reason, the output from the command omits
-entries that match the <branch1> tree.
+
+Performs a merge, but does not make any new commits and does not read
+from or write to either the working tree or index.
+
+The second form is deprecated and supported only for backward
+compatibility.  It will likely be removed in the future, and will not
+be discussed further in this manual.
+
+The first form will merge the two branches, doing a real merge.  A real
+merge is distinguished from a trivial merge in that it includes:
+
+  * three way content merges of individual files
+  * rename detection
+  * proper directory/file conflict handling
+  * recursive ancestor consolidation (i.e. when there is more than one
+    merge base, creating a virtual merge base by merging the merge bases)
+  * etc.
+
+After the merge completes, it will create a new toplevel tree object.
+See `OUTPUT` below for details.
+
+OUTPUT
+------
+
+For either a successful or conflicted merge, the output from
+git-merge-tree is simply one line:
+
+	<OID of toplevel tree>
+
+The printed tree object corresponds to what would be checked out in
+the working tree at the end of `git merge`, and thus may have files
+with conflict markers in them.
+
+EXIT STATUS
+-----------
+
+For a successful, non-conflicted merge, the exit status is 0.  When the
+merge has conflicts, the exit status is 1.  If the merge is not able to
+complete (or start) due to some kind of error, the exit status is
+something other than 0 or 1.
+
+USAGE NOTES
+-----------
+
+git-merge-tree was written to be low-level plumbing, similar to
+hash-object, mktree, commit-tree, update-ref, and mktag.  Thus, it could
+be used as a part of a series of steps such as
+
+       NEWTREE=$(git merge-tree --write-tree $BRANCH1 $BRANCH2)
+       test $? -eq 0 || die "There were conflicts..."
+       NEWCOMMIT=$(git commit-tree $NEWTREE -p $BRANCH1 -p $BRANCH2)
+       git update-ref $BRANCH1 $NEWCOMMIT
+
+However, it does not quite fit into the same category of low-level
+plumbing commands since the possibility of merge conflicts give it a
+much higher chance of the command not succeeding.
 
 GIT
 ---
builtin/merge-tree.c +43 -1

56 lines not on this page · raw message

t/t4301-merge-tree-write-tree.sh new file +88 -0

88 lines not on this page · raw message

Signature · 1 line
gitgitgadget
#129 lore raw

Re: [PATCH v3 04/15] merge-tree: implement real merges

Quoted 26 lines · Elijah Newren via GitGitGadget
builtin/merge-tree.c +21 -1
@@ -392,7 +395,46 @@ struct merge_tree_options {
 static int real_merge(struct merge_tree_options *o,
 		      const char *branch1, const char *branch2)
 {
-	die(_("real merges are not yet implemented"));
+	struct commit *parent1, *parent2;
+	struct commit_list *common;
+	struct commit_list *merge_bases = NULL;
+	struct commit_list *j;
+	struct merge_options opt;
+	struct merge_result result = { 0 };
+
+	parent1 = get_merge_parent(branch1);
+	if (!parent1)
+		help_unknown_ref(branch1, "merge-tree",
+				 _("not something we can merge"));
+
+	parent2 = get_merge_parent(branch2);
+	if (!parent2)
+		help_unknown_ref(branch2, "merge-tree",
+				 _("not something we can merge"));
+
+	init_merge_options(&opt, the_repository);
+
+	opt.show_rename_progress = 0;
+

builtin/merge-tree.c struct merge_tree_options { · line 428

Quoted 10 lines
builtin/merge-tree.c +10 -0
+	opt.branch1 = branch1;
+	opt.branch2 = branch2;
+
+	/*
+	 * Get the merge bases, in reverse order; see comment above
+	 * merge_incore_recursive in merge-ort.h
+	 */
+	common = get_merge_bases(parent1, parent2);
+	if (!common)
+		die(_("refusing to merge unrelated histories"));

It appears to me that "merge-tree" in this mode, with the above code, cannot be used as a workhorse to implement server-side cherry-pick (or revert), which needs to allow the user to specify an arbitrary "common ancestor", instead of computing on its own.

To replay the change made by commit A on top of commit X (i.e. "cherry-pick A on X"), we have to be able to say "compute the three-way merge between A and X, pretending as if A^ were their common ancestor". The story is the same for revert---we compute three-way merge between A^ and X, pretending as if A were their common ancestor.

The above interface into this function, sadly, does not seem to allow such a request, unless I am missing something.

And if I am correct, it is a shame---after all, the point of the merge-trees command is to take three trees and run a three-way merge, and not being able to merge three "trees" and require "commits" makes this mode much less useful than its potential.

#130 lore raw

Re: [PATCH v3 04/15] merge-tree: implement real merges

Quoted 52 lines · Junio C Hamano
Quoted 26 lines · Elijah Newren via GitGitGadget
builtin/merge-tree.c +21 -1
@@ -392,7 +395,46 @@ struct merge_tree_options {
 static int real_merge(struct merge_tree_options *o,
                    const char *branch1, const char *branch2)
 {
-     die(_("real merges are not yet implemented"));
+     struct commit *parent1, *parent2;
+     struct commit_list *common;
+     struct commit_list *merge_bases = NULL;
+     struct commit_list *j;
+     struct merge_options opt;
+     struct merge_result result = { 0 };
+
+     parent1 = get_merge_parent(branch1);
+     if (!parent1)
+             help_unknown_ref(branch1, "merge-tree",
+                              _("not something we can merge"));
+
+     parent2 = get_merge_parent(branch2);
+     if (!parent2)
+             help_unknown_ref(branch2, "merge-tree",
+                              _("not something we can merge"));
+
+     init_merge_options(&opt, the_repository);
+
+     opt.show_rename_progress = 0;
+

builtin/merge-tree.c struct merge_tree_options { · line 428

Quoted 10 lines
builtin/merge-tree.c +10 -0
+     opt.branch1 = branch1;
+     opt.branch2 = branch2;
+
+     /*
+      * Get the merge bases, in reverse order; see comment above
+      * merge_incore_recursive in merge-ort.h
+      */
+     common = get_merge_bases(parent1, parent2);
+     if (!common)
+             die(_("refusing to merge unrelated histories"));

It appears to me that "merge-tree" in this mode, with the above code, cannot be used as a workhorse to implement server-side cherry-pick (or revert), which needs to allow the user to specify an arbitrary "common ancestor", instead of computing on its own.

To replay the change made by commit A on top of commit X (i.e. "cherry-pick A on X"), we have to be able to say "compute the three-way merge between A and X, pretending as if A^ were their common ancestor". The story is the same for revert---we compute three-way merge between A^ and X, pretending as if A were their common ancestor.

Quoted 7 lines

The above interface into this function, sadly, does not seem to allow such a request, unless I am missing something.

And if I am correct, it is a shame---after all, the point of the merge-trees command is to take three trees and run a three-way merge, and not being able to merge three "trees" and require "commits" makes this mode much less useful than its potential.

Yes, you are reading right. I think the cherry-pick/rebase replacement actually deserves a separate command from what merges should use; replaying a sequence of commits just has a number of UI differences and abilities that I think pull it in a different direction. I didn't want to wait and submit everything all at once (this series is long enough), and I figured that providing the in-core equivalent to git merge was a simpler first step worth submitting first before later providing the in-core equivalents to git rebase/git cherry-pick.

(Also, I'm a bit wary of providing a command meant to just do a single three-way merge with a defined merge-base, because that seems to be a path towards returning to a scripted rebase. Having a way to do only a single such special merge is fine but we should avoid encouraging people to go down that route.)

#131 lore raw

Re: [PATCH v3 04/15] merge-tree: implement real merges

Quoted 5 lines · Elijah Newren

Yes, you are reading right. I think the cherry-pick/rebase replacement actually deserves a separate command from what merges should use; replaying a sequence of commits just has a number of UI differences and abilities that I think pull it in a different direction.

I completely disagree. Each individual step in a sequence of replaying commits in order (or in reverse order) should be scriptable as a single merge-tree that takes "apply the change to go from A^ to A on X". Sequencing and placing UI around it is a job for the script that drives merge-tree.

#132 lore raw

Re: [PATCH v3 04/15] merge-tree: implement real merges

Quoted 9 lines · Junio C Hamano
Quoted 5 lines · Elijah Newren

Yes, you are reading right. I think the cherry-pick/rebase replacement actually deserves a separate command from what merges should use; replaying a sequence of commits just has a number of UI differences and abilities that I think pull it in a different direction.

Quoted 5 lines

I completely disagree. Each individual step in a sequence of replaying commits in order (or in reverse order) should be scriptable as a single merge-tree that takes "apply the change to go from A^ to A on X". Sequencing and placing UI around it is a job for the script that drives merge-tree.

Adding such an ability to merge-tree would be trivial -- it basically involves just two things: (1) accepting one extra argument, and (2) calling merge_incore_nonrecursive() instead of merge_incore_recursive().

However, I think forking a subprocess for every merge of a series of commits is a completely unreasonable overhead, so even if we provide such an option to merge-tree, I still want a separate plumbing-ish tool that does non-worktree/non-index replaying of commits which is not written as a driver of merge-tree. That other tool should just call merge_incore_nonrecursive() directly. And such a tool, since it should handle an arbitrary number of commits, should certainly be able to handle just one commit. From that angle, it feels like adding another mode to merge-tree would just be a partial duplication of the other tool.

However, if the other tool doesn't obviate the need for this additional mode (perhaps it ends up being forced to be too porcelain-ish insteading of plumbing-ish?), or folks really just want another merge-tree mode, I'm happy to add one along with the tool I submit later. Does that sound reasonable to you, or is there something you're still objecting to that I've missed?

#133 lore raw

Re: [PATCH v3 04/15] merge-tree: implement real merges

Quoted 21 lines · Elijah Newren
Quoted 9 lines · Junio C Hamano
Quoted 5 lines · Elijah Newren

Yes, you are reading right. I think the cherry-pick/rebase replacement actually deserves a separate command from what merges should use; replaying a sequence of commits just has a number of UI differences and abilities that I think pull it in a different direction.

Quoted 5 lines

I completely disagree. Each individual step in a sequence of replaying commits in order (or in reverse order) should be scriptable as a single merge-tree that takes "apply the change to go from A^ to A on X". Sequencing and placing UI around it is a job for the script that drives merge-tree.

Adding such an ability to merge-tree would be trivial -- it basically involves just two things: (1) accepting one extra argument, and (2) calling merge_incore_nonrecursive() instead of merge_incore_recursive().

Quoted 10 lines

However, I think forking a subprocess for every merge of a series of commits is a completely unreasonable overhead, so even if we provide such an option to merge-tree, I still want a separate plumbing-ish tool that does non-worktree/non-index replaying of commits which is not written as a driver of merge-tree. That other tool should just call merge_incore_nonrecursive() directly. And such a tool, since it should handle an arbitrary number of commits, should certainly be able to handle just one commit. From that angle, it feels like adding another mode to merge-tree would just be a partial duplication of the other tool.

I wonder how the UI of a tool that does non-worktree/non-index cherry-picks will look like. I'd expect it to produce the same output as merge-tree, except cherry-pick should probably output a commit OID, not a tree.

Maybe we want a unified command that produces commits from any sequence of merge/cherry-pick/revert/reword steps. The obvious UI would use something like the rebase-todo list as input. For example:

	$ echo '
	pick commit1
	reword commit2	# edit commit message in $GIT_EDITOR
	merge commit3 -m "log message"
	' | git create-commit commit0
	<OID of final commit>

we start from commit0 and apply steps one-by-one. Obviously, one unsolved problem is how to pass parameters like commit messages if no editor should be invoked (my sketch uses -m).
If any of the steps fails when merging merge, then we get the tree with conflicts

	$ echo '
	pick commit1
	pick commit2
	pick commit-that-does-not-apply
	' | git create-commit commit0
	<OID of commit after step 2>
	<OID of toplevel tree after failed merge>
	<Conflicted file info>
	<Informational messages>

Replaying a series of commits might look like this:

	$ echo 'pick commit1 ^commit0' | git create-commit new-base

I'm concluding that this is a difficult UI problem, and having a merge-tree command that accepts a "common ancestor" parameter could make it easier to experiment. Of course that depends on who is experimenting.

Quoted 7 lines

However, if the other tool doesn't obviate the need for this additional mode (perhaps it ends up being forced to be too porcelain-ish insteading of plumbing-ish?), or folks really just want another merge-tree mode, I'm happy to add one along with the tool I submit later. Does that sound reasonable to you, or is there something you're still objecting to that I've missed?

#134 lore raw

Re: [PATCH v3 04/15] merge-tree: implement real merges

Hi,

Quoted 65 lines · Johannes Altmanninger
Quoted 21 lines · Elijah Newren
Quoted 9 lines · Junio C Hamano
Quoted 5 lines · Elijah Newren

Yes, you are reading right. I think the cherry-pick/rebase replacement actually deserves a separate command from what merges should use; replaying a sequence of commits just has a number of UI differences and abilities that I think pull it in a different direction.

Quoted 5 lines

I completely disagree. Each individual step in a sequence of replaying commits in order (or in reverse order) should be scriptable as a single merge-tree that takes "apply the change to go from A^ to A on X". Sequencing and placing UI around it is a job for the script that drives merge-tree.

Adding such an ability to merge-tree would be trivial -- it basically involves just two things: (1) accepting one extra argument, and (2) calling merge_incore_nonrecursive() instead of merge_incore_recursive().

Quoted 10 lines

However, I think forking a subprocess for every merge of a series of commits is a completely unreasonable overhead, so even if we provide such an option to merge-tree, I still want a separate plumbing-ish tool that does non-worktree/non-index replaying of commits which is not written as a driver of merge-tree. That other tool should just call merge_incore_nonrecursive() directly. And such a tool, since it should handle an arbitrary number of commits, should certainly be able to handle just one commit. From that angle, it feels like adding another mode to merge-tree would just be a partial duplication of the other tool.

I wonder how the UI of a tool that does non-worktree/non-index cherry-picks will look like. I'd expect it to produce the same output as merge-tree, except cherry-pick should probably output a commit OID, not a tree.

Maybe we want a unified command that produces commits from any sequence of merge/cherry-pick/revert/reword steps. The obvious UI would use something like the rebase-todo list as input. For example:

        $ echo '
        pick commit1
        reword commit2  # edit commit message in $GIT_EDITOR
        merge commit3 -m "log message"
        ' | git create-commit commit0
        <OID of final commit>

we start from commit0 and apply steps one-by-one. Obviously, one unsolved problem is how to pass parameters like commit messages if no editor should be invoked (my sketch uses -m).
If any of the steps fails when merging merge, then we get the tree with conflicts

        $ echo '
        pick commit1
        pick commit2
        pick commit-that-does-not-apply
        ' | git create-commit commit0
        <OID of commit after step 2>
        <OID of toplevel tree after failed merge>
        <Conflicted file info>
        <Informational messages>
Quoted 5 lines

Replaying a series of commits might look like this:

        $ echo 'pick commit1 ^commit0' | git create-commit new-base

I'm concluding that this is a difficult UI problem

I agree. I've got a lot of thoughts on it, and some work in progress towards it (https://github.com/newren/git/tree/replay -- _very_ hacky, not even close to alpha quality, lots of fixup commits, todo comments, random brain dump files added to the tree, based on a previous round of this patch series, not updated for weeks, etc., etc.)

Quoted 3 lines

and having a merge-tree
command that accepts a "common ancestor" parameter could make it easier to experiment. Of course that depends on who is experimenting.

I think that would result in experiments and eventually full-blown scripts designed around forking subprocesses for every merge, and pushes us back into the world of having a scripted-rebase again. Yes, I know people can transliterate shell back to C; it seems to always be done as a half-way measure with the forking just being done from C or have other UI-warts guided by the shell design. In fact, *that* was the primary reason for me not providing a merge-tree option based on merge_incore_nonrecursive(), despite how trivial it'd be to provide it. If someone wanted a merge_incore_nonrecursive() mode for merge-tree for reasons other than attempting to build a rebase/cherry-pick replacement based on it, then I'd be much happier to provide it.

If someone wants to experiment with what a plumbing-ish rebase/cherry-pick would look like, the _right_ way to do it would be making using of merge_incore_nonrecursive() directly. If they want example code, I already provided some a year and a half ago and got it merged into git.git in the form of t/helper/test-fast-rebase.c. My "replay" branch is based on that code, but (a) moves it from t/helper to a real builtin, (b) removes the hardcoded very strict input, (c) removes the line of code doing the index & working tree updates, and
(d) modifies the output to be a more plumbing-ish style.

We'll certainly have discussions on what that should look like. But a plumbing-ish replacement for merge was much simpler, and made sense to do first. I would prefer to concentrate on getting that hammered down first. Then I'll start discussions on a plumbing-ish rebase/cherry-pick. And if that doesn't fulfill all the needs that folks think they want out of merge-tree, then we can add a merge_incore_nonrecursive()-based mode to merge-tree. It's all coming, but having fought transliterations-of-scripts in merge-recursive.c, sequencer.c, stash.c, rebase.c, etc. for years I really, really don't want any more of that. Let's end that insanity.

#135 lore raw

Re: [PATCH v3 04/15] merge-tree: implement real merges

Hi,

Quoted 70 lines · Elijah Newren
Quoted 69 lines · Johannes Altmanninger
Quoted 21 lines · Elijah Newren
Quoted 9 lines · Junio C Hamano
Quoted 5 lines · Elijah Newren

Yes, you are reading right. I think the cherry-pick/rebase replacement actually deserves a separate command from what merges should use; replaying a sequence of commits just has a number of UI differences and abilities that I think pull it in a different direction.

Quoted 5 lines

I completely disagree. Each individual step in a sequence of replaying commits in order (or in reverse order) should be scriptable as a single merge-tree that takes "apply the change to go from A^ to A on X". Sequencing and placing UI around it is a job for the script that drives merge-tree.

Adding such an ability to merge-tree would be trivial -- it basically involves just two things: (1) accepting one extra argument, and (2) calling merge_incore_nonrecursive() instead of merge_incore_recursive().

Quoted 10 lines

However, I think forking a subprocess for every merge of a series of commits is a completely unreasonable overhead, so even if we provide such an option to merge-tree, I still want a separate plumbing-ish tool that does non-worktree/non-index replaying of commits which is not written as a driver of merge-tree. That other tool should just call merge_incore_nonrecursive() directly. And such a tool, since it should handle an arbitrary number of commits, should certainly be able to handle just one commit. From that angle, it feels like adding another mode to merge-tree would just be a partial duplication of the other tool.

I wonder how the UI of a tool that does non-worktree/non-index cherry-picks will look like. I'd expect it to produce the same output as merge-tree, except cherry-pick should probably output a commit OID, not a tree.

Maybe we want a unified command that produces commits from any sequence of merge/cherry-pick/revert/reword steps. The obvious UI would use something like the rebase-todo list as input. For example:

        $ echo '
        pick commit1
        reword commit2  # edit commit message in $GIT_EDITOR
        merge commit3 -m "log message"
        ' | git create-commit commit0
        <OID of final commit>

we start from commit0 and apply steps one-by-one. Obviously, one unsolved problem is how to pass parameters like commit messages if no editor should be invoked (my sketch uses -m).
If any of the steps fails when merging merge, then we get the tree with conflicts

        $ echo '
        pick commit1
        pick commit2
        pick commit-that-does-not-apply
        ' | git create-commit commit0
        <OID of commit after step 2>
        <OID of toplevel tree after failed merge>
        <Conflicted file info>
        <Informational messages>

Replaying a series of commits might look like this:

        $ echo 'pick commit1 ^commit0' | git create-commit new-base
Quoted 7 lines
Quoted 1 line

I'm concluding that this is a difficult UI problem

I agree. I've got a lot of thoughts on it, and some work in progress towards it (https://github.com/newren/git/tree/replay -- _very_ hacky, not even close to alpha quality, lots of fixup commits, todo comments, random brain dump files added to the tree, based on a previous round of this patch series, not updated for weeks, etc., etc.)

Just chiming in that I find that very exciting. But it's a tangent, and slightly distracting from the topic at hand, so I would like to ask to focus back on server-side merges.

Quoted 17 lines
Quoted 3 lines

and having a merge-tree command that accepts a "common ancestor" parameter could make it easier to experiment. Of course that depends on who is experimenting.

I think that would result in experiments and eventually full-blown scripts designed around forking subprocesses for every merge, and pushes us back into the world of having a scripted-rebase again. Yes, I know people can transliterate shell back to C; it seems to always be done as a half-way measure with the forking just being done from C or have other UI-warts guided by the shell design. In fact, *that* was the primary reason for me not providing a merge-tree option based on merge_incore_nonrecursive(), despite how trivial it'd be to provide it. If someone wanted a merge_incore_nonrecursive() mode for merge-tree for reasons other than attempting to build a rebase/cherry-pick replacement based on it, then I'd be much happier to provide it.

Quoted 9 lines

If someone wants to experiment with what a plumbing-ish rebase/cherry-pick would look like, the _right_ way to do it would be making using of merge_incore_nonrecursive() directly. If they want example code, I already provided some a year and a half ago and got it merged into git.git in the form of t/helper/test-fast-rebase.c. My "replay" branch is based on that code, but (a) moves it from t/helper to a real builtin, (b) removes the hardcoded very strict input, (c) removes the line of code doing the index & working tree updates, and
(d) modifies the output to be a more plumbing-ish style.

I actually implemented that so I could provide apples-to-apples speed comparisons between libgit2 and merge-ort:

-- snip --
From 6a865c691810b67dc15ddb57ad110bd6fdfc2f12 Mon Sep 17 00:00:00 2001
From: Johannes Schindelin <[email protected]>
Date: Fri, 28 Jan 2022 23:28:20 +0100
Subject: [PATCH] merge-tree: optionally force a simple 3-way merge

---
 builtin/merge-tree.c | 72 ++++++++++++++++++++++++++++++--------------
 1 file changed, 50 insertions(+), 22 deletions(-)
builtin/merge-tree.c +50 -22
index 58c0ddc5a3..1007aaaede 100644
@@ -396,6 +396,7 @@ struct merge_tree_options {
 	int allow_unrelated_histories;
 	int show_messages;
 	int exclude_modes_oids_stages;
+	const char *nonrecursive_base;
 };
 
 static int real_merge(struct merge_tree_options *o,
@@ -409,34 +410,58 @@ static int real_merge(struct merge_tree_options *o,
 	struct merge_options opt;
 	struct merge_result result = { 0 };
 
-	parent1 = get_merge_parent(branch1);
-	if (!parent1)
-		help_unknown_ref(branch1, "merge-tree",
-				 _("not something we can merge"));
-
-	parent2 = get_merge_parent(branch2);
-	if (!parent2)
-		help_unknown_ref(branch2, "merge-tree",
-				 _("not something we can merge"));
-
 	init_merge_options(&opt, the_repository);
 
 	opt.show_rename_progress = 0;
 
-	opt.branch1 = branch1;
-	opt.branch2 = branch2;
+	if (o->nonrecursive_base) {
+		struct object_id base_oid, head_oid, merge_oid;
+		struct tree *base_tree, *head_tree, *merge_tree;
+
+		opt.ancestor = "(base)";
+		opt.branch1 = "(branch1)";
+		opt.branch2 = "(branch2)";
+
+		if (get_oid_treeish(o->nonrecursive_base, &base_oid))
+			die("could not parse base '%s'", o->nonrecursive_base);
+		base_tree = parse_tree_indirect(&base_oid);
+		if (get_oid_treeish(branch1, &head_oid))
+			die("could not parse head '%s'", branch1);
+		head_tree = parse_tree_indirect(&head_oid);
+		if (get_oid_treeish(branch2, &merge_oid))
+			die("could not parse merge '%s'", branch2);
+		merge_tree = parse_tree_indirect(&merge_oid);
+
+		merge_incore_nonrecursive(&opt,
+					  base_tree, head_tree, merge_tree,
+					  &result);
+	} else {
+		parent1 = get_merge_parent(branch1);
+		if (!parent1)
+			help_unknown_ref(branch1, "merge-tree",
+					 _("not something we can merge"));
+
+		parent2 = get_merge_parent(branch2);
+		if (!parent2)
+			help_unknown_ref(branch2, "merge-tree",
+					 _("not something we can merge"));
+
+		opt.branch1 = branch1;
+		opt.branch2 = branch2;
 
-	/*
-	 * Get the merge bases, in reverse order; see comment above
-	 * merge_incore_recursive in merge-ort.h
-	 */
-	common = get_merge_bases(parent1, parent2);
-	if (!common && !o->allow_unrelated_histories)
-		die(_("refusing to merge unrelated histories"));
-	for (j = common; j; j = j->next)
-		commit_list_insert(j->item, &merge_bases);
+		/*
+		 * Get the merge bases, in reverse order; see comment above
+		 * merge_incore_recursive in merge-ort.h
+		 */
+		common = get_merge_bases(parent1, parent2);
+		if (!common && !o->allow_unrelated_histories)
+			die(_("refusing to merge unrelated histories"));
+		for (j = common; j; j = j->next)
+			commit_list_insert(j->item, &merge_bases);
+
+		merge_incore_recursive(&opt, merge_bases, parent1, parent2, &result);
+	}
 
-	merge_incore_recursive(&opt, merge_bases, parent1, parent2, &result);
 	if (result.clean < 0)
 		die(_("failure to merge"));
 
@@ -501,6 +526,9 @@ int cmd_merge_tree(int argc, const char **argv, const char *prefix)
 			   &o.allow_unrelated_histories,
 			   N_("allow merging unrelated histories"),
 			   PARSE_OPT_NONEG),
+		OPT_STRING(0, "force-non-recursive-base", &o.nonrecursive_base,
+			   N_("base-tree"),
+			   N_("force a simple three-way merge")),
 		OPT_END()
 	};
 

-- snap --

I do strongly agree that this should _not_ enter core Git's code, I just provide this in case someone else wants to play with merge-ort on the server side in an existing code base.

Quoted 10 lines

We'll certainly have discussions on what that should look like. But a plumbing-ish replacement for merge was much simpler, and made sense to do first. I would prefer to concentrate on getting that hammered down first. Then I'll start discussions on a plumbing-ish rebase/cherry-pick. And if that doesn't fulfill all the needs that folks think they want out of merge-tree, then we can add a merge_incore_nonrecursive()-based mode to merge-tree. It's all coming, but having fought transliterations-of-scripts in merge-recursive.c, sequencer.c, stash.c, rebase.c, etc. for years I really, really don't want any more of that. Let's end that insanity.

Being the driving force behind many a "built-in-ification" of scripted commands, I wholeheartedly agree. You can still see the fall-out of designing commands in a scripted fashion, without any way to represent data structures other than strings. I wish we had come up with a better design to prototype commands than to write shell scripts. But I have to admit that even I do not have any better idea than to work on a proper API for libgit.a (which has historically invariably seen push-back from Junio).

While I agree that this discussion is a valuable one, right now I would like to focus on getting the server-side merges done, and once that has happened, move on to the replay/sequencer/API discussion (which will probably be a big one, not so much for technical reasons but more for all too human ones).

Ciao,
Dscho

#136 lore raw

Re: [PATCH v3 04/15] merge-tree: implement real merges

Quoted 4 lines · Johannes Schindelin

Hi,

On Thu, 3 Feb 2022, Elijah Newren wrote:

[...]

Quoted 9 lines
Quoted 5 lines

I agree. I've got a lot of thoughts on it, and some work in progress towards it (https://github.com/newren/git/tree/replay -- _very_ hacky, not even close to alpha quality, lots of fixup commits, todo comments, random brain dump files added to the tree, based on a previous round of this patch series, not updated for weeks, etc., etc.)

Just chiming in that I find that very exciting. But it's a tangent, and slightly distracting from the topic at hand, so I would like to ask to focus back on server-side merges.

+1 for refocusing.

Quoted 20 lines
Quoted 10 lines

We'll certainly have discussions on what that should look like. But a plumbing-ish replacement for merge was much simpler, and made sense to do first. I would prefer to concentrate on getting that hammered down first. Then I'll start discussions on a plumbing-ish rebase/cherry-pick. And if that doesn't fulfill all the needs that folks think they want out of merge-tree, then we can add a merge_incore_nonrecursive()-based mode to merge-tree. It's all coming, but having fought transliterations-of-scripts in merge-recursive.c, sequencer.c, stash.c, rebase.c, etc. for years I really, really don't want any more of that. Let's end that insanity.

Being the driving force behind many a "built-in-ification" of scripted commands, I wholeheartedly agree. You can still see the fall-out of designing commands in a scripted fashion, without any way to represent data structures other than strings. I wish we had come up with a better design to prototype commands than to write shell scripts. But I have to admit that even I do not have any better idea than to work on a proper API for libgit.a (which has historically invariably seen push-back from Junio).

Quoted 5 lines

While I agree that this discussion is a valuable one, right now I would like to focus on getting the server-side merges done, and once that has happened, move on to the replay/sequencer/API discussion (which will probably be a big one, not so much for technical reasons but more for all too human ones).

I'm just guessing, but I suspect your prediction for the future replay/sequencer/rebase/API discussion will be spot on. :-)

#137 lore raw

Re: [PATCH v3 04/15] merge-tree: implement real merges

In reply to Johannes Altmanninger
Quoted 3 lines · Johannes Altmanninger

I wonder how the UI of a tool that does non-worktree/non-index cherry-picks will look like. I'd expect it to produce the same output as merge-tree, except cherry-pick should probably output a commit OID, not a tree.

A tool that creates a "merge", "cherry-pick" and "revert" should all output a resulting commit object name, not a tree object.

And "merge-tree" that creates a three-way merge to apply a change to go from tree A to tree B on top of tree X should take three tree-ish object names and produce a resulting one tree object name.

Using the "merge-tree" that merges two trees using the third tree as a common ancestor, these three higher-level tools can perform "merge", "cherry-pick", and "revert". They would all take two commit objects and produce one commit object.

#138 lore raw

Re: [PATCH v3 04/15] merge-tree: implement real merges

In reply to Elijah Newren
Quoted 5 lines · Elijah Newren

Adding such an ability to merge-tree would be trivial -- it basically involves just two things: (1) accepting one extra argument, and (2) calling merge_incore_nonrecursive() instead of merge_incore_recursive().

Quoted 10 lines

However, I think forking a subprocess for every merge of a series of commits is a completely unreasonable overhead, so even if we provide such an option to merge-tree, I still want a separate plumbing-ish tool that does non-worktree/non-index replaying of commits which is not written as a driver of merge-tree. That other tool should just call merge_incore_nonrecursive() directly. And such a tool, since it should handle an arbitrary number of commits, should certainly be able to handle just one commit. From that angle, it feels like adding another mode to merge-tree would just be a partial duplication of the other tool.

The above does not make much sense to me.

I am hearing that "multi-step cherry-picks and reverts need to be fast and we need something like sequencer that is all written in C, and single-step cherry-pick is merely a special case that does not deserve a plumbing".

But that argument leads to "and the same something-like-sequencer that is all written in C would need '--rebase-merges' that can pick multi-step merge sequences, and single-step merge does not deserve a plumbing", which is an argument against this topic that is utterly absurd.

So why isn't your objection not equally absurd against having a single step cherry-pick or revert primitive as a plumbing?

#139 lore raw

Re: [PATCH v3 04/15] merge-tree: implement real merges

Quoted 19 lines · Junio C Hamano
Quoted 5 lines · Elijah Newren

Adding such an ability to merge-tree would be trivial -- it basically involves just two things: (1) accepting one extra argument, and (2) calling merge_incore_nonrecursive() instead of merge_incore_recursive().

Quoted 10 lines

However, I think forking a subprocess for every merge of a series of commits is a completely unreasonable overhead, so even if we provide such an option to merge-tree, I still want a separate plumbing-ish tool that does non-worktree/non-index replaying of commits which is not written as a driver of merge-tree. That other tool should just call merge_incore_nonrecursive() directly. And such a tool, since it should handle an arbitrary number of commits, should certainly be able to handle just one commit. From that angle, it feels like adding another mode to merge-tree would just be a partial duplication of the other tool.

Quoted 4 lines

The above does not make much sense to me.

I am hearing that "multi-step cherry-picks and reverts need to be fast and we need something like sequencer that is all written in C,

Yes, I agree with that part so far. jj is kicking our butt on rebase speed; I'm not sure if we can catch it, but it'd be nice to see us not be more than a hundred times slower.

Quoted 2 lines

and single-step cherry-pick is merely a special case that does not deserve a plumbing".

Well, apparently I failed at communication if that's what you heard. Perhaps I can step back and provide my high-level goals, and then mention how this series fits in. My high-level goals:

* new sequencer-like replay tool, including multiple abilities
today's rebase/cherry-pick tools don't have
* enable folks to use merging machinery for server side operations
(merge, rebase, cherry-pick, revert)
* do not repeat or encourage the rebase-as-shell-script mistakes of yesteryear
* somehow split this up into reviewable chunks

Now, in particular, the "merge divergent branches" piece seemed like a really simple portion of the problem space for which I could get some early feedback without having to address the whole problem space all at once, and which doesn't seem to have any downside risk.

And even with my attempt to narrow it in scope, and even despite lots of early feedback from the Git Virtual Contributor Summit six months ago, it's been nearly two months of active discussions including all kinds of intrinsic and tangential points about the UI and design. Why try to prematurely widen the scope? Can we just focus on merging divergent branches for now, and cover the rest later?

Quoted 8 lines

But that argument leads to "and the same something-like-sequencer that is all written in C would need '--rebase-merges' that can pick multi-step merge sequences, and single-step merge does not deserve a plumbing", which is an argument against this topic that is utterly absurd.

So why isn't your objection not equally absurd against having a single step cherry-pick or revert primitive as a plumbing?

The objection you are arguing against is not my position. In fact, I'm not even objecting to having a single-step cherry-pick, I'm objecting to providing it _now_, which I thought would have been clear from the portion of my email you snipped ("...I'm happy to add [a single step cherry-pick primitive] along with the tool I submit later..."). Since that wasn't clear, and since that wasn't my only communication failure here, let me attempt to be clearer about my objection(s):

1. I'm really trying to pick off a small piece of the problem space and get feedback on it without unnecessarily complicating things with unrelated issues. Thus, this series is _only_ about merging branches that have diverged, and leaves commit replaying for later.

2. Two folks have chimed in about the single step cherry-pick, and the ONLY reason given for wanting such a thing was to create a rebasing/cherry-picking script which was driven by repeatedly invoking this low-level primitive command. That's also the only usecase I can currently think of for such a primitive. To me, that means providing such a low-level command now would be likely to result in the rebase-as-a-script mistake of yesteryear. I think we can avoid that pitfall by first providing a tool that avoids the repeatedly-fork-git-subprocesses model. (Also, providing a low-level single-step cherry-pick command also has the added negative of further distracting from the focus on merging divergent branches.)

3. The merge primitive in this series is useful completely independent of any rebasing script (it would not be used solely for rebasing merges, if it's used for that purpose at all, as evidenced by the fact that dscho is already trying to use it for doing new real merges).

4. Once we have a git-replay tool that can replay a sequence of commits, there _might_ not be a need for a single commit replaying primitive. If we provided one as you and Johannes Altimanninger were asking for, and it turned out to be deemed useless because the later tool I provide can do everything it can and more, haven't we just wasted time in providing it? And perhaps also wasted future time as we then have work to do to deprecate and remove the new command or mode? (NOTE: I did *not* say there was "no need" for a single-commit replaying primitive -- I said there "might not" be a need.)

Also, since you bring up --rebase-merges, there's an additional point about it that might be relevant:

5. While you could implement a naive --rebase-merges in terms of a primitive for merging divergent branches (or vice-versa, i.e. implement merging divergent branches from a naive --rebase-merges implementation), I think replaying merges more intelligently[*] is actually a distinct operation from doing a new merge of divergent branches and that you probably can't implement one in terms of the other. (I'm not certain on this, and definitely don't want to argue the finer points on it while my implementation is still half-baked, but I really do think they are different things right now.)

[*] https://lore.kernel.org/git/CABPp-BHp+d62dCyAaJfh1cZ8xVpGyb97mZryd02aCOX=Qn=Ltw@mail.gmail.com/

#140 lore raw

Re: [PATCH v3 04/15] merge-tree: implement real merges

Quoted 6 lines · Elijah Newren

The objection you are arguing against is not my position. In fact, I'm not even objecting to having a single-step cherry-pick, I'm objecting to providing it _now_, which I thought would have been clear from the portion of my email you snipped ("...I'm happy to add [a single step cherry-pick primitive] along with the tool I submit later...").

The entry point into the in-core merge machinery of ort already knows how to accept externally defined merge-base(s) to bypass the "caller didn't give us the merge base, so let's figure them out by using the two heads being merged" logic, so it just felt backwards *not* to have a vanilla three-way merge that can take three trees and be used for merge, cherry-pick or revert as the single primitive in the very beginning before we talk about multi-step operations.

So, I guess I am still not getting where the "I'm happy to _add_" part comes from. If we start with a primitive (internally callable from C) "here are three trees O A B, do the three-way merge", then there is nothing to "add" at all to expose a single-step cherry-pick. In fact, to the users of merge-tree, the result does not have to have any fixed meaning. If they pass common ancestors as the merge bases as Os and the current HEAD and the other branch as A and B, they get a merge. If they pass the commit to be picked as B, current HEAD as A and B's parent as O, they get a cherry-pick.

Perhaps starting from "You are allowed to give me two commits A B, and I do not let you specify the commit O to use as a common 'ancestor'" is the root cause of making this thing feel backwards. I agree with the goal of having an all-incore machinery that can do a merge. I just do not see the reason why you have to build it in a way that cannot be reused for other two directions of merge-y operations.

#141 lore raw

Re: [PATCH v3 04/15] merge-tree: implement real merges

Quoted 10 lines · Junio C Hamano
Quoted 6 lines · Elijah Newren

The objection you are arguing against is not my position. In fact, I'm not even objecting to having a single-step cherry-pick, I'm objecting to providing it _now_, which I thought would have been clear from the portion of my email you snipped ("...I'm happy to add [a single step cherry-pick primitive] along with the tool I submit later...").

Quoted 7 lines

The entry point into the in-core merge machinery of ort already knows how to accept externally defined merge-base(s) to bypass the "caller didn't give us the merge base, so let's figure them out by using the two heads being merged" logic, so it just felt backwards *not* to have a vanilla three-way merge that can take three trees and be used for merge, cherry-pick or revert as the single primitive in the very beginning before we talk about multi-step operations.

"The entry point"?

There are two entry points: merge_incore_recursive(), and merge_incore_nonrecursive(). The former is analogous to merge-recursive's merge_recursive(), and the latter is analogous to merge_trees().

"git merge" always uses merge_incore_recursive()/merge_recursive(). The other big merge-y operations (cherry-pick, rebase, revert), always use merge_incore_nonrecursive()/merge_trees().

(Technically, merge-recursive has a third entry point used by am & stash, but let's ignore it for a moment.)

Quoted 10 lines

So, I guess I am still not getting where the "I'm happy to _add_" part comes from. If we start with a primitive (internally callable from C) "here are three trees O A B, do the three-way merge", then there is nothing to "add" at all to expose a single-step cherry-pick. In fact, to the users of merge-tree, the result does not have to have any fixed meaning. If they pass common ancestors as the merge bases as Os and the current HEAD and the other branch as A and B, they get a merge. If they pass the commit to be picked as B, current HEAD as A and B's parent as O, they get a cherry-pick.

Quoted 7 lines

Perhaps starting from "You are allowed to give me two commits A B, and I do not let you specify the commit O to use as a common 'ancestor'" is the root cause of making this thing feel backwards. I agree with the goal of having an all-incore machinery that can do a merge. I just do not see the reason why you have to build it in a way that cannot be reused for other two directions of merge-y operations.

So, am I correct to understand that what bugs you is actually merge-recursive's and merge-ort's API? That you don't want these two types of merges to have different entry points, and that there should in fact only be one?

That might be an interesting line of investigation to try to modify the API to achieve that, but that feels like a bigger task that is somewhat tangential to this series.

Without such a thing, handling both merging-of-divergent-branches and three-way-merge-with-specified-merge-base would be separate codepaths that call different functions. I implemented one of those two codepaths, and if we want the other then it needs to be added. That's where the "add" part comes from, in my view.

Does that help, or am I still missing what you're saying?

#142 lore raw

Re: [PATCH v3 04/15] merge-tree: implement real merges

Quoted 4 lines · Elijah Newren

So, am I correct to understand that what bugs you is actually merge-recursive's and merge-ort's API? That you don't want these two types of merges to have different entry points, and that there should in fact only be one?

It is more like

It is more than OK that there are two, but the basic primitive is the "we have this and that tree objects to merge, and use this tree object as the ancestor" non-recursive thing, with the recursive one being just a thin wrapper around it to compute common ancestors, using the three-way primitive to reduce them into a single virtual ancestor, and finally using the three-way primitive to come up with the final result.

And making the composite "recursive" feature available long before the underlying "non-recursive" primitive becomes easily accessible to the scripters and system builders simply felt backwards.

#143 lore raw

Re: [PATCH v3 04/15] merge-tree: implement real merges

Quoted 2 lines · Junio C Hamano

It is more like
...

Actually, I misspoke. It is a bit different.

In my mind, the building block hierarchy would have been

(1) Take three tree objects A, B, and O, and do the three-way merge. No history relation is assumed among them.

(2) Take two tree objects A and B, with one or more commit objects Os; use (2) recursively to reduce Os into a single O and then apply (1) on A, B and O.

(3) Take two commit objects A and B. Compute Os out of A and B and use (2) once to merge A and B.

I think the basic primitive that should be exposed to an external world (read: plumbing) this year, after all years of experience with merge-recursive, should be (2), not (1).

If you have (2), then (3) is trivially possible (it is just a single call to get_merge_base()). "git merge-tree A B" without having to spell out bases is so convenent and you do not have to write "git merge-tree A B $(git merge-base --all A B)", so I am OK for it to exist, but it is not essential.

If you have (2) and exposed (2) as the primitive plumbing, cherry-pick and revert would be a narrow special case of passing only one O to the machinery.

And coming from the above point of view, exposing (3) as the primitive plumbing to scripters and system builders, and later having to _add_ support to allow (2), felt backwards. It should be trivial for us to make (2) available before we can even offer (3), but what is happening to this new plumbing command goes in the opposite order.

It may be, as you said, the problem the underlying ort API has that somehow makes it harder to expose (2), in which case, yes, I think that is what bugs me.

#144 lore raw

Re: [PATCH v3 04/15] merge-tree: implement real merges

Quoted 7 lines · Junio C Hamano
Quoted 2 lines · Junio C Hamano

It is more like
...

Actually, I misspoke. It is a bit different.

Quoted 8 lines

In my mind, the building block hierarchy would have been

(1) Take three tree objects A, B, and O, and do the three-way merge. No history relation is assumed among them.

(2) Take two tree objects A and B, with one or more commit objects Os; use (2) recursively to reduce Os into a single O and then apply (1) on A, B and O.

Accepting multiple bases is nice (because it frees users of having to recursively merge their merge-bases),

Let's say we take this series in its current form:

	git merge-tree [--write-tree] [<options>] <branch1> <branch2>
	git merge-tree [--trivial-merge] <base-tree> <branch1> <branch2> (deprecated)

and later discover we want to add (2), one possible syntax would be

	git merge-tree --write-tree [<options>] <branch1> <branch2> <base>...

(or put the bases in the middle like merge-file). Though the mandatory --write-tree leaves a bad taste.

A separate option is a better alternative:

	git merge-tree [--write-tree] [<options>] --base=<base1>,<base2>,... <branch1> <branch2>

Anyway, no need to worry about that now, especially since the root cause of the ugliness is the legacy --trivial-merge, and there is no way avoid that, even if we add this now.

Quoted 31 lines

(3) Take two commit objects A and B. Compute Os out of A and B and use (2) once to merge A and B.

I think the basic primitive that should be exposed to an external world (read: plumbing) this year, after all years of experience with merge-recursive, should be (2), not (1).

If you have (2), then (3) is trivially possible (it is just a single call to get_merge_base()). "git merge-tree A B" without having to spell out bases is so convenent and you do not have to write "git merge-tree A B $(git merge-base --all A B)", so I am OK for it to exist, but it is not essential.

If you have (2) and exposed (2) as the primitive plumbing, cherry-pick and revert would be a narrow special case of passing only one O to the machinery.

And coming from the above point of view, exposing (3) as the primitive plumbing to scripters and system builders, and later having to _add_ support to allow (2), felt backwards. It should be trivial for us to make (2) available before we can even offer (3), but what is happening to this new plumbing command goes in the opposite order.

It may be, as you said, the problem the underlying ort API has that somehow makes it harder to expose (2), in which case, yes, I think that is what bugs me.

#145 lore raw

Re: [PATCH v3 04/15] merge-tree: implement real merges

In reply to Elijah Newren
Quoted 9 lines · Elijah Newren
Quoted 8 lines · Junio C Hamano
Quoted 5 lines · Elijah Newren

Adding such an ability to merge-tree would be trivial -- it basically involves just two things: (1) accepting one extra argument, and (2) calling merge_incore_nonrecursive() instead of merge_incore_recursive().

Quoted 10 lines
Quoted 10 lines
Quoted 10 lines

However, I think forking a subprocess for every merge of a series of commits is a completely unreasonable overhead, so even if we provide such an option to merge-tree, I still want a separate plumbing-ish tool that does non-worktree/non-index replaying of commits which is not written as a driver of merge-tree. That other tool should just call merge_incore_nonrecursive() directly. And such a tool, since it should handle an arbitrary number of commits, should certainly be able to handle just one commit. From that angle, it feels like adding another mode to merge-tree would just be a partial duplication of the other tool.

I don't think "to avoid duplication" is a good argument for making this plumbing command less flexible, because that's just chasing a local minimum w.r.t. redundancy. More general APIs will lead to a global minimum.

Quoted 59 lines
Quoted 5 lines

The above does not make much sense to me.

I am hearing that "multi-step cherry-picks and reverts need to be fast and we need something like sequencer that is all written in C,

Yes, I agree with that part so far. jj is kicking our butt on rebase speed; I'm not sure if we can catch it, but it'd be nice to see us not be more than a hundred times slower.

Quoted 2 lines

and single-step cherry-pick is merely a special case that does not deserve a plumbing".

Well, apparently I failed at communication if that's what you heard. Perhaps I can step back and provide my high-level goals, and then mention how this series fits in. My high-level goals:

* new sequencer-like replay tool, including multiple abilities
today's rebase/cherry-pick tools don't have
* enable folks to use merging machinery for server side operations
(merge, rebase, cherry-pick, revert)
* do not repeat or encourage the rebase-as-shell-script mistakes of yesteryear
* somehow split this up into reviewable chunks

Now, in particular, the "merge divergent branches" piece seemed like a really simple portion of the problem space for which I could get some early feedback without having to address the whole problem space all at once, and which doesn't seem to have any downside risk.

And even with my attempt to narrow it in scope, and even despite lots of early feedback from the Git Virtual Contributor Summit six months ago, it's been nearly two months of active discussions including all kinds of intrinsic and tangential points about the UI and design. Why try to prematurely widen the scope? Can we just focus on merging divergent branches for now, and cover the rest later?

Quoted 8 lines

But that argument leads to "and the same something-like-sequencer that is all written in C would need '--rebase-merges' that can pick multi-step merge sequences, and single-step merge does not deserve a plumbing", which is an argument against this topic that is utterly absurd.

So why isn't your objection not equally absurd against having a single step cherry-pick or revert primitive as a plumbing?

The objection you are arguing against is not my position. In fact, I'm not even objecting to having a single-step cherry-pick, I'm objecting to providing it _now_, which I thought would have been clear from the portion of my email you snipped ("...I'm happy to add [a single step cherry-pick primitive] along with the tool I submit later..."). Since that wasn't clear, and since that wasn't my only communication failure here, let me attempt to be clearer about my objection(s):

1. I'm really trying to pick off a small piece of the problem space and get feedback on it without unnecessarily complicating things with unrelated issues. Thus, this series is _only_ about merging branches that have diverged, and leaves commit replaying for later.

Quoted 11 lines

2. Two folks have chimed in about the single step cherry-pick, and the ONLY reason given for wanting such a thing was to create a rebasing/cherry-picking script which was driven by repeatedly invoking this low-level primitive command. That's also the only usecase I can currently think of for such a primitive. To me, that means providing such a low-level command now would be likely to result in the rebase-as-a-script mistake of yesteryear. I think we can avoid that pitfall by first providing a tool that avoids the repeatedly-fork-git-subprocesses model. (Also, providing a low-level single-step cherry-pick command also has the added negative of further distracting from the focus on merging divergent branches.)

I agree that it's not a good idea to call merge-tree in a loop for cherry-picking commit sequences.

At the same time, it is weird for such a low-level tool to not allow specifying merge bases.
Accepting merge bases is the more logical API, that might allow curious users to figure out how revert/cherry-pick are implemented.

I intuitively prefer the version that accepts merge bases but I don't have a good use case, so I think it's okay to add that later if we ever find use for it.

Quoted 6 lines

3. The merge primitive in this series is useful completely independent of any rebasing script (it would not be used solely for rebasing merges, if it's used for that purpose at all, as evidenced by the fact that dscho is already trying to use it for doing new real merges).

Quoted 9 lines

4. Once we have a git-replay tool that can replay a sequence of commits, there _might_ not be a need for a single commit replaying primitive. If we provided one as you and Johannes Altimanninger were asking for, and it turned out to be deemed useless because the later tool I provide can do everything it can and more, haven't we just wasted time in providing it? And perhaps also wasted future time as we then have work to do to deprecate and remove the new command or mode? (NOTE: I did *not* say there was "no need" for a single-commit replaying primitive -- I said there "might not" be a need.)

If we get a tool that can do multiple cherry-picks, I think there is no technical reason against having an equivalent tool that can do multiple merges. In that future, merge-tree might be mostly obsolete.

In general, this is a difficult discussion. It's really hard to judge this series without a bigger picture of how our future UI will look like. (Thanks for sharing the replay code BTW, there are some nice features in there.) Though I agree that integrating this (minimal) series first makes a ton of sense, because it already supports a valid use case.

I feel like the output format is a bit experimental because it doesn't give much of the conflict information in a machine-parseable format. Of course it's good enough for many uses (so I don't think this should block this topic) but I think we should have a plan on how to change the output format in future without adding ugly compatibility hacks. Marking merge-tree as "experimental" (like git-switch/git-restore) comes to mind. That would work although it's not the most user-friendly way.

(OK I just saw that you are still looking into the output format in CABPp-BG++YqesTxp+JL3XzwrogfMag1NscoMpCOExmV9z6Py9A@mail.gmail.com )

I wanted to implement some (cherry-picking) scripts using merge-tree but I don't have enough time or need, so I don't have much feedback on the output format today. I can imagine that it would be nice to have a clear distinction between content conflicts and non-content conflicts, but let's worry about that later..

Quoted 15 lines

Also, since you bring up --rebase-merges, there's an additional point about it that might be relevant:

5. While you could implement a naive --rebase-merges in terms of a primitive for merging divergent branches (or vice-versa, i.e. implement merging divergent branches from a naive --rebase-merges implementation), I think replaying merges more intelligently[*] is actually a distinct operation from doing a new merge of divergent branches and that you probably can't implement one in terms of the other. (I'm not certain on this, and definitely don't want to argue the finer points on it while my implementation is still half-baked, but I really do think they are different things right now.)

[*] https://lore.kernel.org/git/CABPp-BHp+d62dCyAaJfh1cZ8xVpGyb97mZryd02aCOX=Qn=Ltw@mail.gmail.com/

#146 lore raw

Re: [PATCH v3 04/15] merge-tree: implement real merges

In reply to Junio C Hamano

Hi Junio,

Quoted 18 lines · Junio C Hamano
Quoted 5 lines · Elijah Newren

Adding such an ability to merge-tree would be trivial -- it basically involves just two things: (1) accepting one extra argument, and (2) calling merge_incore_nonrecursive() instead of merge_incore_recursive().

Quoted 10 lines

However, I think forking a subprocess for every merge of a series of commits is a completely unreasonable overhead, so even if we provide such an option to merge-tree, I still want a separate plumbing-ish tool that does non-worktree/non-index replaying of commits which is not written as a driver of merge-tree. That other tool should just call merge_incore_nonrecursive() directly. And such a tool, since it should handle an arbitrary number of commits, should certainly be able to handle just one commit. From that angle, it feels like adding another mode to merge-tree would just be a partial duplication of the other tool.

Quoted 6 lines

The above does not make much sense to me.

I am hearing that "multi-step cherry-picks and reverts need to be fast and we need something like sequencer that is all written in C, and single-step cherry-pick is merely a special case that does not deserve a plumbing".

Correct. The single cherry-pick will be a trivial fall-out of such a sequencer, and the opposite is not true: if we taught merge-tree the options --cherry-pick or --revert, the result would be a dead end because it does not make sense to extend merge-tree to do what the sequencer would do.

Quoted 5 lines

But that argument leads to "and the same something-like-sequencer that is all written in C would need '--rebase-merges' that can pick multi-step merge sequences, and single-step merge does not deserve a plumbing", which is an argument against this topic that is utterly absurd.

But that --rebase-merges-like behavior is far off in the future, and the sequencer is not.

If you step back for a moment and think about the existing use cases where we want to use merge-tree on the server side, there are GitHub's Pull Requests (and I suspect that all other Git hosting services followed suite), where you have three options:

- merge
- squash
- rebase

The first two options are actually pretty much done, as we already have a way with the current iteration of merge-tree to get the tree, and that's all we need from merge-tree, the rest can be done by calling commit-tree with the appropriate parent(s) and commit messages.

In contrast, rebase will require not only tree objects to be generated, but much more. It is a fundamentally more complex operation because of that.

Now, if there were already server-side user interfaces to cherry-pick, I could potentially see that the next logical step would be to support something like the --force-non-recursive-base=<tree-ish> option I have already implemented over here (for separate reasons).

But I am unaware of such a user interface. I _am_ aware of the rebase option to apply Pull Requests. So I think that's the logical direction we're going from here.

Quoted 2 lines

So why isn't your objection not equally absurd against having a single step cherry-pick or revert primitive as a plumbing?

Well, if you care so deeply about it, I will offer up that patch to support --force-non-recursive-base=<tree-ish> (where the cherry-pick and revert primitive would not need to exist but be the special case of passing CHERRY_PICK_HEAD^ as the appropriate argument).

What gets me excited much more, though, is the rebase operation. And therefore I would like to spend more focus on that, and focus is a limited resource (especially here on the Git mailing list :-)).

Ciao,
Dscho

#147 lore raw

Re: [PATCH v3 04/15] merge-tree: implement real merges

In reply to Elijah Newren via GitGitGadget
Quoted 77 lines · Elijah Newren via GitGitGadget
Documentation/git-merge-tree.txt +49 -12
index 58731c19422..569485815a0 100644
@@ -3,26 +3,73 @@ git-merge-tree(1)
 
 NAME
 ----
-git-merge-tree - Show three-way merge without touching index
+git-merge-tree - Perform merge without touching index or working tree
 
 
 SYNOPSIS
 --------
 [verse]
-'git merge-tree' <base-tree> <branch1> <branch2>
+'git merge-tree' [--write-tree] <branch1> <branch2>
+'git merge-tree' [--trivial-merge] <base-tree> <branch1> <branch2> (deprecated)
 
 DESCRIPTION
 -----------
-Reads three tree-ish, and output trivial merge results and
-conflicting stages to the standard output.  This is similar to
-what three-way 'git read-tree -m' does, but instead of storing the
-results in the index, the command outputs the entries to the
-standard output.
-
-This is meant to be used by higher level scripts to compute
-merge results outside of the index, and stuff the results back into the
-index.  For this reason, the output from the command omits
-entries that match the <branch1> tree.
+
+Performs a merge, but does not make any new commits and does not read
+from or write to either the working tree or index.
+
+The second form is deprecated and supported only for backward
+compatibility.  It will likely be removed in the future, and will not
+be discussed further in this manual.
+
+The first form will merge the two branches, doing a real merge.  A real
+merge is distinguished from a trivial merge in that it includes:
+
+  * three way content merges of individual files
+  * rename detection
+  * proper directory/file conflict handling
+  * recursive ancestor consolidation (i.e. when there is more than one
+    merge base, creating a virtual merge base by merging the merge bases)
+  * etc.
+
+After the merge completes, it will create a new toplevel tree object.
+See `OUTPUT` below for details.
+
+OUTPUT
+------
+
+For either a successful or conflicted merge, the output from
+git-merge-tree is simply one line:
+
+	<OID of toplevel tree>
+
+The printed tree object corresponds to what would be checked out in
+the working tree at the end of `git merge`, and thus may have files
+with conflict markers in them.
+
+EXIT STATUS
+-----------
+
+For a successful, non-conflicted merge, the exit status is 0.  When the
+merge has conflicts, the exit status is 1.  If the merge is not able to
+complete (or start) due to some kind of error, the exit status is
+something other than 0 or 1.
+
+USAGE NOTES
+-----------
+
+git-merge-tree was written to be low-level plumbing, similar to
+hash-object, mktree, commit-tree, update-ref, and mktag.  Thus, it could

Documentation/git-merge-tree.txt git-merge-tree(1) · line 72

Quoted 10 lines
Documentation/git-merge-tree.txt +10 -0
+be used as a part of a series of steps such as
+
+       NEWTREE=$(git merge-tree --write-tree $BRANCH1 $BRANCH2)
+       test $? -eq 0 || die "There were conflicts..."
+       NEWCOMMIT=$(git commit-tree $NEWTREE -p $BRANCH1 -p $BRANCH2)
+       git update-ref $BRANCH1 $NEWCOMMIT
+
+However, it does not quite fit into the same category of low-level
+plumbing commands since the possibility of merge conflicts give it a
+much higher chance of the command not succeeding.

I found this final paragraph confusing. It seems to be hinting at some conclusion it expects readers to make, but I haven't been able to figure out what. Could this be made more explicit, or perhaps dropped altogether?

#148 lore raw

Re: [PATCH v3 04/15] merge-tree: implement real merges

Quoted 2 lines · Josh Steadmon

On 2022.02.02 07:34, Elijah Newren via GitGitGadget wrote:

[...]

Quoted 12 lines
Quoted 12 lines
Documentation/git-merge-tree.txt +12 -0
+USAGE NOTES
+-----------
+
+git-merge-tree was written to be low-level plumbing, similar to
+hash-object, mktree, commit-tree, update-ref, and mktag.  Thus, it could
+be used as a part of a series of steps such as
+
+       NEWTREE=$(git merge-tree --write-tree $BRANCH1 $BRANCH2)
+       test $? -eq 0 || die "There were conflicts..."
+       NEWCOMMIT=$(git commit-tree $NEWTREE -p $BRANCH1 -p $BRANCH2)
+       git update-ref $BRANCH1 $NEWCOMMIT
+
Quoted 8 lines

Documentation/git-merge-tree.txt git-merge-tree(1) · line 72

Quoted 3 lines
Documentation/git-merge-tree.txt +3 -0
+However, it does not quite fit into the same category of low-level
+plumbing commands since the possibility of merge conflicts give it a
+much higher chance of the command not succeeding.

I found this final paragraph confusing. It seems to be hinting at some conclusion it expects readers to make, but I haven't been able to figure out what. Could this be made more explicit, or perhaps dropped altogether?

Yep, I'll drop it.

#149 lore raw

PATCH v3 5/15 Introduce a variant of the `warning()` function that takes a `FILE *`

In reply to Elijah Newren via GitGitGadget

From: Johannes Schindelin <[email protected]>

We are about to teach diff_warn_rename_limit() to write into a file instead of stderr. That function wants to call warning() when writing to stderr, though, allowing for the warn_routine to be overridden.

Let's introduce a helper for that.

Note: Since there is currently no need to provide similar functions for error() or die(), let alone for the _errno variants, we will leave that to a date when the need for those should arise, if ever.

---
 git-compat-util.h |  1 +
 usage.c           | 14 ++++++++++++++
 2 files changed, 15 insertions(+)
git-compat-util.h +1 -0
index d70ce142861..64ba60e5c71 100644
@@ -475,6 +475,7 @@ int error(const char *err, ...) __attribute__((format (printf, 1, 2)));
 int error_errno(const char *err, ...) __attribute__((format (printf, 1, 2)));
 void warning(const char *err, ...) __attribute__((format (printf, 1, 2)));
 void warning_errno(const char *err, ...) __attribute__((format (printf, 1, 2)));
+void warning_fp(FILE *out, const char *warn, ...) __attribute__((format (printf, 2, 3)));
 
 #ifndef NO_OPENSSL
 #ifdef APPLE_COMMON_CRYPTO
usage.c +14 -0
index c7d233b0de9..0bfd2c603c0 100644
@@ -253,6 +253,20 @@ void warning(const char *warn, ...)
 	va_end(params);
 }
 
+void warning_fp(FILE *out, const char *warn, ...)
+{
+	va_list params;
+
+	va_start(params, warn);
+	if (out == stderr)
+		warn_routine(warn, params);
+	else {
+		vfprintf(out, warn, params);
+		fputc('\n', out);
+	}
+	va_end(params);
+}
+
 /* Only set this, ever, from t/helper/, when verifying that bugs are caught. */
 int BUG_exit_code;
 
Signature · 1 line
gitgitgadget
#150 lore raw

PATCH v3 6/15 diff: allow diff_warn_rename_limit to write somewhere besides stderr

In reply to Elijah Newren via GitGitGadget

From: Johannes Schindelin <[email protected]>

diff_warn_rename_limit() is hardcoded to write to stderr. Make it accept a file stream parameter to make it more flexible.

---
 diff.c            | 13 ++++++++-----
 diff.h            |  3 ++-
 merge-ort.c       |  2 +-
 merge-recursive.c |  3 ++-
 4 files changed, 13 insertions(+), 8 deletions(-)
diff.c +8 -5
index 1bfb01c18ec..28368110147 100644
@@ -6377,17 +6377,20 @@ static const char rename_limit_advice[] =
 N_("you may want to set your %s variable to at least "
    "%d and retry the command.");
 
-void diff_warn_rename_limit(const char *varname, int needed, int degraded_cc)
+void diff_warn_rename_limit(const char *varname, int needed, int degraded_cc,
+			    FILE *out)
 {
 	fflush(stdout);
 	if (degraded_cc)
-		warning(_(degrade_cc_to_c_warning));
+		warning_fp(out, _(degrade_cc_to_c_warning));
 	else if (needed)
-		warning(_(rename_limit_warning));
+		warning_fp(out, _(rename_limit_warning));
 	else
 		return;
+
+
 	if (0 < needed)
-		warning(_(rename_limit_advice), varname, needed);
+		warning_fp(out, _(rename_limit_advice), varname, needed);
 }
 
 static void create_filepairs_for_header_only_notifications(struct diff_options *o)
@@ -6870,7 +6873,7 @@ int diff_result_code(struct diff_options *opt, int status)
 
 	diff_warn_rename_limit("diff.renameLimit",
 			       opt->needed_rename_limit,
-			       opt->degraded_cc_to_c);
+			       opt->degraded_cc_to_c, stderr);
 	if (!opt->flags.exit_with_status &&
 	    !(opt->output_format & DIFF_FORMAT_CHECKDIFF))
 		return status;
diff.h +2 -1
index ce9e2cf2e4f..40c5b78fb0a 100644
@@ -597,7 +597,8 @@ void diffcore_fix_diff_index(void);
 int diff_queue_is_empty(struct diff_options *o);
 void diff_flush(struct diff_options*);
 void diff_free(struct diff_options*);
-void diff_warn_rename_limit(const char *varname, int needed, int degraded_cc);
+void diff_warn_rename_limit(const char *varname, int needed, int degraded_cc,
+			    FILE *out);
 
 /* diff-raw status letters */
 #define DIFF_STATUS_ADDED		'A'
merge-ort.c +1 -1
index 9bf15a01db8..46e72b62880 100644
@@ -4305,7 +4305,7 @@ void merge_switch_to_result(struct merge_options *opt,
 
 		/* Also include needed rename limit adjustment now */
 		diff_warn_rename_limit("merge.renamelimit",
-				       opti->renames.needed_limit, 0);
+				       opti->renames.needed_limit, 0, stderr);
 
 		trace2_region_leave("merge", "display messages", opt->repo);
 	}
merge-recursive.c +2 -1
index 9ec1e6d043a..01ca82773cc 100644
@@ -3738,7 +3738,8 @@ static void merge_finalize(struct merge_options *opt)
 		strbuf_release(&opt->obuf);
 	if (show(opt, 2))
 		diff_warn_rename_limit("merge.renamelimit",
-				       opt->priv->needed_rename_limit, 0);
+				       opt->priv->needed_rename_limit, 0,
+				       stderr);
 	FREE_AND_NULL(opt->priv);
 }
 
Signature · 1 line
gitgitgadget
#151 lore raw

PATCH v3 7/15 merge-ort: split out a separate display_update_messages() function

In reply to Elijah Newren via GitGitGadget

From: Elijah Newren <[email protected]>

This patch includes no new code; it simply moves a bunch of lines into a new function. As such, there are no functional changes. This is just a preparatory step to allow the printed messages to be handled differently by other callers, such as in git merge-tree --write-tree.

(Patch best viewed with

     --color-moved --color-moved-ws=allow-indentation-change

to see that it is a simple code movement.)

---
 merge-ort.c | 77 ++++++++++++++++++++++++++++-------------------------
 merge-ort.h |  8 ++++++
 2 files changed, 49 insertions(+), 36 deletions(-)
merge-ort.c +41 -36
index 46e72b62880..82d2faf5bf9 100644
@@ -4235,6 +4235,45 @@ static int record_conflicted_index_entries(struct merge_options *opt)
 	return errs;
 }
 
+void merge_display_update_messages(struct merge_options *opt,
+				   struct merge_result *result)
+{
+	struct merge_options_internal *opti = result->priv;
+	struct hashmap_iter iter;
+	struct strmap_entry *e;
+	struct string_list olist = STRING_LIST_INIT_NODUP;
+	int i;
+
+	if (opt->record_conflict_msgs_as_headers)
+		BUG("Either display conflict messages or record them as headers, not both");
+
+	trace2_region_enter("merge", "display messages", opt->repo);
+
+	/* Hack to pre-allocate olist to the desired size */
+	ALLOC_GROW(olist.items, strmap_get_size(&opti->output),
+		   olist.alloc);
+
+	/* Put every entry from output into olist, then sort */
+	strmap_for_each_entry(&opti->output, &iter, e) {
+		string_list_append(&olist, e->key)->util = e->value;
+	}
+	string_list_sort(&olist);
+
+	/* Iterate over the items, printing them */
+	for (i = 0; i < olist.nr; ++i) {
+		struct strbuf *sb = olist.items[i].util;
+
+		printf("%s", sb->buf);
+	}
+	string_list_clear(&olist, 0);
+
+	/* Also include needed rename limit adjustment now */
+	diff_warn_rename_limit("merge.renamelimit",
+			       opti->renames.needed_limit, 0, stderr);
+
+	trace2_region_leave("merge", "display messages", opt->repo);
+}
+
 void merge_switch_to_result(struct merge_options *opt,
 			    struct tree *head,
 			    struct merge_result *result,
@@ -4273,42 +4312,8 @@ void merge_switch_to_result(struct merge_options *opt,
 		trace2_region_leave("merge", "write_auto_merge", opt->repo);
 	}
 
-	if (display_update_msgs) {
-		struct merge_options_internal *opti = result->priv;
-		struct hashmap_iter iter;
-		struct strmap_entry *e;
-		struct string_list olist = STRING_LIST_INIT_NODUP;
-		int i;
-
-		if (opt->record_conflict_msgs_as_headers)
-			BUG("Either display conflict messages or record them as headers, not both");
-
-		trace2_region_enter("merge", "display messages", opt->repo);
-
-		/* Hack to pre-allocate olist to the desired size */
-		ALLOC_GROW(olist.items, strmap_get_size(&opti->output),
-			   olist.alloc);
-
-		/* Put every entry from output into olist, then sort */
-		strmap_for_each_entry(&opti->output, &iter, e) {
-			string_list_append(&olist, e->key)->util = e->value;
-		}
-		string_list_sort(&olist);
-
-		/* Iterate over the items, printing them */
-		for (i = 0; i < olist.nr; ++i) {
-			struct strbuf *sb = olist.items[i].util;
-
-			printf("%s", sb->buf);
-		}
-		string_list_clear(&olist, 0);
-
-		/* Also include needed rename limit adjustment now */
-		diff_warn_rename_limit("merge.renamelimit",
-				       opti->renames.needed_limit, 0, stderr);
-
-		trace2_region_leave("merge", "display messages", opt->repo);
-	}
+	if (display_update_msgs)
+		merge_display_update_messages(opt, result);
 
 	merge_finalize(opt, result);
 }
merge-ort.h +8 -0
index fe599b87868..e5aec45b18f 100644
@@ -80,6 +80,14 @@ void merge_switch_to_result(struct merge_options *opt,
 			    int update_worktree_and_index,
 			    int display_update_msgs);
 
+/*
+ * Display messages about conflicts and which files were 3-way merged.
+ * Automatically called by merge_switch_to_result() with stream == stdout,
+ * so only call this when bypassing merge_switch_to_result().
+ */
+void merge_display_update_messages(struct merge_options *opt,
+				   struct merge_result *result);
+
 /* Do needed cleanup when not calling merge_switch_to_result() */
 void merge_finalize(struct merge_options *opt,
 		    struct merge_result *result);
Signature · 1 line
gitgitgadget
#152 lore raw

PATCH v3 8/15 merge-ort: allow update messages to be written to different file stream

In reply to Elijah Newren via GitGitGadget

From: Elijah Newren <[email protected]>

This modifies the new display_update_messages() function to allow printing to somewhere other than stdout. It also consolidates the location of the diff_warn_rename_limit() message with the rest of the CONFLICT and other update messages to all go to the same stream.

---
 merge-ort.c | 9 +++++----
 merge-ort.h | 3 ++-
 2 files changed, 7 insertions(+), 5 deletions(-)
merge-ort.c +5 -4
index 82d2faf5bf9..d28d1721d14 100644
@@ -4236,7 +4236,8 @@ static int record_conflicted_index_entries(struct merge_options *opt)
 }
 
 void merge_display_update_messages(struct merge_options *opt,
-				   struct merge_result *result)
+				   struct merge_result *result,
+				   FILE *stream)
 {
 	struct merge_options_internal *opti = result->priv;
 	struct hashmap_iter iter;
@@ -4263,13 +4264,13 @@ void merge_display_update_messages(struct merge_options *opt,
 	for (i = 0; i < olist.nr; ++i) {
 		struct strbuf *sb = olist.items[i].util;
 
-		printf("%s", sb->buf);
+		strbuf_write(sb, stream);
 	}
 	string_list_clear(&olist, 0);
 
 	/* Also include needed rename limit adjustment now */
 	diff_warn_rename_limit("merge.renamelimit",
-			       opti->renames.needed_limit, 0, stderr);
+			       opti->renames.needed_limit, 0, stream);
 
 	trace2_region_leave("merge", "display messages", opt->repo);
 }
@@ -4313,7 +4314,7 @@ void merge_switch_to_result(struct merge_options *opt,
 	}
 
 	if (display_update_msgs)
-		merge_display_update_messages(opt, result);
+		merge_display_update_messages(opt, result, stdout);
 
 	merge_finalize(opt, result);
 }
merge-ort.h +2 -1
index e5aec45b18f..d643b47cb7c 100644
@@ -86,7 +86,8 @@ void merge_switch_to_result(struct merge_options *opt,
  * so only call this when bypassing merge_switch_to_result().
  */
 void merge_display_update_messages(struct merge_options *opt,
-				   struct merge_result *result);
+				   struct merge_result *result,
+				   FILE *stream);
 
 /* Do needed cleanup when not calling merge_switch_to_result() */
 void merge_finalize(struct merge_options *opt,
Signature · 1 line
gitgitgadget
#153 lore raw

Re: [PATCH v3 08/15] merge-ort: allow update messages to be written to different file stream

Quoted 30 lines · Elijah Newren via GitGitGadget

From: Elijah Newren <[email protected]>

This modifies the new display_update_messages() function to allow printing to somewhere other than stdout. It also consolidates the location of the diff_warn_rename_limit() message with the rest of the CONFLICT and other update messages to all go to the same stream.

---
 merge-ort.c | 9 +++++----
 merge-ort.h | 3 ++-
 2 files changed, 7 insertions(+), 5 deletions(-)
merge-ort.c +2 -1
index 82d2faf5bf9..d28d1721d14 100644
@@ -4236,7 +4236,8 @@ static int record_conflicted_index_entries(struct merge_options *opt)
 }
 
 void merge_display_update_messages(struct merge_options *opt,
-				   struct merge_result *result)
+				   struct merge_result *result,
+				   FILE *stream)
 {
 	struct merge_options_internal *opti = result->priv;
 	struct hashmap_iter iter;
@@ -4263,13 +4264,13 @@ void merge_display_update_messages(struct merge_options *opt,
 	for (i = 0; i < olist.nr; ++i) {
 		struct strbuf *sb = olist.items[i].util;

merge-ort.c void merge_display_update_messages(struct merge_options *opt, · line 4273

Quoted 10 lines
merge-ort.c +2 -2
 
-		printf("%s", sb->buf);
+		strbuf_write(sb, stream);
 	}
 	string_list_clear(&olist, 0);
 
 	/* Also include needed rename limit adjustment now */
 	diff_warn_rename_limit("merge.renamelimit",
-			       opti->renames.needed_limit, 0, stderr);
+			       opti->renames.needed_limit, 0, stream);

At the tip of this series I tried to s/stream/stderr/g this, and t4301-merge-tree-write-tree.sh passes, doesn't this warning_fp() special behavior need a test somewhere?

I assumed that warning_fp() would be using vreportf() in usage.c, but it's not, it's just falling back to the equivalent of fprintf(out, ...), no? I don't really see why 05/15 and parts of 06/15 & this are needed over a much simpler simple helper macro like the below. applied on top of this series.

I would get it if the point was to actually use the full usage.c machinery, but we're just either calling warning(), or printing a formatted string to a file FILE *. There's no need to go through usage.c for that, and adding an API to it that behaves like this new warning_fp() is really confusing.

I.e. an API in usage.c that allowed warning to a given FD would be trying to replace the "2" in the write_in_full() call in vreportf(), I would think.

diff.c +9 -2
index 28368110147..4cf67e93dea 100644
@@ -6377,14 +6377,21 @@ static const char rename_limit_advice[] =
 N_("you may want to set your %s variable to at least "
    "%d and retry the command.");
 
+#define warning_fp(out, fmt, ...) do { \
+	if (out == stderr) \
+		warning(fmt, __VA_ARGS__); \
+	else \
+		fprintf(out, fmt, __VA_ARGS__); \
+} while (0)
+
 void diff_warn_rename_limit(const char *varname, int needed, int degraded_cc,
 			    FILE *out)
 {
 	fflush(stdout);
 	if (degraded_cc)
-		warning_fp(out, _(degrade_cc_to_c_warning));
+		warning_fp(out, _(degrade_cc_to_c_warning), NULL);
 	else if (needed)
-		warning_fp(out, _(rename_limit_warning));
+		warning_fp(out, _(rename_limit_warning), NULL);
 	else
 		return;
 
git-compat-util.h +0 -1
index 64ba60e5c71..d70ce142861 100644
@@ -475,7 +475,6 @@ int error(const char *err, ...) __attribute__((format (printf, 1, 2)));
 int error_errno(const char *err, ...) __attribute__((format (printf, 1, 2)));
 void warning(const char *err, ...) __attribute__((format (printf, 1, 2)));
 void warning_errno(const char *err, ...) __attribute__((format (printf, 1, 2)));
-void warning_fp(FILE *out, const char *warn, ...) __attribute__((format (printf, 2, 3)));
 
 #ifndef NO_OPENSSL
 #ifdef APPLE_COMMON_CRYPTO
usage.c +0 -14
index 0bfd2c603c0..c7d233b0de9 100644
@@ -253,20 +253,6 @@ void warning(const char *warn, ...)
 	va_end(params);
 }
 
-void warning_fp(FILE *out, const char *warn, ...)
-{
-	va_list params;
-
-	va_start(params, warn);
-	if (out == stderr)
-		warn_routine(warn, params);
-	else {
-		vfprintf(out, warn, params);
-		fputc('\n', out);
-	}
-	va_end(params);
-}
-
 /* Only set this, ever, from t/helper/, when verifying that bugs are caught. */
 int BUG_exit_code;
 
#154 lore raw

Re: [PATCH v3 08/15] merge-ort: allow update messages to be written to different file stream

Quoted 39 lines · Ævar Arnfjörð Bjarmason
Quoted 36 lines · Elijah Newren via GitGitGadget

From: Elijah Newren <[email protected]>

This modifies the new display_update_messages() function to allow printing to somewhere other than stdout. It also consolidates the location of the diff_warn_rename_limit() message with the rest of the CONFLICT and other update messages to all go to the same stream.

---
 merge-ort.c | 9 +++++----
 merge-ort.h | 3 ++-
 2 files changed, 7 insertions(+), 5 deletions(-)
merge-ort.c +3 -2
index 82d2faf5bf9..d28d1721d14 100644
@@ -4236,7 +4236,8 @@ static int record_conflicted_index_entries(struct merge_options *opt)
 }
 
 void merge_display_update_messages(struct merge_options *opt,
-                                struct merge_result *result)
+                                struct merge_result *result,
+                                FILE *stream)
 {
      struct merge_options_internal *opti = result->priv;
      struct hashmap_iter iter;
@@ -4263,13 +4264,13 @@ void merge_display_update_messages(struct merge_options *opt,
      for (i = 0; i < olist.nr; ++i) {
              struct strbuf *sb = olist.items[i].util;
 
-             printf("%s", sb->buf);
+             strbuf_write(sb, stream);
      }
      string_list_clear(&olist, 0);
 
Quoted 8 lines

merge-ort.c void merge_display_update_messages(struct merge_options *opt, · line 4273

Quoted 4 lines
merge-ort.c +1 -1
      /* Also include needed rename limit adjustment now */
      diff_warn_rename_limit("merge.renamelimit",
-                            opti->renames.needed_limit, 0, stderr);
+                            opti->renames.needed_limit, 0, stream);

At the tip of this series I tried to s/stream/stderr/g this, and t4301-merge-tree-write-tree.sh passes, doesn't this warning_fp() special behavior need a test somewhere?

That's a fair point, but...this gets back to my cover letter comments about patches 5, 6, and 8. They implement a code feature that seems useful in general...but which Dscho and Christian didn't like using in this particular command; they just wanted all output on stdout.

So, it's hard to add a test, because there's no code anywhere that exercises it in this series anymore. I originally wanted this feature in my remerge-diff series, but the idea of conflict headers made me punt it to this series. I wanted it for this series, but Dscho and Christian didn't. I could have punted again, but decided the underlying want kept coming up and decided to not excise it -- especially since Dscho was helping improve it. And Junio commented that he liked the idea[1].

[1] https://lore.kernel.org/git/[email protected]/

But yeah, it does leave it feeling slightly odd that we implemented a feature that nothing is currently using. Maybe these 3 should be split off into their own series? Still wouldn't have a test yet, though.

Quoted 5 lines

I assumed that warning_fp() would be using vreportf() in usage.c, but it's not, it's just falling back to the equivalent of fprintf(out, ...), no? I don't really see why 05/15 and parts of 06/15 & this are needed over a much simpler simple helper macro like the below. applied on top of this series.

That macro is simple? I thought I basically understood Dscho's code, but looking at what you did with diff_warn_rename_limit(), I think I'm lost.

Quoted 5 lines

I would get it if the point was to actually use the full usage.c machinery, but we're just either calling warning(), or printing a formatted string to a file FILE *. There's no need to go through usage.c for that, and adding an API to it that behaves like this new warning_fp() is really confusing.

Because the formatted string being printed to the file won't have the same "warning: " prefix that is normally added to stuff in usage?

That's a fair point; that does have a bit of a consistency problem. And I'd rather the messages were consistent regardless of where they are printed.

Quoted 3 lines

I.e. an API in usage.c that allowed warning to a given FD would be trying to replace the "2" in the write_in_full() call in vreportf(), I would think.

Hmm, makes sense.

Quoted 15 lines
diff.c +7 -0
index 28368110147..4cf67e93dea 100644
@@ -6377,14 +6377,21 @@ static const char rename_limit_advice[] =
 N_("you may want to set your %s variable to at least "
    "%d and retry the command.");
 
+#define warning_fp(out, fmt, ...) do { \
+       if (out == stderr) \
+               warning(fmt, __VA_ARGS__); \
+       else \
+               fprintf(out, fmt, __VA_ARGS__); \
+} while (0)
+

diff.c static const char rename_limit_advice[] = · line 6394

Quoted 10 lines
diff.c +2 -2
 void diff_warn_rename_limit(const char *varname, int needed, int degraded_cc,
                            FILE *out)
 {
        fflush(stdout);
        if (degraded_cc)
-               warning_fp(out, _(degrade_cc_to_c_warning));
+               warning_fp(out, _(degrade_cc_to_c_warning), NULL);
        else if (needed)
-               warning_fp(out, _(rename_limit_warning));
+               warning_fp(out, _(rename_limit_warning), NULL);

Why do the only callers have a NULL parameter here? Is this one of those va_list/va_args things I never bothered to properly learn?

Quoted 39 lines
diff.c +0 -0
        else
                return;
 
git-compat-util.h +0 -1
index 64ba60e5c71..d70ce142861 100644
@@ -475,7 +475,6 @@ int error(const char *err, ...) __attribute__((format (printf, 1, 2)));
 int error_errno(const char *err, ...) __attribute__((format (printf, 1, 2)));
 void warning(const char *err, ...) __attribute__((format (printf, 1, 2)));
 void warning_errno(const char *err, ...) __attribute__((format (printf, 1, 2)));
-void warning_fp(FILE *out, const char *warn, ...) __attribute__((format (printf, 2, 3)));
 
 #ifndef NO_OPENSSL
 #ifdef APPLE_COMMON_CRYPTO
usage.c +0 -14
index 0bfd2c603c0..c7d233b0de9 100644
@@ -253,20 +253,6 @@ void warning(const char *warn, ...)
        va_end(params);
 }
 
-void warning_fp(FILE *out, const char *warn, ...)
-{
-       va_list params;
-
-       va_start(params, warn);
-       if (out == stderr)
-               warn_routine(warn, params);
-       else {
-               vfprintf(out, warn, params);
-               fputc('\n', out);
-       }
-       va_end(params);
-}
-
 /* Only set this, ever, from t/helper/, when verifying that bugs are caught. */
 int BUG_exit_code;
#155 lore raw

Re: [PATCH v3 08/15] merge-ort: allow update messages to be written to different file stream

Quoted 76 lines · Elijah Newren
Quoted 39 lines · Ævar Arnfjörð Bjarmason
Quoted 36 lines · Elijah Newren via GitGitGadget

From: Elijah Newren <[email protected]>

This modifies the new display_update_messages() function to allow printing to somewhere other than stdout. It also consolidates the location of the diff_warn_rename_limit() message with the rest of the CONFLICT and other update messages to all go to the same stream.

---
 merge-ort.c | 9 +++++----
 merge-ort.h | 3 ++-
 2 files changed, 7 insertions(+), 5 deletions(-)
merge-ort.c +3 -2
index 82d2faf5bf9..d28d1721d14 100644
@@ -4236,7 +4236,8 @@ static int record_conflicted_index_entries(struct merge_options *opt)
 }
 
 void merge_display_update_messages(struct merge_options *opt,
-                                struct merge_result *result)
+                                struct merge_result *result,
+                                FILE *stream)
 {
      struct merge_options_internal *opti = result->priv;
      struct hashmap_iter iter;
@@ -4263,13 +4264,13 @@ void merge_display_update_messages(struct merge_options *opt,
      for (i = 0; i < olist.nr; ++i) {
              struct strbuf *sb = olist.items[i].util;
 
-             printf("%s", sb->buf);
+             strbuf_write(sb, stream);
      }
      string_list_clear(&olist, 0);
 
Quoted 8 lines

merge-ort.c void merge_display_update_messages(struct merge_options *opt, · line 4273

Quoted 4 lines
merge-ort.c +1 -1
      /* Also include needed rename limit adjustment now */
      diff_warn_rename_limit("merge.renamelimit",
-                            opti->renames.needed_limit, 0, stderr);
+                            opti->renames.needed_limit, 0, stream);

At the tip of this series I tried to s/stream/stderr/g this, and t4301-merge-tree-write-tree.sh passes, doesn't this warning_fp() special behavior need a test somewhere?

That's a fair point, but...this gets back to my cover letter comments about patches 5, 6, and 8. They implement a code feature that seems useful in general...but which Dscho and Christian didn't like using in this particular command; they just wanted all output on stdout.

So, it's hard to add a test, because there's no code anywhere that exercises it in this series anymore. I originally wanted this feature in my remerge-diff series, but the idea of conflict headers made me punt it to this series. I wanted it for this series, but Dscho and Christian didn't. I could have punted again, but decided the underlying want kept coming up and decided to not excise it -- especially since Dscho was helping improve it. And Junio commented that he liked the idea[1].

[1] https://lore.kernel.org/git/[email protected]/

But yeah, it does leave it feeling slightly odd that we implemented a feature that nothing is currently using. Maybe these 3 should be split off into their own series? Still wouldn't have a test yet, though.

Quoted 5 lines

I assumed that warning_fp() would be using vreportf() in usage.c, but it's not, it's just falling back to the equivalent of fprintf(out, ...), no? I don't really see why 05/15 and parts of 06/15 & this are needed over a much simpler simple helper macro like the below. applied on top of this series.

Quoted 3 lines

That macro is simple? I thought I basically understood Dscho's code, but looking at what you did with diff_warn_rename_limit(), I think I'm lost.

I guess that's a matter of taste, yeah it's a bit of macro soup if you're not familiar with it. FWIW (sans bug I noted below) it's the macro soup we already use for other functions in usage.c.

Quoted 8 lines
Quoted 5 lines

I would get it if the point was to actually use the full usage.c machinery, but we're just either calling warning(), or printing a formatted string to a file FILE *. There's no need to go through usage.c for that, and adding an API to it that behaves like this new warning_fp() is really confusing.

Because the formatted string being printed to the file won't have the same "warning: " prefix that is normally added to stuff in usage?

But the pre-image doesn't add that either. We're just calling vfprintf(), not our own vreportf().

Quoted 3 lines

That's a fair point; that does have a bit of a consistency problem. And I'd rather the messages were consistent regardless of where they are printed.

I think that makes sense, that's why I added die_message() recently. If you meant to print a "warning: " prefix I think it would also be fine in this case to just do it inline. See prior art at:

    git grep '"(fatal|error|warning):' -- '*.c'
Quoted 5 lines
Quoted 3 lines

I.e. an API in usage.c that allowed warning to a given FD would be trying to replace the "2" in the write_in_full() call in vreportf(), I would think.

Hmm, makes sense.

The reason I'm barking up this particular tree is that I've got some upcoming patches for usage.c (the C99-only macro series):
https://lore.kernel.org/git/[email protected]/

It would need to deal with anything in the API. In this case there's not much to deal with, since it's really not at all using the rest of usage.c, it's just a "or to stderr".

Quoted 20 lines
Quoted 20 lines
diff.c +7 -0
index 28368110147..4cf67e93dea 100644
@@ -6377,14 +6377,21 @@ static const char rename_limit_advice[] =
 N_("you may want to set your %s variable to at least "
    "%d and retry the command.");
 
+#define warning_fp(out, fmt, ...) do { \
+       if (out == stderr) \
+               warning(fmt, __VA_ARGS__); \
+       else \
+               fprintf(out, fmt, __VA_ARGS__); \
+} while (0)
+
 void diff_warn_rename_limit(const char *varname, int needed, int degraded_cc,
                            FILE *out)
 {
        fflush(stdout);
        if (degraded_cc)
Quoted 8 lines

diff.c static const char rename_limit_advice[] = · line 6394

Quoted 5 lines
diff.c +2 -2
-               warning_fp(out, _(degrade_cc_to_c_warning));
+               warning_fp(out, _(degrade_cc_to_c_warning), NULL);
        else if (needed)
-               warning_fp(out, _(rename_limit_warning));
+               warning_fp(out, _(rename_limit_warning), NULL);

Why do the only callers have a NULL parameter here? Is this one of those va_list/va_args things I never bothered to properly learn?

That's wrong (I blame tiredness last night),an actual working version is produced below. Clang accepted my broken code, but gcc rightly yells about it:

diff.c +7 -0
index 28368110147..a2bc2595533 100644
@@ -6377,6 +6377,13 @@ static const char rename_limit_advice[] =
 N_("you may want to set your %s variable to at least "
    "%d and retry the command.");
 
+#define warning_fp(out, ...) do { \
+	if (out == stderr) \
+		warning(__VA_ARGS__); \
+	else \
+		fprintf(out, __VA_ARGS__); \
+} while (0)
+
 void diff_warn_rename_limit(const char *varname, int needed, int degraded_cc,
 			    FILE *out)
 {
git-compat-util.h +0 -1
index 64ba60e5c71..d70ce142861 100644
@@ -475,7 +475,6 @@ int error(const char *err, ...) __attribute__((format (printf, 1, 2)));
 int error_errno(const char *err, ...) __attribute__((format (printf, 1, 2)));
 void warning(const char *err, ...) __attribute__((format (printf, 1, 2)));
 void warning_errno(const char *err, ...) __attribute__((format (printf, 1, 2)));
-void warning_fp(FILE *out, const char *warn, ...) __attribute__((format (printf, 2, 3)));
 
 #ifndef NO_OPENSSL
 #ifdef APPLE_COMMON_CRYPTO
usage.c +0 -14
index 0bfd2c603c0..c7d233b0de9 100644
@@ -253,20 +253,6 @@ void warning(const char *warn, ...)
 	va_end(params);
 }
 
-void warning_fp(FILE *out, const char *warn, ...)
-{
-	va_list params;
-
-	va_start(params, warn);
-	if (out == stderr)
-		warn_routine(warn, params);
-	else {
-		vfprintf(out, warn, params);
-		fputc('\n', out);
-	}
-	va_end(params);
-}
-
 /* Only set this, ever, from t/helper/, when verifying that bugs are caught. */
 int BUG_exit_code;
 

I do think you'd probably prefer the non-macro version, which is pretty much just going back to this:
https://lore.kernel.org/git/6fb4f4580a581b2e43bc4b8deaa3d2d2bf4a8756.1643479633.git.gitgitgadget@gmail.com/

diff.c +15 -4
index 28368110147..21c9561f546 100644
@@ -6380,17 +6380,28 @@ N_("you may want to set your %s variable to at least "
 void diff_warn_rename_limit(const char *varname, int needed, int degraded_cc,
 			    FILE *out)
 {
+	const char *msg;
+
 	fflush(stdout);
 	if (degraded_cc)
-		warning_fp(out, _(degrade_cc_to_c_warning));
+		msg = _(degrade_cc_to_c_warning);
 	else if (needed)
-		warning_fp(out, _(rename_limit_warning));
+		msg = _(rename_limit_warning);
 	else
 		return;
 
+	if (out == stderr)
+		warning("%s", msg);
+	else
+		fprintf(stderr, "%s", msg);
 
-	if (0 < needed)
-		warning_fp(out, _(rename_limit_advice), varname, needed);
+	if (0 >= needed)
+		return;
+
+	if (out == stderr)
+		warning(_(rename_limit_advice), varname, needed);
+	else
+		fprintf(stderr, _(rename_limit_advice), varname, needed);
 }
 
 static void create_filepairs_for_header_only_notifications(struct diff_options *o)
git-compat-util.h +0 -1
index 64ba60e5c71..d70ce142861 100644
@@ -475,7 +475,6 @@ int error(const char *err, ...) __attribute__((format (printf, 1, 2)));
 int error_errno(const char *err, ...) __attribute__((format (printf, 1, 2)));
 void warning(const char *err, ...) __attribute__((format (printf, 1, 2)));
 void warning_errno(const char *err, ...) __attribute__((format (printf, 1, 2)));
-void warning_fp(FILE *out, const char *warn, ...) __attribute__((format (printf, 2, 3)));
 
 #ifndef NO_OPENSSL
 #ifdef APPLE_COMMON_CRYPTO
usage.c +0 -14
index 0bfd2c603c0..c7d233b0de9 100644
@@ -253,20 +253,6 @@ void warning(const char *warn, ...)
 	va_end(params);
 }
 
-void warning_fp(FILE *out, const char *warn, ...)
-{
-	va_list params;
-
-	va_start(params, warn);
-	if (out == stderr)
-		warn_routine(warn, params);
-	else {
-		vfprintf(out, warn, params);
-		fputc('\n', out);
-	}
-	va_end(params);
-}
-
 /* Only set this, ever, from t/helper/, when verifying that bugs are caught. */
 int BUG_exit_code;
 

Note that both your pre-image, my macro version and Johannes's linked-to-above are technically buggy in that they treat a non-formatting format as a formatting format. I.e. we should use warning("%s", msg) in that case, not warning(msg).

See 927dc330705 (advice.h: add missing __attribute__((format)) & fix usage, 2021-07-13) for a similar bug/fix.

#156 lore raw

Re: [PATCH v3 08/15] merge-ort: allow update messages to be written to different file stream

Quoted 4 lines · Ævar Arnfjörð Bjarmason
Quoted 1 line · Elijah Newren

On Wed, Feb 2, 2022 at 6:01 PM Ævar Arnfjörð Bjarmason <[email protected]> wrote:

[...]

Quoted 11 lines
Quoted 8 lines
Quoted 5 lines

I would get it if the point was to actually use the full usage.c machinery, but we're just either calling warning(), or printing a formatted string to a file FILE *. There's no need to go through usage.c for that, and adding an API to it that behaves like this new warning_fp() is really confusing.

Because the formatted string being printed to the file won't have the same "warning: " prefix that is normally added to stuff in usage?

But the pre-image doesn't add that either. We're just calling vfprintf(), not our own vreportf().

Right, I'm saying that I thought you were reporting the original patch as buggy because it doesn't produce the same message when given a different stream; it'll omit the "warning: " prefix. And I was agreeing that it was buggy for those reasons.

Or was there a different reason you didn't like that function being in usage.c?

Quoted 9 lines
Quoted 3 lines

That's a fair point; that does have a bit of a consistency problem. And I'd rather the messages were consistent regardless of where they are printed.

I think that makes sense, that's why I added die_message() recently. If you meant to print a "warning: " prefix I think it would also be fine in this case to just do it inline. See prior art at:

    git grep '"(fatal|error|warning):' -- '*.c'

So, making diff_warn_rename_limit() stop using warning(), and just always directly writing out and including "warning:" in its message?

I'm wondering if that might cause problems if there are any existing callers of diff_warn_rename_limit() that might also be using set_warn_routine() (e.g. perhaps apply.c?). Of course, those callers probably couldn't handle anything other than the default stream. Hmm...

Quoted 29 lines
Quoted 20 lines
Quoted 20 lines
diff.c +7 -0
index 28368110147..4cf67e93dea 100644
@@ -6377,14 +6377,21 @@ static const char rename_limit_advice[] =
 N_("you may want to set your %s variable to at least "
    "%d and retry the command.");
 
+#define warning_fp(out, fmt, ...) do { \
+       if (out == stderr) \
+               warning(fmt, __VA_ARGS__); \
+       else \
+               fprintf(out, fmt, __VA_ARGS__); \
+} while (0)
+
 void diff_warn_rename_limit(const char *varname, int needed, int degraded_cc,
                            FILE *out)
 {
        fflush(stdout);
        if (degraded_cc)
Quoted 8 lines

diff.c N_("you may want to set your %s variable to at least "

Quoted 5 lines
diff.c +2 -2
-               warning_fp(out, _(degrade_cc_to_c_warning));
+               warning_fp(out, _(degrade_cc_to_c_warning), NULL);
        else if (needed)
-               warning_fp(out, _(rename_limit_warning));
+               warning_fp(out, _(rename_limit_warning), NULL);

Why do the only callers have a NULL parameter here? Is this one of those va_list/va_args things I never bothered to properly learn?

Quoted 3 lines

That's wrong (I blame tiredness last night),an actual working version is produced below. Clang accepted my broken code, but gcc rightly yells about it:

Well, seeing the new code makes me feel better as it makes more sense to me now. ;-)

Quoted 7 lines

Note that both your pre-image, my macro version and Johannes's linked-to-above are technically buggy in that they treat a non-formatting format as a formatting format. I.e. we should use warning("%s", msg) in that case, not warning(msg).

See 927dc330705 (advice.h: add missing __attribute__((format)) & fix usage, 2021-07-13) for a similar bug/fix.

Good point.

Man, what a can of worms this all is. Maybe I really should just drop patches 5, 6, and 8 for now...

#157 lore raw

Re: [PATCH v3 08/15] merge-ort: allow update messages to be written to different file stream

Quoted 18 lines · Elijah Newren
Quoted 4 lines · Ævar Arnfjörð Bjarmason
Quoted 1 line · Elijah Newren

On Wed, Feb 2, 2022 at 6:01 PM Ævar Arnfjörð Bjarmason <[email protected]> wrote:

[...]

Quoted 11 lines
Quoted 8 lines
Quoted 5 lines

I would get it if the point was to actually use the full usage.c machinery, but we're just either calling warning(), or printing a formatted string to a file FILE *. There's no need to go through usage.c for that, and adding an API to it that behaves like this new warning_fp() is really confusing.

Because the formatted string being printed to the file won't have the same "warning: " prefix that is normally added to stuff in usage?

But the pre-image doesn't add that either. We're just calling vfprintf(), not our own vreportf().

Quoted 6 lines

Right, I'm saying that I thought you were reporting the original patch as buggy because it doesn't produce the same message when given a different stream; it'll omit the "warning: " prefix. And I was agreeing that it was buggy for those reasons.

Or was there a different reason you didn't like that function being in usage.c?

Maybe it was accidentally a bug report :) But no, I was just observing that it was odd that it was in usage.c when it seemingly had almost nothing to do with what that API accomplishes.

But maybe the underlying issue is that the "warning: " part is missing here. But I didn't mean to report that/missed it.

Quoted 10 lines
Quoted 9 lines
Quoted 3 lines

That's a fair point; that does have a bit of a consistency problem. And I'd rather the messages were consistent regardless of where they are printed.

I think that makes sense, that's why I added die_message() recently. If you meant to print a "warning: " prefix I think it would also be fine in this case to just do it inline. See prior art at:

    git grep '"(fatal|error|warning):' -- '*.c'
Quoted 8 lines

So, making diff_warn_rename_limit() stop using warning(), and just always directly writing out and including "warning:" in its message?

I'm wondering if that might cause problems if there are any existing callers of diff_warn_rename_limit() that might also be using set_warn_routine() (e.g. perhaps apply.c?). Of course, those callers probably couldn't handle anything other than the default stream. Hmm...

Using set_warn_routine() is the "right" way to do it currently, and with or without a "warning: " prefix the current API use is "wrong" if the purpose is to have it behave nicely with the pluggable usage.c API.

But of course that may not be the goal at all, i.e. I think here we've probably stopped caring about usage.c's formatting, logging etc. entirely, and are just emitting a string.

Just like serve.c emits "E <msg>" or whatever (and not with error()).

Quoted 41 lines
Quoted 29 lines
Quoted 20 lines
Quoted 20 lines
diff.c +7 -0
index 28368110147..4cf67e93dea 100644
@@ -6377,14 +6377,21 @@ static const char rename_limit_advice[] =
 N_("you may want to set your %s variable to at least "
    "%d and retry the command.");
 
+#define warning_fp(out, fmt, ...) do { \
+       if (out == stderr) \
+               warning(fmt, __VA_ARGS__); \
+       else \
+               fprintf(out, fmt, __VA_ARGS__); \
+} while (0)
+
 void diff_warn_rename_limit(const char *varname, int needed, int degraded_cc,
                            FILE *out)
 {
        fflush(stdout);
        if (degraded_cc)
Quoted 8 lines

diff.c N_("you may want to set your %s variable to at least "

Quoted 5 lines
diff.c +2 -2
-               warning_fp(out, _(degrade_cc_to_c_warning));
+               warning_fp(out, _(degrade_cc_to_c_warning), NULL);
        else if (needed)
-               warning_fp(out, _(rename_limit_warning));
+               warning_fp(out, _(rename_limit_warning), NULL);

Why do the only callers have a NULL parameter here? Is this one of those va_list/va_args things I never bothered to properly learn?

Quoted 3 lines

That's wrong (I blame tiredness last night),an actual working version is produced below. Clang accepted my broken code, but gcc rightly yells about it:

Well, seeing the new code makes me feel better as it makes more sense to me now. ;-)

Quoted 5 lines

Note that both your pre-image, my macro version and Johannes's linked-to-above are technically buggy in that they treat a non-formatting format as a formatting format. I.e. we should use warning("%s", msg) in that case, not warning(msg).

Quoted 7 lines
Quoted 2 lines

See 927dc330705 (advice.h: add missing __attribute__((format)) & fix usage, 2021-07-13) for a similar bug/fix.

Good point.

Man, what a can of worms this all is. Maybe I really should just drop patches 5, 6, and 8 for now...

Yeah, I really think it's worth it to just sprinkle a tiny bit of if/else (or a macro) here and print to stderr inline or not. We can make some use of some usage.c when there's good reason to do so, but this bit just seems like a needless digression.

I hope all of this has helped somewhat ...

#158 lore raw

Re: [PATCH v3 08/15] merge-ort: allow update messages to be written to different file stream

Quoted 113 lines · Ævar Arnfjörð Bjarmason
Quoted 18 lines · Elijah Newren
Quoted 4 lines · Ævar Arnfjörð Bjarmason
Quoted 1 line · Elijah Newren

On Wed, Feb 2, 2022 at 6:01 PM Ævar Arnfjörð Bjarmason <[email protected]> wrote:

[...]

Quoted 11 lines
Quoted 8 lines
Quoted 5 lines

I would get it if the point was to actually use the full usage.c machinery, but we're just either calling warning(), or printing a formatted string to a file FILE *. There's no need to go through usage.c for that, and adding an API to it that behaves like this new warning_fp() is really confusing.

Because the formatted string being printed to the file won't have the same "warning: " prefix that is normally added to stuff in usage?

But the pre-image doesn't add that either. We're just calling vfprintf(), not our own vreportf().

Quoted 6 lines

Right, I'm saying that I thought you were reporting the original patch as buggy because it doesn't produce the same message when given a different stream; it'll omit the "warning: " prefix. And I was agreeing that it was buggy for those reasons.

Or was there a different reason you didn't like that function being in usage.c?

Maybe it was accidentally a bug report :) But no, I was just observing that it was odd that it was in usage.c when it seemingly had almost nothing to do with what that API accomplishes.

But maybe the underlying issue is that the "warning: " part is missing here. But I didn't mean to report that/missed it.

Quoted 10 lines
Quoted 9 lines
Quoted 3 lines

That's a fair point; that does have a bit of a consistency problem. And I'd rather the messages were consistent regardless of where they are printed.

I think that makes sense, that's why I added die_message() recently. If you meant to print a "warning: " prefix I think it would also be fine in this case to just do it inline. See prior art at:

    git grep '"(fatal|error|warning):' -- '*.c'
Quoted 8 lines

So, making diff_warn_rename_limit() stop using warning(), and just always directly writing out and including "warning:" in its message?

I'm wondering if that might cause problems if there are any existing callers of diff_warn_rename_limit() that might also be using set_warn_routine() (e.g. perhaps apply.c?). Of course, those callers probably couldn't handle anything other than the default stream. Hmm...

Using set_warn_routine() is the "right" way to do it currently, and with or without a "warning: " prefix the current API use is "wrong" if the purpose is to have it behave nicely with the pluggable usage.c API.

But of course that may not be the goal at all, i.e. I think here we've probably stopped caring about usage.c's formatting, logging etc. entirely, and are just emitting a string.

Just like serve.c emits "E <msg>" or whatever (and not with error()).

Quoted 41 lines
Quoted 29 lines
Quoted 20 lines
Quoted 20 lines
diff.c +7 -0
index 28368110147..4cf67e93dea 100644
@@ -6377,14 +6377,21 @@ static const char rename_limit_advice[] =
 N_("you may want to set your %s variable to at least "
    "%d and retry the command.");
 
+#define warning_fp(out, fmt, ...) do { \
+       if (out == stderr) \
+               warning(fmt, __VA_ARGS__); \
+       else \
+               fprintf(out, fmt, __VA_ARGS__); \
+} while (0)
+
 void diff_warn_rename_limit(const char *varname, int needed, int degraded_cc,
                            FILE *out)
 {
        fflush(stdout);
        if (degraded_cc)
Quoted 8 lines

diff.c N_("you may want to set your %s variable to at least "

Quoted 5 lines
diff.c +2 -2
-               warning_fp(out, _(degrade_cc_to_c_warning));
+               warning_fp(out, _(degrade_cc_to_c_warning), NULL);
        else if (needed)
-               warning_fp(out, _(rename_limit_warning));
+               warning_fp(out, _(rename_limit_warning), NULL);

Why do the only callers have a NULL parameter here? Is this one of those va_list/va_args things I never bothered to properly learn?

Quoted 3 lines

That's wrong (I blame tiredness last night),an actual working version is produced below. Clang accepted my broken code, but gcc rightly yells about it:

Well, seeing the new code makes me feel better as it makes more sense to me now. ;-)

Quoted 5 lines

Note that both your pre-image, my macro version and Johannes's linked-to-above are technically buggy in that they treat a non-formatting format as a formatting format. I.e. we should use warning("%s", msg) in that case, not warning(msg).

Quoted 7 lines
Quoted 2 lines

See 927dc330705 (advice.h: add missing __attribute__((format)) & fix usage, 2021-07-13) for a similar bug/fix.

Good point.

Man, what a can of worms this all is. Maybe I really should just drop patches 5, 6, and 8 for now...

Quoted 6 lines

Yeah, I really think it's worth it to just sprinkle a tiny bit of if/else (or a macro) here and print to stderr inline or not. We can make some use of some usage.c when there's good reason to do so, but this bit just seems like a needless digression.

I hope all of this has helped somewhat ...

Absolutely; thanks for reviewing! These parts may just end up in me dropping some patches for now (since they're not actually being used anyway), but I think it's all good feedback.

#159 lore raw

Re: [PATCH v3 08/15] merge-ort: allow update messages to be written to different file stream

Hi Elijah,

Quoted 12 lines · Elijah Newren
Quoted 11 lines · Ævar Arnfjörð Bjarmason
Quoted 2 lines · Elijah Newren

Man, what a can of worms this all is. Maybe I really should just drop patches 5, 6, and 8 for now...

Yeah, I really think it's worth it to just sprinkle a tiny bit of if/else (or a macro) here and print to stderr inline or not. We can make some use of some usage.c when there's good reason to do so, but this bit just seems like a needless digression.

Quoted 5 lines
Quoted 1 line

I hope all of this has helped somewhat ...

Absolutely; thanks for reviewing! These parts may just end up in me dropping some patches for now (since they're not actually being used anyway), but I think it's all good feedback.

So we dropped some useful patches future-proofing merge-tree for the sake of appeasing a refactoring with no immediately obvious benefit? I really don't like that direction.

Ciao,
Dscho

#160 lore raw

Re: [PATCH v3 08/15] merge-ort: allow update messages to be written to different file stream

Quoted 19 lines · Johannes Schindelin

Hi Elijah,

Quoted 14 lines · Elijah Newren
Quoted 12 lines · Ævar Arnfjörð Bjarmason
Quoted 2 lines · Elijah Newren

Man, what a can of worms this all is. Maybe I really should just drop patches 5, 6, and 8 for now...

Yeah, I really think it's worth it to just sprinkle a tiny bit of if/else (or a macro) here and print to stderr inline or not. We can make some use of some usage.c when there's good reason to do so, but this bit just seems like a needless digression.

I hope all of this has helped somewhat ...

Quoted 7 lines
Quoted 3 lines

Absolutely; thanks for reviewing! These parts may just end up in me dropping some patches for now (since they're not actually being used anyway), but I think it's all good feedback.

So we dropped some useful patches future-proofing merge-tree for the sake of appeasing a refactoring with no immediately obvious benefit? I really don't like that direction.

Even before any of Ævar's comments, I had already noted on my cover letter[1] that "to be honest, patches 5, 6, & 8 may be less relevant since we're now including these messages on stdout anyway" -- so I was already wondering if I should defer them to some future series. Then when Ævar reviewed the series, he noted (1) that I lacked tests of these changes (which is true, because nothing uses them, and I can't easily add a test as I have no current usecase in mind), and (2) these patches would print a "warning: " prefix when printing to stdout but not print such a prefix otherwise, which felt inconsistent. Those seemed to reinforce the comment I had already made that these changes were unused in my series and maybe should be separated. I still like the general idea behind the future proofing you did here, so maybe I was just being lazy, but between those factors I decided that punting until later made sense.

[1] https://lore.kernel.org/git/[email protected]/

#161 lore raw

Re: [PATCH v3 08/15] merge-ort: allow update messages to be written to different file stream

Hi Elijah,

Quoted 25 lines · Elijah Newren
Quoted 23 lines · Johannes Schindelin

Hi Elijah,

Quoted 17 lines · Elijah Newren
Quoted 6 lines · Ævar Arnfjörð Bjarmason
Quoted 2 lines · Elijah Newren

Man, what a can of worms this all is. Maybe I really should just drop patches 5, 6, and 8 for now...

Quoted 6 lines

Yeah, I really think it's worth it to just sprinkle a tiny bit of if/else (or a macro) here and print to stderr inline or not. We can make some use of some usage.c when there's good reason to do so, but this bit just seems like a needless digression.

I hope all of this has helped somewhat ...

Absolutely; thanks for reviewing! These parts may just end up in me dropping some patches for now (since they're not actually being used anyway), but I think it's all good feedback.

Quoted 8 lines
Quoted 3 lines

So we dropped some useful patches future-proofing merge-tree for the sake of appeasing a refactoring with no immediately obvious benefit? I really don't like that direction.

Even before any of Ævar's comments, I had already noted on my cover letter[1] that "to be honest, patches 5, 6, & 8 may be less relevant since we're now including these messages on stdout anyway" -- so I was already wondering if I should defer them to some future series.

Ah, that was not clear to me. In that case, I retract my objections.

Thanks for clarifying,
Dscho

#162 lore raw

PATCH v3 9/15 merge-tree: support including merge messages in output

In reply to Elijah Newren via GitGitGadget

From: Elijah Newren <[email protected]>

When running git merge-tree --write-tree, we previously would only return an exit status reflecting the cleanness of a merge, and print out the toplevel tree of the resulting merge. Merges also have informational messages, such as:
* "Auto-merging <PATH>"
* "CONFLICT (content): ..."
* "CONFLICT (file/directory)"
* etc.
In fact, when non-content conflicts occur (such as file/directory, modify/delete, add/add with differing modes, rename/rename (1to2), etc.), these informational messages may be the only notification the user gets since these conflicts are not representable in the contents of the file.

Add a --[no-]messages option so that callers can request these messages be included at the end of the output. Include such messages by default when there are conflicts, and omit them by default when the merge is clean.

---
 Documentation/git-merge-tree.txt | 45 +++++++++++++++++++++++++++-----
 builtin/merge-tree.c             | 19 ++++++++++++--
 t/t4301-merge-tree-write-tree.sh | 21 +++++++++++++++
 3 files changed, 76 insertions(+), 9 deletions(-)
Documentation/git-merge-tree.txt +38 -7
index 569485815a0..42e0f8f6183 100644
@@ -9,7 +9,7 @@ git-merge-tree - Perform merge without touching index or working tree
 SYNOPSIS
 --------
 [verse]
-'git merge-tree' [--write-tree] <branch1> <branch2>
+'git merge-tree' [--write-tree] [<options>] <branch1> <branch2>
 'git merge-tree' [--trivial-merge] <base-tree> <branch1> <branch2> (deprecated)
 
 DESCRIPTION
@@ -35,17 +35,47 @@ merge is distinguished from a trivial merge in that it includes:
 After the merge completes, it will create a new toplevel tree object.
 See `OUTPUT` below for details.
 
+OPTIONS
+-------
+
+--[no-]messages::
+	Write any informational messages such as "Auto-merging <path>"
+	or CONFLICT notices to the end of stdout.  If unspecified, the
+	default is to include these messages if there are merge
+	conflicts, and to omit them otherwise.
+
 OUTPUT
 ------
 
-For either a successful or conflicted merge, the output from
-git-merge-tree is simply one line:
+By default, for a successful merge, the output from git-merge-tree is
+simply one line:
+
+	<OID of toplevel tree>
+
+Whereas for a conflicted merge, the output is by default of the form:
 
 	<OID of toplevel tree>
+	<Informational messages>
+
+These are discussed individually below.
+
+OID of toplevel tree
+~~~~~~~~~~~~~~~~~~~~
+
+This is a tree object that represents what would be checked out in the
+working tree at the end of `git merge`.  If there were conflicts, then
+files within this tree may have embedded conflict markers.
+
+Informational messages
+~~~~~~~~~~~~~~~~~~~~~~
+
+This always starts with a blank line to separate it from the previous
+section, and then has free-form messages about the merge, such as:
 
-The printed tree object corresponds to what would be checked out in
-the working tree at the end of `git merge`, and thus may have files
-with conflict markers in them.
+  * "Auto-merging <file>"
+  * "CONFLICT (rename/delete): <oldfile> renamed...but deleted in..."
+  * "Failed to merge submodule <submodule> (<reason>)"
+  * "Warning: cannot merge binary files: <filename>"
 
 EXIT STATUS
 -----------
@@ -69,7 +99,8 @@ be used as a part of a series of steps such as
 
 However, it does not quite fit into the same category of low-level
 plumbing commands since the possibility of merge conflicts give it a
-much higher chance of the command not succeeding.
+much higher chance of the command not succeeding (and NEWTREE containing
+a bunch of stuff other than just a toplevel tree).
 
 GIT
 ---
builtin/merge-tree.c +17 -2
index d14c9f6e44e..6a556ab1c9c 100644
@@ -390,6 +390,7 @@ static int trivial_merge(const char *base,
 
 struct merge_tree_options {
 	int mode;
+	int show_messages;
 };
 
 static int real_merge(struct merge_tree_options *o,
@@ -432,18 +433,27 @@ static int real_merge(struct merge_tree_options *o,
 	merge_incore_recursive(&opt, merge_bases, parent1, parent2, &result);
 	if (result.clean < 0)
 		die(_("failure to merge"));
+
+	if (o->show_messages == -1)
+		o->show_messages = !result.clean;
+
 	puts(oid_to_hex(&result.tree->object.oid));
+	if (o->show_messages) {
+		printf("\n");
+		merge_display_update_messages(&opt, &result, stdout);
+	}
 	merge_finalize(&opt, &result);
 	return !result.clean; /* result.clean < 0 handled above */
 }
 
 int cmd_merge_tree(int argc, const char **argv, const char *prefix)
 {
-	struct merge_tree_options o = { 0 };
+	struct merge_tree_options o = { .show_messages = -1 };
 	int expected_remaining_argc;
+	int original_argc;
 
 	const char * const merge_tree_usage[] = {
-		N_("git merge-tree [--write-tree] <branch1> <branch2>"),
+		N_("git merge-tree [--write-tree] [<options>] <branch1> <branch2>"),
 		N_("git merge-tree [--trivial-merge] <base-tree> <branch1> <branch2>"),
 		NULL
 	};
@@ -453,10 +463,13 @@ int cmd_merge_tree(int argc, const char **argv, const char *prefix)
 			    'w'),
 		OPT_CMDMODE(0, "trivial-merge", &o.mode,
 			    N_("do a trivial merge only"), 't'),
+		OPT_BOOL(0, "messages", &o.show_messages,
+			 N_("also show informational/conflict messages")),
 		OPT_END()
 	};
 
 	/* Parse arguments */
+	original_argc = argc;
 	argc = parse_options(argc, argv, prefix, mt_options,
 			     merge_tree_usage, PARSE_OPT_STOP_AT_NON_OPTION);
 	if (o.mode) {
@@ -468,6 +481,8 @@ int cmd_merge_tree(int argc, const char **argv, const char *prefix)
 			usage_with_options(merge_tree_usage, mt_options);
 		o.mode = (argc == 2 ? 'w' : 't');
 	}
+	if (o.mode == 't' && original_argc < argc)
+		die(_("--trivial-merge is incompatible with all other options"));
 
 	/* Do the relevant type of merge */
 	if (o.mode == 'w')
t/t4301-merge-tree-write-tree.sh +21 -0
index 66c3eaf2021..e2255711f9c 100755
@@ -85,4 +85,25 @@ test_expect_success 'Barf on too many arguments' '
 	grep "^usage: git merge-tree" expect
 '
 
+test_expect_success 'test conflict notices and such' '
+	test_expect_code 1 git merge-tree --write-tree side1 side2 >out &&
+	sed -e "s/[0-9a-f]\{40,\}/HASH/g" out >actual &&
+
+	# Expected results:
+	#   "greeting" should merge with conflicts
+	#   "numbers" should merge cleanly
+	#   "whatever" has *both* a modify/delete and a file/directory conflict
+	cat <<-EOF >expect &&
+	HASH
+
+	Auto-merging greeting
+	CONFLICT (content): Merge conflict in greeting
+	Auto-merging numbers
+	CONFLICT (file/directory): directory in the way of whatever from side1; moving it to whatever~side1 instead.
+	CONFLICT (modify/delete): whatever~side1 deleted in side2 and modified in side1.  Version side1 of whatever~side1 left in tree.
+	EOF
+
+	test_cmp expect actual
+'
+
 test_done
Signature · 1 line
gitgitgadget
#163 lore raw

PATCH v3 10/15 merge-ort: provide a merge_get_conflicted_files() helper function

In reply to Elijah Newren via GitGitGadget

From: Elijah Newren <[email protected]>

After a merge, this function allows the user to extract the same information that would be printed by ls-files -u, which means files with their mode, oid, and stage.

---
 merge-ort.c | 31 +++++++++++++++++++++++++++++++
 merge-ort.h | 21 +++++++++++++++++++++
 2 files changed, 52 insertions(+)
merge-ort.c +31 -0
index d28d1721d14..c4ce6027dc4 100644
@@ -4275,6 +4275,37 @@ void merge_display_update_messages(struct merge_options *opt,
 	trace2_region_leave("merge", "display messages", opt->repo);
 }
 
+void merge_get_conflicted_files(struct merge_result *result,
+				struct string_list *conflicted_files)
+{
+	struct hashmap_iter iter;
+	struct strmap_entry *e;
+	struct merge_options_internal *opti = result->priv;
+
+	strmap_for_each_entry(&opti->conflicted, &iter, e) {
+		const char *path = e->key;
+		struct conflict_info *ci = e->value;
+		int i;
+
+		VERIFY_CI(ci);
+
+		for (i = MERGE_BASE; i <= MERGE_SIDE2; i++) {
+			struct stage_info *si;
+
+			if (!(ci->filemask & (1ul << i)))
+				continue;
+
+			si = xmalloc(sizeof(*si));
+			si->stage = i+1;
+			si->mode = ci->stages[i].mode;
+			oidcpy(&si->oid, &ci->stages[i].oid);
+			string_list_append(conflicted_files, path)->util = si;
+		}
+	}
+	/* string_list_sort() uses a stable sort, so we're good */
+	string_list_sort(conflicted_files);
+}
+
 void merge_switch_to_result(struct merge_options *opt,
 			    struct tree *head,
 			    struct merge_result *result,
merge-ort.h +21 -0
index d643b47cb7c..e635a294ea8 100644
@@ -2,6 +2,7 @@
 #define MERGE_ORT_H
 
 #include "merge-recursive.h"
+#include "hash.h"
 
 struct commit;
 struct tree;
@@ -89,6 +90,26 @@ void merge_display_update_messages(struct merge_options *opt,
 				   struct merge_result *result,
 				   FILE *stream);
 
+struct stage_info {
+	struct object_id oid;
+	int mode;
+	int stage;
+};
+
+/*
+ * Provide a list of path -> {struct stage_info*} mappings for
+ * all conflicted files.  Note that each path could appear up to three
+ * times in the list, corresponding to 3 different stage entries.  In short,
+ * this basically provides the info that would be printed by `ls-files -u`.
+ *
+ * result should have been populated by a call to
+ * one of the merge_incore_[non]recursive() functions.
+ *
+ * conflicted_files should be empty before calling this function.
+ */
+void merge_get_conflicted_files(struct merge_result *result,
+				struct string_list *conflicted_files);
+
 /* Do needed cleanup when not calling merge_switch_to_result() */
 void merge_finalize(struct merge_options *opt,
 		    struct merge_result *result);
Signature · 1 line
gitgitgadget
#164 lore raw

PATCH v3 11/15 merge-tree: provide a list of which files have conflicts

In reply to Elijah Newren via GitGitGadget

From: Elijah Newren <[email protected]>

Callers of git merge-tree --write-tree will often want to know which files had conflicts. While they could potentially attempt to parse the CONFLICT notices printed, those messages are not meant to be machine readable. Provide a simpler mechanism of just printing the files (in the same format as git ls-files with quoting, but restricted to unmerged files) in the output before the free-form messages.

---
 Documentation/git-merge-tree.txt |  8 ++++++++
 builtin/merge-tree.c             | 24 ++++++++++++++++++++++--
 t/t4301-merge-tree-write-tree.sh | 11 +++++++++++
 3 files changed, 41 insertions(+), 2 deletions(-)
Documentation/git-merge-tree.txt +8 -0
index 42e0f8f6183..160e8f44b62 100644
@@ -55,6 +55,7 @@ simply one line:
 Whereas for a conflicted merge, the output is by default of the form:
 
 	<OID of toplevel tree>
+	<Conflicted file list>
 	<Informational messages>
 
 These are discussed individually below.
@@ -66,6 +67,13 @@ This is a tree object that represents what would be checked out in the
 working tree at the end of `git merge`.  If there were conflicts, then
 files within this tree may have embedded conflict markers.
 
+Conflicted file list
+~~~~~~~~~~~~~~~~~~~~
+
+This is a sequence of lines containing a filename on each line, quoted
+as explained for the configuration variable `core.quotePath` (see
+linkgit:git-config[1]).
+
 Informational messages
 ~~~~~~~~~~~~~~~~~~~~~~
 
builtin/merge-tree.c +22 -2
index 6a556ab1c9c..54dae018203 100644
@@ -11,6 +11,9 @@
 #include "blob.h"
 #include "exec-cmd.h"
 #include "merge-blobs.h"
+#include "quote.h"
+
+static int line_termination = '\n';
 
 struct merge_list {
 	struct merge_list *next;
@@ -394,7 +397,8 @@ struct merge_tree_options {
 };
 
 static int real_merge(struct merge_tree_options *o,
-		      const char *branch1, const char *branch2)
+		      const char *branch1, const char *branch2,
+		      const char *prefix)
 {
 	struct commit *parent1, *parent2;
 	struct commit_list *common;
@@ -438,6 +442,22 @@ static int real_merge(struct merge_tree_options *o,
 		o->show_messages = !result.clean;
 
 	puts(oid_to_hex(&result.tree->object.oid));
+	if (!result.clean) {
+		struct string_list conflicted_files = STRING_LIST_INIT_NODUP;
+		const char *last = NULL;
+		int i;
+
+		merge_get_conflicted_files(&result, &conflicted_files);
+		for (i = 0; i < conflicted_files.nr; i++) {
+			const char *name = conflicted_files.items[i].string;
+			if (last && !strcmp(last, name))
+				continue;
+			write_name_quoted_relative(
+				name, prefix, stdout, line_termination);
+			last = name;
+		}
+		string_list_clear(&conflicted_files, 1);
+	}
 	if (o->show_messages) {
 		printf("\n");
 		merge_display_update_messages(&opt, &result, stdout);
@@ -486,7 +506,7 @@ int cmd_merge_tree(int argc, const char **argv, const char *prefix)
 
 	/* Do the relevant type of merge */
 	if (o.mode == 'w')
-		return real_merge(&o, argv[0], argv[1]);
+		return real_merge(&o, argv[0], argv[1], prefix);
 	else
 		return trivial_merge(argv[0], argv[1], argv[2]);
 }
t/t4301-merge-tree-write-tree.sh +11 -0
index e2255711f9c..7113d060bc5 100755
@@ -95,6 +95,8 @@ test_expect_success 'test conflict notices and such' '
 	#   "whatever" has *both* a modify/delete and a file/directory conflict
 	cat <<-EOF >expect &&
 	HASH
+	greeting
+	whatever~side1
 
 	Auto-merging greeting
 	CONFLICT (content): Merge conflict in greeting
@@ -106,4 +108,13 @@ test_expect_success 'test conflict notices and such' '
 	test_cmp expect actual
 '
 
+test_expect_success 'Just the conflicted files without the messages' '
+	test_expect_code 1 git merge-tree --write-tree --no-messages side1 side2 >out &&
+	sed -e "s/[0-9a-f]\{40,\}/HASH/g" out >actual &&
+
+	test_write_lines HASH greeting whatever~side1 >expect &&
+
+	test_cmp expect actual
+'
+
 test_done
Signature · 1 line
gitgitgadget
#165 lore raw

PATCH v3 12/15 merge-tree: provide easy access to `ls-files -u` style info

In reply to Elijah Newren via GitGitGadget

From: Elijah Newren <[email protected]>

Much like git merge updates the index with information of the form
(mode, oid, stage, name)
provide this output for conflicted files for merge-tree as well. Provide an --exclude-modes-oids-stages/-l option for users to exclude the mode, oid, and stage and only get the list of conflicted filenames.

---
 Documentation/git-merge-tree.txt | 30 ++++++++++++++++++++++++------
 builtin/merge-tree.c             | 11 ++++++++++-
 t/t4301-merge-tree-write-tree.sh | 26 ++++++++++++++++++++++++--
 3 files changed, 58 insertions(+), 9 deletions(-)
Documentation/git-merge-tree.txt +24 -6
index 160e8f44b62..55bb7bc61c1 100644
@@ -38,6 +38,11 @@ See `OUTPUT` below for details.
 OPTIONS
 -------
 
+--exclude-oids-and-modes::
+	Instead of writing a list of (mode, oid, stage, path) tuples
+	to output for conflicted files, just provide a list of
+	filenames with conflicts.
+
 --[no-]messages::
 	Write any informational messages such as "Auto-merging <path>"
 	or CONFLICT notices to the end of stdout.  If unspecified, the
@@ -55,7 +60,7 @@ simply one line:
 Whereas for a conflicted merge, the output is by default of the form:
 
 	<OID of toplevel tree>
-	<Conflicted file list>
+	<Conflicted file info>
 	<Informational messages>
 
 These are discussed individually below.
@@ -67,18 +72,23 @@ This is a tree object that represents what would be checked out in the
 working tree at the end of `git merge`.  If there were conflicts, then
 files within this tree may have embedded conflict markers.
 
-Conflicted file list
+Conflicted file info
 ~~~~~~~~~~~~~~~~~~~~
 
-This is a sequence of lines containing a filename on each line, quoted
-as explained for the configuration variable `core.quotePath` (see
-linkgit:git-config[1]).
+This is a sequence of lines with the format
+
+	<mode> <object> <stage> <filename>
+
+The filename will be quoted as explained for the configuration
+variable `core.quotePath` (see linkgit:git-config[1]).  However, if
+the `--exclude-oids-and-modes` option is passed, the mode, object, and
+stage will be omitted.
 
 Informational messages
 ~~~~~~~~~~~~~~~~~~~~~~
 
 This always starts with a blank line to separate it from the previous
-section, and then has free-form messages about the merge, such as:
+sections, and then has free-form messages about the merge, such as:
 
   * "Auto-merging <file>"
   * "CONFLICT (rename/delete): <oldfile> renamed...but deleted in..."
@@ -110,6 +120,14 @@ plumbing commands since the possibility of merge conflicts give it a
 much higher chance of the command not succeeding (and NEWTREE containing
 a bunch of stuff other than just a toplevel tree).
 
+git-merge-tree was written to provide users with the same information
+that they'd have access to if using `git merge`:
+  * what would be written to the working tree (the <OID of toplevel tree>)
+  * the higher order stages that would be written to the index (the
+    <Conflicted file info>)
+  * any messages that would have been printed to stdout (the <Informational
+    messages>)
+
 GIT
 ---
 Part of the linkgit:git[1] suite
builtin/merge-tree.c +10 -1
index 54dae018203..dc52cd02dce 100644
@@ -394,6 +394,7 @@ static int trivial_merge(const char *base,
 struct merge_tree_options {
 	int mode;
 	int show_messages;
+	int exclude_modes_oids_stages;
 };
 
 static int real_merge(struct merge_tree_options *o,
@@ -450,7 +451,11 @@ static int real_merge(struct merge_tree_options *o,
 		merge_get_conflicted_files(&result, &conflicted_files);
 		for (i = 0; i < conflicted_files.nr; i++) {
 			const char *name = conflicted_files.items[i].string;
-			if (last && !strcmp(last, name))
+			struct stage_info *c = conflicted_files.items[i].util;
+			if (!o->exclude_modes_oids_stages)
+				printf("%06o %s %d\t",
+				       c->mode, oid_to_hex(&c->oid), c->stage);
+			else if (last && !strcmp(last, name))
 				continue;
 			write_name_quoted_relative(
 				name, prefix, stdout, line_termination);
@@ -485,6 +490,10 @@ int cmd_merge_tree(int argc, const char **argv, const char *prefix)
 			    N_("do a trivial merge only"), 't'),
 		OPT_BOOL(0, "messages", &o.show_messages,
 			 N_("also show informational/conflict messages")),
+		OPT_BOOL_F('l', "exclude-modes-oids-stages",
+			   &o.exclude_modes_oids_stages,
+			   N_("list conflicted files without modes/oids/stages"),
+			   PARSE_OPT_NONEG),
 		OPT_END()
 	};
 
t/t4301-merge-tree-write-tree.sh +24 -2
index 7113d060bc5..1572f460da0 100755
@@ -47,6 +47,7 @@ test_expect_success 'Content merge and a few conflicts' '
 	expected_tree=$(cat .git/AUTO_MERGE) &&
 
 	# We will redo the merge, while we are still in a conflicted state!
+	git ls-files -u >conflicted-file-info &&
 	test_when_finished "git reset --hard" &&
 
 	test_expect_code 1 git merge-tree --write-tree side1 side2 >RESULT &&
@@ -86,7 +87,7 @@ test_expect_success 'Barf on too many arguments' '
 '
 
 test_expect_success 'test conflict notices and such' '
-	test_expect_code 1 git merge-tree --write-tree side1 side2 >out &&
+	test_expect_code 1 git merge-tree --write-tree --exclude-modes-oids-stages side1 side2 >out &&
 	sed -e "s/[0-9a-f]\{40,\}/HASH/g" out >actual &&
 
 	# Expected results:
@@ -109,7 +110,7 @@ test_expect_success 'test conflict notices and such' '
 '
 
 test_expect_success 'Just the conflicted files without the messages' '
-	test_expect_code 1 git merge-tree --write-tree --no-messages side1 side2 >out &&
+	test_expect_code 1 git merge-tree --write-tree --no-messages --exclude-modes-oids-stages side1 side2 >out &&
 	sed -e "s/[0-9a-f]\{40,\}/HASH/g" out >actual &&
 
 	test_write_lines HASH greeting whatever~side1 >expect &&
@@ -117,4 +118,25 @@ test_expect_success 'Just the conflicted files without the messages' '
 	test_cmp expect actual
 '
 
+test_expect_success 'Check conflicted oids and modes without messages' '
+	test_expect_code 1 git merge-tree --write-tree --no-messages side1 side2 >out &&
+	sed -e "s/[0-9a-f]\{40,\}/HASH/g" out >actual &&
+
+	# Compare the basic output format
+	q_to_tab >expect <<-\EOF &&
+	HASH
+	100644 HASH 1Qgreeting
+	100644 HASH 2Qgreeting
+	100644 HASH 3Qgreeting
+	100644 HASH 1Qwhatever~side1
+	100644 HASH 2Qwhatever~side1
+	EOF
+
+	test_cmp expect actual &&
+
+	# Check the actual hashes against the `ls-files -u` output too
+	tail -n +2 out | sed -e s/side1/HEAD/ >actual &&
+	test_cmp conflicted-file-info actual
+'
+
 test_done
Signature · 1 line
gitgitgadget
#166 lore raw

Re: [PATCH v3 12/15] merge-tree: provide easy access to `ls-files -u` style info

Quoted 6 lines · Elijah Newren via GitGitGadget

From: Elijah Newren <[email protected]>

Much like git merge updates the index with information of the form
(mode, oid, stage, name)
provide this output for conflicted files for merge-tree as well. Provide an --exclude-modes-oids-stages/-l option for users to exclude

This.

Quoted 1 line

+--exclude-oids-and-modes::

No longer matches this.

#167 lore raw

Re: [PATCH v3 12/15] merge-tree: provide easy access to `ls-files -u` style info

Quoted 10 lines · Ævar Arnfjörð Bjarmason
Quoted 6 lines · Elijah Newren via GitGitGadget

From: Elijah Newren <[email protected]>

Much like git merge updates the index with information of the form
(mode, oid, stage, name)
provide this output for conflicted files for merge-tree as well. Provide an --exclude-modes-oids-stages/-l option for users to exclude

Quoted 5 lines

This.

Quoted 1 line

+--exclude-oids-and-modes::

No longer matches this.

Thanks; good catch.

#168 lore raw

PATCH v3 13/15 merge-tree: allow `ls-files -u` style info to be NUL terminated

In reply to Elijah Newren via GitGitGadget

From: Elijah Newren <[email protected]>

Much as git ls-files has a -z option, let's add one to merge-tree so that the conflict-info section can be NUL terminated (and avoid quoting of unusual filenames).

---
 Documentation/git-merge-tree.txt | 21 +++++++++++++----
 builtin/merge-tree.c             |  4 +++-
 t/t4301-merge-tree-write-tree.sh | 40 ++++++++++++++++++++++++++++++++
 3 files changed, 60 insertions(+), 5 deletions(-)
Documentation/git-merge-tree.txt +17 -4
index 55bb7bc61c1..02f766716f9 100644
@@ -38,6 +38,12 @@ See `OUTPUT` below for details.
 OPTIONS
 -------
 
+-z::
+	Do not quote filenames in the <Conflicted file info> section,
+	and end each filename with a NUL character rather than
+	newline.  Also begin the messages section with a NUL character
+	instead of a newline.  See OUTPUT below for more information.
+
 --exclude-oids-and-modes::
 	Instead of writing a list of (mode, oid, stage, path) tuples
 	to output for conflicted files, just provide a list of
@@ -70,7 +76,8 @@ OID of toplevel tree
 
 This is a tree object that represents what would be checked out in the
 working tree at the end of `git merge`.  If there were conflicts, then
-files within this tree may have embedded conflict markers.
+files within this tree may have embedded conflict markers.  This section
+is always followed by a newline.
 
 Conflicted file info
 ~~~~~~~~~~~~~~~~~~~~
@@ -82,19 +89,25 @@ This is a sequence of lines with the format
 The filename will be quoted as explained for the configuration
 variable `core.quotePath` (see linkgit:git-config[1]).  However, if
 the `--exclude-oids-and-modes` option is passed, the mode, object, and
-stage will be omitted.
+stage will be omitted.  If `-z` is passed, the "lines" are terminated
+by a NUL character instead of a newline character.
 
 Informational messages
 ~~~~~~~~~~~~~~~~~~~~~~
 
-This always starts with a blank line to separate it from the previous
-sections, and then has free-form messages about the merge, such as:
+This always starts with a blank line (or NUL if `-z` is passed) to
+separate it from the previous sections, and then has free-form
+messages about the merge, such as:
 
   * "Auto-merging <file>"
   * "CONFLICT (rename/delete): <oldfile> renamed...but deleted in..."
   * "Failed to merge submodule <submodule> (<reason>)"
   * "Warning: cannot merge binary files: <filename>"
 
+Note that these free-form messages will never have a NUL character
+in or between them, even if -z is passed.  It is simply a large block
+of text taking up the remainder of the output.
+
 EXIT STATUS
 -----------
 
builtin/merge-tree.c +3 -1
index dc52cd02dce..7e55f0fa301 100644
@@ -464,7 +464,7 @@ static int real_merge(struct merge_tree_options *o,
 		string_list_clear(&conflicted_files, 1);
 	}
 	if (o->show_messages) {
-		printf("\n");
+		putchar(line_termination);
 		merge_display_update_messages(&opt, &result, stdout);
 	}
 	merge_finalize(&opt, &result);
@@ -490,6 +490,8 @@ int cmd_merge_tree(int argc, const char **argv, const char *prefix)
 			    N_("do a trivial merge only"), 't'),
 		OPT_BOOL(0, "messages", &o.show_messages,
 			 N_("also show informational/conflict messages")),
+		OPT_SET_INT('z', NULL, &line_termination,
+			    N_("separate paths with the NUL character"), '\0'),
 		OPT_BOOL_F('l', "exclude-modes-oids-stages",
 			   &o.exclude_modes_oids_stages,
 			   N_("list conflicted files without modes/oids/stages"),
t/t4301-merge-tree-write-tree.sh +40 -0
index 1572f460da0..f89d87c26b7 100755
@@ -139,4 +139,44 @@ test_expect_success 'Check conflicted oids and modes without messages' '
 	test_cmp conflicted-file-info actual
 '
 
+test_expect_success 'NUL terminated conflicted file "lines"' '
+	git checkout -b tweak1 side1 &&
+	test_write_lines zero 1 2 3 4 5 6 >numbers &&
+	git add numbers &&
+	git mv numbers "Αυτά μου φαίνονται κινέζικα" &&
+	git commit -m "Renamed numbers" &&
+
+	test_expect_code 1 git merge-tree --write-tree -z tweak1 side2 >out &&
+	sed -e "s/[0-9a-f]\{40,\}/HASH/g" out >actual &&
+
+	# Expected results:
+	#   "greeting" should merge with conflicts
+	#   "whatever" has *both* a modify/delete and a file/directory conflict
+	#   "Αυτά μου φαίνονται κινέζικα" should have a conflict
+	echo HASH >expect &&
+
+	q_to_tab <<-EOF | lf_to_nul >>expect &&
+	100644 HASH 1Qgreeting
+	100644 HASH 2Qgreeting
+	100644 HASH 3Qgreeting
+	100644 HASH 1Qwhatever~tweak1
+	100644 HASH 2Qwhatever~tweak1
+	100644 HASH 1QΑυτά μου φαίνονται κινέζικα
+	100644 HASH 2QΑυτά μου φαίνονται κινέζικα
+	100644 HASH 3QΑυτά μου φαίνονται κινέζικα
+
+	EOF
+
+	cat <<-EOF >>expect &&
+	Auto-merging greeting
+	CONFLICT (content): Merge conflict in greeting
+	CONFLICT (file/directory): directory in the way of whatever from tweak1; moving it to whatever~tweak1 instead.
+	CONFLICT (modify/delete): whatever~tweak1 deleted in side2 and modified in tweak1.  Version tweak1 of whatever~tweak1 left in tree.
+	Auto-merging Αυτά μου φαίνονται κινέζικα
+	CONFLICT (content): Merge conflict in Αυτά μου φαίνονται κινέζικα
+	EOF
+
+	test_cmp expect actual
+'
+
 test_done
Signature · 1 line
gitgitgadget
#169 lore raw

PATCH v3 14/15 merge-tree: add a --allow-unrelated-histories flag

In reply to Elijah Newren via GitGitGadget

From: Elijah Newren <[email protected]>

Folks may want to merge histories that have no common ancestry; provide a flag with the same name as used by git merge to allow this.

---
 Documentation/git-merge-tree.txt |  5 +++++
 builtin/merge-tree.c             |  7 ++++++-
 t/t4301-merge-tree-write-tree.sh | 24 +++++++++++++++++++++++-
 3 files changed, 34 insertions(+), 2 deletions(-)
Documentation/git-merge-tree.txt +5 -0
index 02f766716f9..e6a9ff2768b 100644
@@ -55,6 +55,11 @@ OPTIONS
 	default is to include these messages if there are merge
 	conflicts, and to omit them otherwise.
 
+--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
+	check and make the merge proceed anyway.
+
 OUTPUT
 ------
 
builtin/merge-tree.c +6 -1
index 7e55f0fa301..58c0ddc5a32 100644
@@ -393,6 +393,7 @@ static int trivial_merge(const char *base,
 
 struct merge_tree_options {
 	int mode;
+	int allow_unrelated_histories;
 	int show_messages;
 	int exclude_modes_oids_stages;
 };
@@ -430,7 +431,7 @@ static int real_merge(struct merge_tree_options *o,
 	 * merge_incore_recursive in merge-ort.h
 	 */
 	common = get_merge_bases(parent1, parent2);
-	if (!common)
+	if (!common && !o->allow_unrelated_histories)
 		die(_("refusing to merge unrelated histories"));
 	for (j = common; j; j = j->next)
 		commit_list_insert(j->item, &merge_bases);
@@ -496,6 +497,10 @@ int cmd_merge_tree(int argc, const char **argv, const char *prefix)
 			   &o.exclude_modes_oids_stages,
 			   N_("list conflicted files without modes/oids/stages"),
 			   PARSE_OPT_NONEG),
+		OPT_BOOL_F(0, "allow-unrelated-histories",
+			   &o.allow_unrelated_histories,
+			   N_("allow merging unrelated histories"),
+			   PARSE_OPT_NONEG),
 		OPT_END()
 	};
 
t/t4301-merge-tree-write-tree.sh +23 -1
index f89d87c26b7..4de089d976d 100755
@@ -38,7 +38,13 @@ test_expect_success setup '
 	>whatever/empty &&
 	git add numbers greeting whatever/empty &&
 	test_tick &&
-	git commit -m other-modifications
+	git commit -m other-modifications &&
+
+	git switch --orphan unrelated &&
+	>something-else &&
+	git add something-else &&
+	test_tick &&
+	git commit -m first-commit
 '
 
 test_expect_success 'Content merge and a few conflicts' '
@@ -179,4 +185,20 @@ test_expect_success 'NUL terminated conflicted file "lines"' '
 	test_cmp expect actual
 '
 
+test_expect_success 'error out by default for unrelated histories' '
+	test_expect_code 128 git merge-tree --write-tree side1 unrelated 2>error &&
+
+	grep "refusing to merge unrelated histories" error
+'
+
+test_expect_success 'can override merge of unrelated histories' '
+	git merge-tree --write-tree --allow-unrelated-histories side1 unrelated >tree &&
+	TREE=$(cat tree) &&
+
+	git rev-parse side1:numbers side1:greeting side1:whatever unrelated:something-else >expect &&
+	git rev-parse $TREE:numbers $TREE:greeting $TREE:whatever $TREE:something-else >actual &&
+
+	test_cmp expect actual
+'
+
 test_done
Signature · 1 line
gitgitgadget
#170 lore raw

PATCH v3 15/15 git-merge-tree.txt: add a section on potentional usage mistakes

In reply to Elijah Newren via GitGitGadget

From: Elijah Newren <[email protected]>

---
 Documentation/git-merge-tree.txt | 46 ++++++++++++++++++++++++++++++++
 1 file changed, 46 insertions(+)
Documentation/git-merge-tree.txt +46 -0
index e6a9ff2768b..6a2ed475106 100644
@@ -146,6 +146,52 @@ that they'd have access to if using `git merge`:
   * any messages that would have been printed to stdout (the <Informational
     messages>)
 
+MISTAKES TO AVOID
+-----------------
+
+Do NOT look through the resulting toplevel tree to try to find which
+files conflict; parse the <Conflicted file info> section instead.  Not
+only would parsing an entire tree be horrendously slow in large
+repositories, there are numerous types of conflicts not representable by
+conflict markers (modify/delete, mode conflict, binary file changed on
+both sides, file/directory conflicts, various rename conflict
+permutations, etc.)
+
+Do NOT interpret an empty <Conflicted file info> list as a clean merge;
+check the exit status.  A merge can have conflicts without having
+individual files conflict (there are a few types of directory rename
+conflicts that fall into this category, and others might also be added
+in the future).
+
+Do NOT attempt to guess or make the user guess the conflict types from
+the <Conflicted file info> list.  The information there is insufficient
+to do so.  For example: Rename/rename(1to2) conflicts (both sides
+renamed the same file differently) will result in three different file
+having higher order stages (but each only has one higher order stage),
+with no way (short of the <Informational messages> section) to determine
+which three files are related.  File/directory conflicts also result in
+a file with exactly one higher order stage.
+Possibly-involved-in-directory-rename conflicts (when
+"merge.directoryRenames" is unset or set to "conflicts") also result in
+a file with exactly one higher order stage.  In all cases, the
+<Informational messages> section has the necessary info, though it is
+not designed to be machine parseable.
+
+Do NOT assume all filenames listed in the <Informational messages>
+section had conflicts.  Messages can be included for files that have no
+conflicts, such as "Auto-merging <file>".
+
+AVOID taking the OIDS from the <Conflicted file info> and re-merging
+them to present the conflicts to the user.  This will lose information.
+Instead, look up the version of the file found within the <OID of
+toplevel tree> and show that instead.  In particular, the latter will
+have conflict markers annotated with the original branch/commit being
+merged and, if renames were involved, the original filename.  While you
+could include the original branch/commit in the conflict marker
+annotations when re-merging, the original filename is not available from
+the <Conflicted file info> and thus you would be losing information that
+might help the user resolve the conflict.
+
 GIT
 ---
 Part of the linkgit:git[1] suite
Signature · 1 line
gitgitgadget
#171 lore raw

v4 cover In-core git merge-tree ("Server side merges")

In reply to Elijah Newren via GitGitGadget

Note: Depends on en/remerge-diff (to avoid a small textual conflict)

== Basic Summary ==

This series introduces a new mode to git merge-tree allowing it to perform real merges (three-way text content merges, recursive ancestor consolidation, rename detection, proper directory/file conflict handling, etc.) and write the result as a toplevel tree. It doesn't touch the working tree or index, and doesn't create any commits or update any refs. It could be used to do merges when in a bare repository (thus potentially making it of interest to Git hosting sites, i.e. "Server side merges"), or for doing a merge of branches that aren't checked out.

It does not handle similar functionality for cherry-picks, rebases, or reverts; that is also of interest, but is being deferred for a future series.

== Quick Overview ==

* Patches 1-2: preparatory cleanups
* Patches 3-4: implement basic real merges
* Patches 5-6: include informational messages ("CONFLICT" messages and such) in output
* Patches 7-10: add ability to include ls-files -u style of info in the output
* Patch 11: support --allow-unrelated-histories
* Patch 12: augment the manual with potential usage mistakes

== Updates Log ==

Many thanks to the many reviewers who provided good feedback on the most recent round -- Junio, Ævar, Josh, Emily, and perhaps some others I've forgotten from review club.

Updates since v3 (or v5, if you include the rounds at
https://lore.kernel.org/git/[email protected]/):

* Dropped previous patches 5, 6, and 8 of the old series; they weren't being used and opened a can of worms[1]
* [Patch 3] Restructured argument checking, including using an enum
* [Patch 4] Restored the extended paragraph about the deprecated form of git-merge-tree, mentioned write-tree in plumbing commands, and a few other small fixups to the documentation
* [Patch 4] Also provide an example of a clean merge rather than just a conflicted one
* [Patch 6] Fix the incompatible arguments check and add some tests for it
* [Patch 6] Introduce an anonymize_hash() shell function to make tests easier to read (less repeated sed)
* [Patch 9] Rename --exclude-modes-oids-stages to --name-only; no short option for now
* [Patch 10] When -z passed, the tree in the first section should have a trailing NUL rather than trailing newline [1]
https://lore.kernel.org/git/CABPp-BEKuXHELVx4=5JJTj5HVOKZ=Y-4G4BK47BCZYYRSrkFsQ@mail.gmail.com/

Stuff NOT included that reviewers brought up in earlier rounds:

* Very generic (mode, oid, stage, filename) printing formatting[2]
* Always printing 3 stages for each filename with conflicts[3]
* Attempting to group conflict stages by logical conflict rather than by affected target filepath[4]
* Providing similar functionality for doing cherry-picks/rebases/reverts, i.e. a scheme for three-way merges with a specified merge-base[5]. That's being deferred to a future series. [2]
https://lore.kernel.org/git/CABPp-BGnOes7J_piDyBUeuLVm274w4-9G3k0vR-0it3z7TPn_w@mail.gmail.com/
[3]
https://lore.kernel.org/git/CABPp-BG2rMEYBLuBW=0wtpJe4aUFGCFa8D0NTSKz9Sm+CkXPxw@mail.gmail.com/
[4]
https://lore.kernel.org/git/CABPp-BGCL0onSmpgKuO1k2spYCkx=v27ed9TSSxFib=OdDcLbw@mail.gmail.com/
[5]
https://lore.kernel.org/git/CABPp-BEaemkGGm0cSofP0gau7YN-y6HFoi0yJbHA8+iGjxsYSA@mail.gmail.com/

Updates since v2:

* Improved patches from Dscho for the diff_warn_rename_limit() handling
* Add a -z option for NUL-terminated conflict info lines (so that filenames do not have to be quoted)

Updates since v1 (or v3 depending on how you count; thanks to René, Ævar, Christian, Dscho for very helpful feedback):

* New patch from Dscho allowing diff_warn_rename_limit() to print somewhere other than stdout (I hope he's okay with me including his Signed-off-by)
* Now prints filenames relative to prefix, much like ls-files
* Renamed --exclude-oids-and-modes to --exclude-modes-oids-stages and gave it a -l shorthand; I'm wondering if I should just drop this option, though.
* And numerous cleanups, in lots of areas:
* Multiple parse-options cleanups
* Lots of commit message cleanups
* Wording tweaks to the "Description" section of the manual
* Several small code cleanups
* I dropped the RFC label

Updates since original submission v2 (thanks to Christian, Dscho, Ramsay, and René for suggestions and comments):

* Significant changes to output format:
* Flags no longer take a filename for additional output; they write to stdout instead.
* More information included by default when there are conflicts (no need to request it with additional flags, instead flags can be used to suppress it).
* Provide (mode, oid, stage, file) tuples -- i.e. ls-files -u style of information -- when there are conflicts. Add a flag to only list conflicted files if that's preferred.
* Much more thorough manual for git-merge-tree.txt
* Renamed option from --real to --write-tree
* Accept an optional --trivial-merge option to get old style merge-tree behavior
* Allow both --write-tree and --trivial-merge to be omitted since we can deduce which from number of arguments
* Document exit code when the merge cannot be run (so we can distinguish other error cases from conflicts)
* testcase cleanups: test_tick, early skip of test when using recursive backend, variable renames, etc.
* various minor code cleanups
* Add a new --allow-unrelated-histories option (with same meaning as the one used in git merge)
* Rebased on top of en/remerge-diff to avoid a small conflict

Updates since original submission v1 (thanks to Johannes Altmanninger and Fabian for suggestions):

* Fixed a bad patch splitting, and a style issue pointed out by Johannes Altimanninger
* Fixed misleading commit messages in new test cases
* Fixed my comments about how commit-tree could be used to correctly use two -p flags

Elijah Newren (12):
merge-tree: rename merge_trees() to trivial_merge_trees()
merge-tree: move logic for existing merge into new function
merge-tree: add option parsing and initial shell for real merge
function
merge-tree: implement real merges
merge-ort: split out a separate display_update_messages() function
merge-tree: support including merge messages in output
merge-ort: provide a merge_get_conflicted_files() helper function
merge-tree: provide a list of which files have conflicts
merge-tree: provide easy access to ls-files -u style info
merge-tree: allow ls-files -u style info to be NUL terminated
merge-tree: add a --allow-unrelated-histories flag git-merge-tree.txt: add a section on potentional usage mistakes

 Documentation/git-merge-tree.txt | 204 ++++++++++++++++++++++++--
 builtin/merge-tree.c             | 189 ++++++++++++++++++++++--
 git.c                            |   2 +-
 merge-ort.c                      | 109 +++++++++-----
 merge-ort.h                      |  29 ++++
 t/t4301-merge-tree-write-tree.sh | 238 +++++++++++++++++++++++++++++++
 6 files changed, 709 insertions(+), 62 deletions(-)
 create mode 100755 t/t4301-merge-tree-write-tree.sh

base-commit: ea5df61cf358d3c831189e2f04863abc2157e3e1
Published-As: https://github.com/gitgitgadget/git/releases/tag/pr-1122%2Fnewren%2Fin-core-merge-tree-v4
Fetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-1122/newren/in-core-merge-tree-v4
Pull-Request: https://github.com/gitgitgadget/git/pull/1122

Range-diff vs v3:

  1:  4a7cd5542bb =  1:  4a7cd5542bb merge-tree: rename merge_trees() to trivial_merge_trees()
  2:  4780ff6784d =  2:  4780ff6784d merge-tree: move logic for existing merge into new function
  3:  63f42df21ae !  3:  60253745f5c merge-tree: add option parsing and initial shell for real merge function
     @@ builtin/merge-tree.c: static int trivial_merge(int argc, const char **argv)
       	return 0;
       }
       
     ++enum mode {
     ++	MODE_UNKNOWN,
     ++	MODE_TRIVIAL,
     ++	MODE_REAL,
     ++};
     ++
      +struct merge_tree_options {
      +	int mode;
      +};
     @@ builtin/merge-tree.c: static int trivial_merge(int argc, const char **argv)
      +	struct option mt_options[] = {
      +		OPT_CMDMODE(0, "write-tree", &o.mode,
      +			    N_("do a real merge instead of a trivial merge"),
     -+			    'w'),
     ++			    MODE_REAL),
      +		OPT_CMDMODE(0, "trivial-merge", &o.mode,
     -+			    N_("do a trivial merge only"), 't'),
     ++			    N_("do a trivial merge only"), MODE_TRIVIAL),
      +		OPT_END()
      +	};
      +
      +	/* Parse arguments */
      +	argc = parse_options(argc, argv, prefix, mt_options,
      +			     merge_tree_usage, PARSE_OPT_STOP_AT_NON_OPTION);
     -+	if (o.mode) {
     -+		expected_remaining_argc = (o.mode == 'w' ? 2 : 3);
     -+		if (argc != expected_remaining_argc)
     ++	switch (o.mode) {
     ++	default:
     ++		BUG("unexpected command mode %d", o.mode);
     ++	case MODE_UNKNOWN:
     ++		switch (argc) {
     ++		default:
      +			usage_with_options(merge_tree_usage, mt_options);
     -+	} else {
     -+		if (argc < 2 || argc > 3)
     -+			usage_with_options(merge_tree_usage, mt_options);
     -+		o.mode = (argc == 2 ? 'w' : 't');
     ++		case 2:
     ++			o.mode = MODE_REAL;
     ++			break;
     ++		case 3:
     ++			o.mode = MODE_TRIVIAL;
     ++			break;
     ++		}
     ++		expected_remaining_argc = argc;
     ++		break;
     ++	case MODE_REAL:
     ++		expected_remaining_argc = 2;
     ++		break;
     ++	case MODE_TRIVIAL:
     ++		expected_remaining_argc = 3;
     ++		break;
      +	}
      +
     ++	if (argc != expected_remaining_argc)
     ++		usage_with_options(merge_tree_usage, mt_options);
     ++
      +	/* Do the relevant type of merge */
     -+	if (o.mode == 'w')
     ++	if (o.mode == MODE_REAL)
      +		return real_merge(&o, argv[0], argv[1]);
      +	else
      +		return trivial_merge(argv[0], argv[1], argv[2]);

4: 02c29f920d0 ! 4: d7b51da94e6 merge-tree: implement real merges
@@ Commit message
conflict/warning messages normally output during a merge, or have quick access to a list of files with conflicts. That is not available in this preliminary implementation, but subsequent commits will add that
- ability.
+ ability (meaning that NEWTREE would be a lot more than a tree in the
+ case of conflicts).

This also marks the traditional trivial merge of merge-tree as deprecated. The trivial merge not only had limited applicability, the
@@ Documentation/git-merge-tree.txt: git-merge-tree(1)
+Performs a merge, but does not make any new commits and does not read +from or write to either the working tree or index.
+
-+The second form is deprecated and supported only for backward -+compatibility. It will likely be removed in the future, and will not -+be discussed further in this manual.
-+
+The first form will merge the two branches, doing a real merge. A real +merge is distinguished from a trivial merge in that it includes:
+
@@ Documentation/git-merge-tree.txt: git-merge-tree(1)
+ merge base, creating a virtual merge base by merging the merge bases)
+ * etc.
+
-+After the merge completes, it will create a new toplevel tree object. -+See OUTPUT below for details.
++After the merge completes, the first form will create a new toplevel ++tree object. See OUTPUT below for details.
++
++The second form is deprecated; it is kept for backward compatibility ++reasons but may be deleted in the future. Other than the optional ++--trivial-merge, it accepts no options. It can only do a trivial ++merge. It reads three tree-ish, and outputs trivial merge results and ++conflicting stages to the standard output in a semi-diff format. ++Since this was designed for higher level scripts to consume and merge ++the results back into the index, it omits entries that match ++<branch1>. The result of this second form is is similar to what ++three-way 'git read-tree -m' does, but instead of storing the results ++in the index, the command outputs the entries to the standard output. ++This form not only has limited applicability, the output format is ++also difficult to work with, and it will generally be less performant ++than the first form even on successful merges (especially if working ++in large repositories). The remainder of this manual will only ++discuss the first form.
+
+OUTPUT
+------
@@ Documentation/git-merge-tree.txt: git-merge-tree(1)
+For a successful, non-conflicted merge, the exit status is 0. When the +merge has conflicts, the exit status is 1. If the merge is not able to +complete (or start) due to some kind of error, the exit status is
-+something other than 0 or 1.
++something other than 0 or 1 (and the output is unspecified).
+
+USAGE NOTES
+-----------
+
+git-merge-tree was written to be low-level plumbing, similar to
-+hash-object, mktree, commit-tree, update-ref, and mktag. Thus, it could -+be used as a part of a series of steps such as ++hash-object, mktree, commit-tree, write-tree, update-ref, and mktag. ++Thus, it could be used as a part of a series of steps such as
+
+ NEWTREE=$(git merge-tree --write-tree $BRANCH1 $BRANCH2)
+ test $? -eq 0 || die "There were conflicts..."
+ NEWCOMMIT=$(git commit-tree $NEWTREE -p $BRANCH1 -p $BRANCH2)
+ git update-ref $BRANCH1 $NEWCOMMIT
-+
-+However, it does not quite fit into the same category of low-level -+plumbing commands since the possibility of merge conflicts give it a -+much higher chance of the command not succeeding.

GIT
---
@@ t/t4301-merge-tree-write-tree.sh (new)
+
+ git branch side1 &&
+ git branch side2 &&
++ git branch side3 &&
+
+ git checkout side1 &&
+ test_write_lines 1 2 3 4 5 6 >numbers &&
@@ t/t4301-merge-tree-write-tree.sh (new)
+ >whatever/empty &&
+ git add numbers greeting whatever/empty &&
+ test_tick &&
-+ git commit -m other-modifications
++ git commit -m other-modifications &&
++
++ git checkout side3 &&
++ git mv numbers sequence &&
++ test_tick &&
++ git commit -m rename-numbers
++'
++
++test_expect_success 'Clean merge' '
++ git merge-tree --write-tree side1 side3 >RESULT &&
++ q_to_tab <<-EOF >expect &&
++ 100644 blob $(git rev-parse side1:greeting)Qgreeting
++ 100644 blob $(git rev-parse side1:numbers)Qsequence
++ 100644 blob $(git rev-parse side1:whatever)Qwhatever
++ EOF
++
++ git ls-tree $(cat RESULT) >actual &&
++ test_cmp expect actual
+'
+
+test_expect_success 'Content merge and a few conflicts' '
@@ t/t4301-merge-tree-write-tree.sh (new)
+'
+
+test_expect_success 'Barf on too many arguments' '
-+ test_expect_code 129 git merge-tree --write-tree side1 side2 side3 2>expect &&
++ test_expect_code 129 git merge-tree --write-tree side1 side2 invalid 2>expect &&
+
+ grep "^usage: git merge-tree" expect
+'

  5:  290b42846b5 <  -:  ----------- Introduce a variant of the `warning()` function that takes a `FILE *`
  6:  2083fbe9b2e <  -:  ----------- diff: allow diff_warn_rename_limit to write somewhere besides stderr
  7:  1be858e6aa6 !  5:  58a5594aeb6 merge-ort: split out a separate display_update_messages() function
     @@ merge-ort.c: static int record_conflicted_index_entries(struct merge_options *op
      +
      +	/* Also include needed rename limit adjustment now */
      +	diff_warn_rename_limit("merge.renamelimit",
     -+			       opti->renames.needed_limit, 0, stderr);
     ++			       opti->renames.needed_limit, 0);
      +
      +	trace2_region_leave("merge", "display messages", opt->repo);
      +}
     @@ merge-ort.c: static int record_conflicted_index_entries(struct merge_options *op
       			    struct tree *head,
       			    struct merge_result *result,
      @@ merge-ort.c: void merge_switch_to_result(struct merge_options *opt,
     + 		fclose(fp);
       		trace2_region_leave("merge", "write_auto_merge", opt->repo);
       	}
     - 
     +-
      -	if (display_update_msgs) {
      -		struct merge_options_internal *opti = result->priv;
      -		struct hashmap_iter iter;
     @@ merge-ort.c: void merge_switch_to_result(struct merge_options *opt,
      -
      -		/* Also include needed rename limit adjustment now */
      -		diff_warn_rename_limit("merge.renamelimit",
     --				       opti->renames.needed_limit, 0, stderr);
     +-				       opti->renames.needed_limit, 0);
      -
      -		trace2_region_leave("merge", "display messages", opt->repo);
      -	}
  8:  04c3bdc44d2 <  -:  ----------- merge-ort: allow update messages to be written to different file stream
  9:  c8ed002408d !  6:  fa55cb4d644 merge-tree: support including merge messages in output

@@ Documentation/git-merge-tree.txt: git-merge-tree - Perform merge without touchin
'git merge-tree' [--trivial-merge] <base-tree> <branch1> <branch2> (deprecated)

DESCRIPTION
-@@ Documentation/git-merge-tree.txt: merge is distinguished from a trivial merge in that it includes:
- After the merge completes, it will create a new toplevel tree object.
- See OUTPUT below for details.
+@@ Documentation/git-merge-tree.txt: than the first form even on successful merges (especially if working
+ in large repositories). The remainder of this manual will only
+ discuss the first form.

+OPTIONS
+-------
@@ Documentation/git-merge-tree.txt: merge is distinguished from a trivial merge in
+ <Informational messages>
+
+These are discussed individually below.
-+
+
+-The printed tree object corresponds to what would be checked out in +-the working tree at the end of git merge, and thus may have files +-with conflict markers in them.
+OID of toplevel tree
+~~~~~~~~~~~~~~~~~~~~
+
@@ Documentation/git-merge-tree.txt: merge is distinguished from a trivial merge in
+
+This always starts with a blank line to separate it from the previous +section, and then has free-form messages about the merge, such as:
-
--The printed tree object corresponds to what would be checked out in --the working tree at the end of git merge, and thus may have files --with conflict markers in them.
++
+ * "Auto-merging <file>"
+ * "CONFLICT (rename/delete): <oldfile> renamed...but deleted in..."
+ * "Failed to merge submodule <submodule> (<reason>)"
@@ Documentation/git-merge-tree.txt: merge is distinguished from a trivial merge in

EXIT STATUS
-----------
-@@ Documentation/git-merge-tree.txt: be used as a part of a series of steps such as -
- However, it does not quite fit into the same category of low-level
- plumbing commands since the possibility of merge conflicts give it a
--much higher chance of the command not succeeding.
-+much higher chance of the command not succeeding (and NEWTREE containing
-+a bunch of stuff other than just a toplevel tree).
+@@ Documentation/git-merge-tree.txt: Thus, it could be used as a part of a series of steps such as
+ NEWCOMMIT=$(git commit-tree $NEWTREE -p $BRANCH1 -p $BRANCH2)
+ git update-ref $BRANCH1 $NEWCOMMIT

++Note that when the exit status is non-zero, NEWTREE in this sequence ++will contain a lot more output than just a tree.
++
GIT
---
+ Part of the linkgit:git[1] suite

## builtin/merge-tree.c ##
-@@ builtin/merge-tree.c: static int trivial_merge(const char *base, +@@ builtin/merge-tree.c: enum mode {

struct merge_tree_options {
int mode;
@@ builtin/merge-tree.c: static int real_merge(struct merge_tree_options *o,
puts(oid_to_hex(&result.tree->object.oid));
+ if (o->show_messages) {
+ printf("\n");
-+ merge_display_update_messages(&opt, &result, stdout);
++ merge_display_update_messages(&opt, &result);
+ }
merge_finalize(&opt, &result);
return !result.clean; /* result.clean < 0 handled above */
@@ builtin/merge-tree.c: static int real_merge(struct merge_tree_options *o,
NULL
};
@@ builtin/merge-tree.c: int cmd_merge_tree(int argc, const char **argv, const char *prefix)
- 'w'),
+ MODE_REAL),
OPT_CMDMODE(0, "trivial-merge", &o.mode,
- N_("do a trivial merge only"), 't'),
+ N_("do a trivial merge only"), MODE_TRIVIAL),
+ OPT_BOOL(0, "messages", &o.show_messages,
+ N_("also show informational/conflict messages")),
OPT_END()
};

/* Parse arguments */
-+ original_argc = argc;
++ original_argc = argc - 1; /* ignoring argv[0] */
argc = parse_options(argc, argv, prefix, mt_options,
merge_tree_usage, PARSE_OPT_STOP_AT_NON_OPTION);
- if (o.mode) {
+ switch (o.mode) {
@@ builtin/merge-tree.c: int cmd_merge_tree(int argc, const char **argv, const char *prefix)
- usage_with_options(merge_tree_usage, mt_options);
- o.mode = (argc == 2 ? 'w' : 't');
+ break;
+ case MODE_TRIVIAL:
+ expected_remaining_argc = 3;
++ /* Removal of --trivial-merge is expected */
++ original_argc--;
+ break;
}
-+ if (o.mode == 't' && original_argc < argc)
++ if (o.mode == MODE_TRIVIAL && argc < original_argc)
+ die(_("--trivial-merge is incompatible with all other options"));

- /* Do the relevant type of merge */
- if (o.mode == 'w')
+ if (argc != expected_remaining_argc)
+ usage_with_options(merge_tree_usage, mt_options);

## t/t4301-merge-tree-write-tree.sh ##
@@ t/t4301-merge-tree-write-tree.sh: test_expect_success 'Barf on too many arguments' '
grep "^usage: git merge-tree" expect
'

++anonymize_hash() {
++ sed -e "s/[0-9a-f]\{40,\}/HASH/g" "$@"
++}
++
+test_expect_success 'test conflict notices and such' '
+ test_expect_code 1 git merge-tree --write-tree side1 side2 >out &&
-+ sed -e "s/[0-9a-f]\{40,\}/HASH/g" out >actual &&
++ anonymize_hash out >actual &&
+
+ # Expected results:
+ # "greeting" should merge with conflicts
@@ t/t4301-merge-tree-write-tree.sh: test_expect_success 'Barf on too many argument
+
+ test_cmp expect actual
+'
++
++for opt in $(git merge-tree --git-completion-helper-all)
++do
++ if test $opt = "--trivial-merge" || test $opt = "--write-tree"
++ then
++ continue
++ fi
++
++ test_expect_success "usage: --trivial-merge is incompatible with $opt" '
++ test_expect_code 128 git merge-tree --trivial-merge $opt side1 side2 side3
++ '
++done
+
test_done
10: 1c2a3f5ef63 ! 7: f3ad7add515 merge-ort: provide a merge_get_conflicted_files() helper function
@@ merge-ort.h

struct commit;
struct tree;
-@@ merge-ort.h: void merge_display_update_messages(struct merge_options *opt,
- struct merge_result *result,
- FILE *stream);
+@@ merge-ort.h: void merge_switch_to_result(struct merge_options *opt,
+ void merge_display_update_messages(struct merge_options *opt,
+ struct merge_result *result);

+struct stage_info {
+ struct object_id oid;
11: 9c2389eef0e ! 8: 6058190d1b1 merge-tree: provide a list of which files have conflicts
@@ builtin/merge-tree.c: static int real_merge(struct merge_tree_options *o,
+ }
if (o->show_messages) {
printf("\n");
- merge_display_update_messages(&opt, &result, stdout);
+ merge_display_update_messages(&opt, &result);
@@ builtin/merge-tree.c: int cmd_merge_tree(int argc, const char **argv, const char *prefix)

/* Do the relevant type of merge */
- if (o.mode == 'w')
+ if (o.mode == MODE_REAL)
- return real_merge(&o, argv[0], argv[1]);
+ return real_merge(&o, argv[0], argv[1], prefix);
else
@@ t/t4301-merge-tree-write-tree.sh: test_expect_success 'test conflict notices and

Auto-merging greeting
CONFLICT (content): Merge conflict in greeting
-@@ t/t4301-merge-tree-write-tree.sh: test_expect_success 'test conflict notices and such' '
- test_cmp expect actual
- '
+@@ t/t4301-merge-tree-write-tree.sh: do
+ '
+ done

+test_expect_success 'Just the conflicted files without the messages' '
+ test_expect_code 1 git merge-tree --write-tree --no-messages side1 side2 >out &&
-+ sed -e "s/[0-9a-f]\{40,\}/HASH/g" out >actual &&
++ anonymize_hash out >actual &&
+
+ test_write_lines HASH greeting whatever~side1 >expect &&
+
12: 2188a8ca1e7 ! 9: 435f66ea699 merge-tree: provide easy access to ls-files -u style info
@@ Commit message
Much like git merge updates the index with information of the form
(mode, oid, stage, name)
provide this output for conflicted files for merge-tree as well.
- Provide an --exclude-modes-oids-stages/-l option for users to exclude
- the mode, oid, and stage and only get the list of conflicted filenames.
+ Provide a --name-only option for users to exclude the mode, oid, and
+ stage and only get the list of conflicted filenames.

## Documentation/git-merge-tree.txt ##
-@@ Documentation/git-merge-tree.txt: See OUTPUT below for details. +@@ Documentation/git-merge-tree.txt: discuss the first form.
OPTIONS
-------

-+--exclude-oids-and-modes::
-+ Instead of writing a list of (mode, oid, stage, path) tuples
-+ to output for conflicted files, just provide a list of
-+ filenames with conflicts.
++--name-only::
++ In the Conflicted file info section, instead of writing a list
++ of (mode, oid, stage, path) tuples to output for conflicted
++ files, just provide a list of filenames with conflicts (and
++ do not list filenames multiple times if they have multiple
++ conflicting stages).
+
--[no-]messages::
Write any informational messages such as "Auto-merging <path>"
@@ Documentation/git-merge-tree.txt: This is a tree object that represents what wou
+
+The filename will be quoted as explained for the configuration +variable core.quotePath (see linkgit:git-config[1]). However, if
-+the --exclude-oids-and-modes option is passed, the mode, object, and -+stage will be omitted.
++the --name-only option is passed, the mode, object, and stage will ++be omitted.

Informational messages
~~~~~~~~~~~~~~~~~~~~~~
@@ Documentation/git-merge-tree.txt: This is a tree object that represents what wou

* "Auto-merging <file>"
* "CONFLICT (rename/delete): <oldfile> renamed...but deleted in..."
-@@ Documentation/git-merge-tree.txt: plumbing commands since the possibility of merge conflicts give it a
- much higher chance of the command not succeeding (and NEWTREE containing
- a bunch of stuff other than just a toplevel tree).
+@@ Documentation/git-merge-tree.txt: Thus, it could be used as a part of a series of steps such as
+ Note that when the exit status is non-zero, NEWTREE in this sequence
+ will contain a lot more output than just a tree.

+git-merge-tree was written to provide users with the same information +that they'd have access to if using git merge:
@@ Documentation/git-merge-tree.txt: plumbing commands since the possibility of mer
Part of the linkgit:git[1] suite

## builtin/merge-tree.c ##
-@@ builtin/merge-tree.c: static int trivial_merge(const char *base, +@@ builtin/merge-tree.c: enum mode {
struct merge_tree_options {
int mode;
int show_messages;
-+ int exclude_modes_oids_stages;
++ int name_only;
};

static int real_merge(struct merge_tree_options *o,
@@ builtin/merge-tree.c: static int real_merge(struct merge_tree_options *o,
const char *name = conflicted_files.items[i].string;
- if (last && !strcmp(last, name))
+ struct stage_info *c = conflicted_files.items[i].util;
-+ if (!o->exclude_modes_oids_stages)
++ if (!o->name_only)
+ printf("%06o %s %d\t",
+ c->mode, oid_to_hex(&c->oid), c->stage);
+ else if (last && !strcmp(last, name))
@@ builtin/merge-tree.c: static int real_merge(struct merge_tree_options *o,
write_name_quoted_relative(
name, prefix, stdout, line_termination);
@@ builtin/merge-tree.c: int cmd_merge_tree(int argc, const char **argv, const char *prefix)
- N_("do a trivial merge only"), 't'),
+ N_("do a trivial merge only"), MODE_TRIVIAL),
OPT_BOOL(0, "messages", &o.show_messages,
N_("also show informational/conflict messages")),
-+ OPT_BOOL_F('l', "exclude-modes-oids-stages",
-+ &o.exclude_modes_oids_stages,
-+ N_("list conflicted files without modes/oids/stages"),
++ OPT_BOOL_F(0, "name-only",
++ &o.name_only,
++ N_("list filenames without modes/oids/stages"),
+ PARSE_OPT_NONEG),
OPT_END()
};
@@ t/t4301-merge-tree-write-tree.sh: test_expect_success 'Content merge and a few c
test_when_finished "git reset --hard" &&

test_expect_code 1 git merge-tree --write-tree side1 side2 >RESULT &&
-@@ t/t4301-merge-tree-write-tree.sh: test_expect_success 'Barf on too many arguments' '
- '
+@@ t/t4301-merge-tree-write-tree.sh: anonymize_hash() {
+ }

test_expect_success 'test conflict notices and such' '
- test_expect_code 1 git merge-tree --write-tree side1 side2 >out &&
-+ test_expect_code 1 git merge-tree --write-tree --exclude-modes-oids-stages side1 side2 >out &&
- sed -e "s/[0-9a-f]\{40,\}/HASH/g" out >actual &&
++ test_expect_code 1 git merge-tree --write-tree --name-only side1 side2 >out &&
+ anonymize_hash out >actual &&

# Expected results:
-@@ t/t4301-merge-tree-write-tree.sh: test_expect_success 'test conflict notices and such' '
- '
+@@ t/t4301-merge-tree-write-tree.sh: do
+ done

test_expect_success 'Just the conflicted files without the messages' '
- test_expect_code 1 git merge-tree --write-tree --no-messages side1 side2 >out &&
-+ test_expect_code 1 git merge-tree --write-tree --no-messages --exclude-modes-oids-stages side1 side2 >out &&
- sed -e "s/[0-9a-f]\{40,\}/HASH/g" out >actual &&
++ test_expect_code 1 git merge-tree --write-tree --no-messages --name-only side1 side2 >out &&
+ anonymize_hash out >actual &&

test_write_lines HASH greeting whatever~side1 >expect &&
@@ t/t4301-merge-tree-write-tree.sh: test_expect_success 'Just the conflicted files without the messages' '
@@ t/t4301-merge-tree-write-tree.sh: test_expect_success 'Just the conflicted files

+test_expect_success 'Check conflicted oids and modes without messages' '
+ test_expect_code 1 git merge-tree --write-tree --no-messages side1 side2 >out &&
-+ sed -e "s/[0-9a-f]\{40,\}/HASH/g" out >actual &&
++ anonymize_hash out >actual &&
+
+ # Compare the basic output format
+ q_to_tab >expect <<-\EOF &&
13: 52339b396fa ! 10: 5f253e298b3 merge-tree: allow ls-files -u style info to be NUL terminated
@@ Commit message

## Documentation/git-merge-tree.txt ##
-@@ Documentation/git-merge-tree.txt: See OUTPUT below for details. +@@ Documentation/git-merge-tree.txt: discuss the first form.
OPTIONS
-------

@@ Documentation/git-merge-tree.txt: See OUTPUT below for details.
+ newline. Also begin the messages section with a NUL character
+ instead of a newline. See OUTPUT below for more information.
+
- --exclude-oids-and-modes::
- Instead of writing a list of (mode, oid, stage, path) tuples
- to output for conflicted files, just provide a list of
+ --name-only::
+ In the Conflicted file info section, instead of writing a list
+ of (mode, oid, stage, path) tuples to output for conflicted
@@ Documentation/git-merge-tree.txt: OID of toplevel tree

This is a tree object that represents what would be checked out in the working tree at the end of git merge. If there were conflicts, then
-files within this tree may have embedded conflict markers. +files within this tree may have embedded conflict markers. This section
-+is always followed by a newline.
++is always followed by a newline (or NUL if -z is passed).

Conflicted file info
~~~~~~~~~~~~~~~~~~~~
@@ Documentation/git-merge-tree.txt: This is a sequence of lines with the format
The filename will be quoted as explained for the configuration variable core.quotePath (see linkgit:git-config[1]). However, if
- the --exclude-oids-and-modes option is passed, the mode, object, and --stage will be omitted.
-+stage will be omitted. If -z is passed, the "lines" are terminated -+by a NUL character instead of a newline character.
+ the --name-only option is passed, the mode, object, and stage will +-be omitted.
++be omitted. If -z is passed, the "lines" are terminated by a NUL ++character instead of a newline character.

Informational messages
~~~~~~~~~~~~~~~~~~~~~~
@@ Documentation/git-merge-tree.txt: This is a sequence of lines with the format

## builtin/merge-tree.c ##
+@@ builtin/merge-tree.c: static int real_merge(struct merge_tree_options *o,
+ if (o->show_messages == -1)
+ o->show_messages = !result.clean;
+
+- puts(oid_to_hex(&result.tree->object.oid));
++ printf("%s%c", oid_to_hex(&result.tree->object.oid), line_termination);
+ if (!result.clean) {
+ struct string_list conflicted_files = STRING_LIST_INIT_NODUP;
+ const char *last = NULL;
@@ builtin/merge-tree.c: static int real_merge(struct merge_tree_options *o,
string_list_clear(&conflicted_files, 1);
}
if (o->show_messages) {
- printf("\n");
+ putchar(line_termination);
- merge_display_update_messages(&opt, &result, stdout);
+ merge_display_update_messages(&opt, &result);
}
merge_finalize(&opt, &result);
@@ builtin/merge-tree.c: int cmd_merge_tree(int argc, const char **argv, const char *prefix)
- N_("do a trivial merge only"), 't'),
+ N_("do a trivial merge only"), MODE_TRIVIAL),
OPT_BOOL(0, "messages", &o.show_messages,
N_("also show informational/conflict messages")),
+ OPT_SET_INT('z', NULL, &line_termination,
+ N_("separate paths with the NUL character"), '\0'),
- OPT_BOOL_F('l', "exclude-modes-oids-stages",
- &o.exclude_modes_oids_stages,
- N_("list conflicted files without modes/oids/stages"),
+ OPT_BOOL_F(0, "name-only",
+ &o.name_only,
+ N_("list filenames without modes/oids/stages"),

## t/t4301-merge-tree-write-tree.sh ##
@@ t/t4301-merge-tree-write-tree.sh: test_expect_success 'Check conflicted oids and modes without messages' '
@@ t/t4301-merge-tree-write-tree.sh: test_expect_success 'Check conflicted oids and
+ git commit -m "Renamed numbers" &&
+
+ test_expect_code 1 git merge-tree --write-tree -z tweak1 side2 >out &&
-+ sed -e "s/[0-9a-f]\{40,\}/HASH/g" out >actual &&
++ anonymize_hash out >actual &&
+
+ # Expected results:
+ # "greeting" should merge with conflicts
+ # "whatever" has *both* a modify/delete and a file/directory conflict
+ # "Αυτά μου φαίνονται κινέζικα" should have a conflict
-+ echo HASH >expect &&
++ echo HASH | lf_to_nul >expect &&
+
+ q_to_tab <<-EOF | lf_to_nul >>expect &&
+ 100644 HASH 1Qgreeting
14: c854ecb5f4a ! 11: e706cf31c6e merge-tree: add a --allow-unrelated-histories flag
@@ Documentation/git-merge-tree.txt: OPTIONS

## builtin/merge-tree.c ##
-@@ builtin/merge-tree.c: static int trivial_merge(const char *base, +@@ builtin/merge-tree.c: enum mode {

struct merge_tree_options {
int mode;
+ int allow_unrelated_histories;
int show_messages;
- int exclude_modes_oids_stages;
+ int name_only;
};
@@ builtin/merge-tree.c: static int real_merge(struct merge_tree_options *o,
* merge_incore_recursive in merge-ort.h
@@ builtin/merge-tree.c: static int real_merge(struct merge_tree_options *o,
for (j = common; j; j = j->next)
commit_list_insert(j->item, &merge_bases);
@@ builtin/merge-tree.c: int cmd_merge_tree(int argc, const char **argv, const char *prefix)
- &o.exclude_modes_oids_stages,
- N_("list conflicted files without modes/oids/stages"),
+ &o.name_only,
+ N_("list filenames without modes/oids/stages"),
PARSE_OPT_NONEG),
+ OPT_BOOL_F(0, "allow-unrelated-histories",
+ &o.allow_unrelated_histories,
@@ builtin/merge-tree.c: int cmd_merge_tree(int argc, const char **argv, const char

## t/t4301-merge-tree-write-tree.sh ##
@@ t/t4301-merge-tree-write-tree.sh: test_expect_success setup '
- >whatever/empty &&
- git add numbers greeting whatever/empty &&
+ git checkout side3 &&
+ git mv numbers sequence &&
test_tick &&
-- git commit -m other-modifications
-+ git commit -m other-modifications &&
+- git commit -m rename-numbers
++ git commit -m rename-numbers &&
+
+ git switch --orphan unrelated &&
+ >something-else &&
@@ t/t4301-merge-tree-write-tree.sh: test_expect_success setup '
+ git commit -m first-commit
'

- test_expect_success 'Content merge and a few conflicts' '
+ test_expect_success 'Clean merge' '
@@ t/t4301-merge-tree-write-tree.sh: test_expect_success 'NUL terminated conflicted file "lines"' '
test_cmp expect actual
'
15: bc8591bbb63 = 12: c279236ab65 git-merge-tree.txt: add a section on potentional usage mistakes

Signature · 1 line
gitgitgadget
#172 lore raw

PATCH v4 1/12 merge-tree: rename merge_trees() to trivial_merge_trees()

From: Elijah Newren <[email protected]>

merge-recursive.h defined its own merge_trees() function, different than the one found in builtin/merge-tree.c. That was okay in the past, but we want merge-tree to be able to use the merge-ort functions, which will end up including merge-recursive.h. Rename the function found in builtin/merge-tree.c to avoid the conflict.

---
 builtin/merge-tree.c | 8 ++++----
 1 file changed, 4 insertions(+), 4 deletions(-)
builtin/merge-tree.c +4 -4
index 5dc94d6f880..06f9eee9f78 100644
@@ -28,7 +28,7 @@ static void add_merge_entry(struct merge_list *entry)
 	merge_result_end = &entry->next;
 }
 
-static void merge_trees(struct tree_desc t[3], const char *base);
+static void trivial_merge_trees(struct tree_desc t[3], const char *base);
 
 static const char *explanation(struct merge_list *entry)
 {
@@ -225,7 +225,7 @@ static void unresolved_directory(const struct traverse_info *info,
 	buf2 = fill_tree_descriptor(r, t + 2, ENTRY_OID(n + 2));
 #undef ENTRY_OID
 
-	merge_trees(t, newbase);
+	trivial_merge_trees(t, newbase);
 
 	free(buf0);
 	free(buf1);
@@ -342,7 +342,7 @@ static int threeway_callback(int n, unsigned long mask, unsigned long dirmask, s
 	return mask;
 }
 
-static void merge_trees(struct tree_desc t[3], const char *base)
+static void trivial_merge_trees(struct tree_desc t[3], const char *base)
 {
 	struct traverse_info info;
 
@@ -378,7 +378,7 @@ int cmd_merge_tree(int argc, const char **argv, const char *prefix)
 	buf1 = get_tree_descriptor(r, t+0, argv[1]);
 	buf2 = get_tree_descriptor(r, t+1, argv[2]);
 	buf3 = get_tree_descriptor(r, t+2, argv[3]);
-	merge_trees(t, "");
+	trivial_merge_trees(t, "");
 	free(buf1);
 	free(buf2);
 	free(buf3);
Signature · 1 line
gitgitgadget
#173 lore raw

PATCH v4 2/12 merge-tree: move logic for existing merge into new function

In reply to Elijah Newren via GitGitGadget

From: Elijah Newren <[email protected]>

In preparation for adding a non-trivial merge capability to merge-tree, move the existing merge logic for trivial merges into a new function.

---
 builtin/merge-tree.c | 12 ++++++++----
 1 file changed, 8 insertions(+), 4 deletions(-)
builtin/merge-tree.c +8 -4
index 06f9eee9f78..914ec960b7e 100644
@@ -366,15 +366,12 @@ static void *get_tree_descriptor(struct repository *r,
 	return buf;
 }
 
-int cmd_merge_tree(int argc, const char **argv, const char *prefix)
+static int trivial_merge(int argc, const char **argv)
 {
 	struct repository *r = the_repository;
 	struct tree_desc t[3];
 	void *buf1, *buf2, *buf3;
 
-	if (argc != 4)
-		usage(merge_tree_usage);
-
 	buf1 = get_tree_descriptor(r, t+0, argv[1]);
 	buf2 = get_tree_descriptor(r, t+1, argv[2]);
 	buf3 = get_tree_descriptor(r, t+2, argv[3]);
@@ -386,3 +383,10 @@ int cmd_merge_tree(int argc, const char **argv, const char *prefix)
 	show_result();
 	return 0;
 }
+
+int cmd_merge_tree(int argc, const char **argv, const char *prefix)
+{
+	if (argc != 4)
+		usage(merge_tree_usage);
+	return trivial_merge(argc, argv);
+}
Signature · 1 line
gitgitgadget
#174 lore raw

PATCH v4 3/12 merge-tree: add option parsing and initial shell for real merge function

In reply to Elijah Newren via GitGitGadget

From: Elijah Newren <[email protected]>

Let merge-tree accept a --write-tree parameter for choosing real merges instead of trivial merges, and accept an optional --trivial-merge option to get the traditional behavior. Note that these accept different numbers of arguments, though, so these names need not actually be used.

Note that real merges differ from trivial merges in that they handle:
- three way content merges
- recursive ancestor consolidation
- renames
- proper directory/file conflict handling
- etc.
Basically all the stuff you'd expect from git merge, just without updating the index and working tree. The initial shell added here does nothing more than die with "real merges are not yet implemented", but that will be fixed in subsequent commits.

---
 builtin/merge-tree.c | 84 +++++++++++++++++++++++++++++++++++++++-----
 git.c                |  2 +-
 2 files changed, 76 insertions(+), 10 deletions(-)
builtin/merge-tree.c +75 -9
index 914ec960b7e..0f9d928e862 100644
@@ -3,13 +3,12 @@
 #include "tree-walk.h"
 #include "xdiff-interface.h"
 #include "object-store.h"
+#include "parse-options.h"
 #include "repository.h"
 #include "blob.h"
 #include "exec-cmd.h"
 #include "merge-blobs.h"
 
-static const char merge_tree_usage[] = "git merge-tree <base-tree> <branch1> <branch2>";
-
 struct merge_list {
 	struct merge_list *next;
 	struct merge_list *link;	/* other stages for this object */
@@ -366,15 +365,17 @@ static void *get_tree_descriptor(struct repository *r,
 	return buf;
 }
 
-static int trivial_merge(int argc, const char **argv)
+static int trivial_merge(const char *base,
+			 const char *branch1,
+			 const char *branch2)
 {
 	struct repository *r = the_repository;
 	struct tree_desc t[3];
 	void *buf1, *buf2, *buf3;
 
-	buf1 = get_tree_descriptor(r, t+0, argv[1]);
-	buf2 = get_tree_descriptor(r, t+1, argv[2]);
-	buf3 = get_tree_descriptor(r, t+2, argv[3]);
+	buf1 = get_tree_descriptor(r, t+0, base);
+	buf2 = get_tree_descriptor(r, t+1, branch1);
+	buf3 = get_tree_descriptor(r, t+2, branch2);
 	trivial_merge_trees(t, "");
 	free(buf1);
 	free(buf2);
@@ -384,9 +385,74 @@ static int trivial_merge(int argc, const char **argv)
 	return 0;
 }
 
+enum mode {
+	MODE_UNKNOWN,
+	MODE_TRIVIAL,
+	MODE_REAL,
+};
+
+struct merge_tree_options {
+	int mode;
+};
+
+static int real_merge(struct merge_tree_options *o,
+		      const char *branch1, const char *branch2)
+{
+	die(_("real merges are not yet implemented"));
+}
+
 int cmd_merge_tree(int argc, const char **argv, const char *prefix)
 {
-	if (argc != 4)
-		usage(merge_tree_usage);
-	return trivial_merge(argc, argv);
+	struct merge_tree_options o = { 0 };
+	int expected_remaining_argc;
+
+	const char * const merge_tree_usage[] = {
+		N_("git merge-tree [--write-tree] <branch1> <branch2>"),
+		N_("git merge-tree [--trivial-merge] <base-tree> <branch1> <branch2>"),
+		NULL
+	};
+	struct option mt_options[] = {
+		OPT_CMDMODE(0, "write-tree", &o.mode,
+			    N_("do a real merge instead of a trivial merge"),
+			    MODE_REAL),
+		OPT_CMDMODE(0, "trivial-merge", &o.mode,
+			    N_("do a trivial merge only"), MODE_TRIVIAL),
+		OPT_END()
+	};
+
+	/* Parse arguments */
+	argc = parse_options(argc, argv, prefix, mt_options,
+			     merge_tree_usage, PARSE_OPT_STOP_AT_NON_OPTION);
+	switch (o.mode) {
+	default:
+		BUG("unexpected command mode %d", o.mode);
+	case MODE_UNKNOWN:
+		switch (argc) {
+		default:
+			usage_with_options(merge_tree_usage, mt_options);
+		case 2:
+			o.mode = MODE_REAL;
+			break;
+		case 3:
+			o.mode = MODE_TRIVIAL;
+			break;
+		}
+		expected_remaining_argc = argc;
+		break;
+	case MODE_REAL:
+		expected_remaining_argc = 2;
+		break;
+	case MODE_TRIVIAL:
+		expected_remaining_argc = 3;
+		break;
+	}
+
+	if (argc != expected_remaining_argc)
+		usage_with_options(merge_tree_usage, mt_options);
+
+	/* Do the relevant type of merge */
+	if (o.mode == MODE_REAL)
+		return real_merge(&o, argv[0], argv[1]);
+	else
+		return trivial_merge(argv[0], argv[1], argv[2]);
 }
git.c +1 -1
index 5ff21be21f3..6090a1289db 100644
@@ -558,7 +558,7 @@ static struct cmd_struct commands[] = {
 	{ "merge-recursive-ours", cmd_merge_recursive, RUN_SETUP | NEED_WORK_TREE | NO_PARSEOPT },
 	{ "merge-recursive-theirs", cmd_merge_recursive, RUN_SETUP | NEED_WORK_TREE | NO_PARSEOPT },
 	{ "merge-subtree", cmd_merge_recursive, RUN_SETUP | NEED_WORK_TREE | NO_PARSEOPT },
-	{ "merge-tree", cmd_merge_tree, RUN_SETUP | NO_PARSEOPT },
+	{ "merge-tree", cmd_merge_tree, RUN_SETUP },
 	{ "mktag", cmd_mktag, RUN_SETUP | NO_PARSEOPT },
 	{ "mktree", cmd_mktree, RUN_SETUP },
 	{ "multi-pack-index", cmd_multi_pack_index, RUN_SETUP },
Signature · 1 line
gitgitgadget
#175 lore raw

PATCH v4 4/12 merge-tree: implement real merges

In reply to Elijah Newren via GitGitGadget

From: Elijah Newren <[email protected]>

This adds the ability to perform real merges rather than just trivial merges (meaning handling three way content merges, recursive ancestor consolidation, renames, proper directory/file conflict handling, and so forth). However, unlike git merge, the working tree and index are left alone and no branch is updated.

The only output is:
- the toplevel resulting tree printed on stdout
- exit status of 0 (clean), 1 (conflicts present), anything else (merge could not be performed; unknown if clean or conflicted)

This output is meant to be used by some higher level script, perhaps in a sequence of steps like this:

   NEWTREE=$(git merge-tree --write-tree $BRANCH1 $BRANCH2)
   test $? -eq 0 || die "There were conflicts..."
   NEWCOMMIT=$(git commit-tree $NEWTREE -p $BRANCH1 -p $BRANCH2)
   git update-ref $BRANCH1 $NEWCOMMIT

Note that higher level scripts may also want to access the conflict/warning messages normally output during a merge, or have quick access to a list of files with conflicts. That is not available in this preliminary implementation, but subsequent commits will add that ability (meaning that NEWTREE would be a lot more than a tree in the case of conflicts).

This also marks the traditional trivial merge of merge-tree as deprecated. The trivial merge not only had limited applicability, the output format was also difficult to work with (and its format undocumented), and will generally be less performant than real merges.

---
 Documentation/git-merge-tree.txt |  79 +++++++++++++++++++----
 builtin/merge-tree.c             |  44 ++++++++++++-
 t/t4301-merge-tree-write-tree.sh | 106 +++++++++++++++++++++++++++++++
 3 files changed, 216 insertions(+), 13 deletions(-)
 create mode 100755 t/t4301-merge-tree-write-tree.sh
Documentation/git-merge-tree.txt +67 -12
index 58731c19422..586733ea564 100644
@@ -3,26 +3,81 @@ git-merge-tree(1)
 
 NAME
 ----
-git-merge-tree - Show three-way merge without touching index
+git-merge-tree - Perform merge without touching index or working tree
 
 
 SYNOPSIS
 --------
 [verse]
-'git merge-tree' <base-tree> <branch1> <branch2>
+'git merge-tree' [--write-tree] <branch1> <branch2>
+'git merge-tree' [--trivial-merge] <base-tree> <branch1> <branch2> (deprecated)
 
 DESCRIPTION
 -----------
-Reads three tree-ish, and output trivial merge results and
-conflicting stages to the standard output.  This is similar to
-what three-way 'git read-tree -m' does, but instead of storing the
-results in the index, the command outputs the entries to the
-standard output.
-
-This is meant to be used by higher level scripts to compute
-merge results outside of the index, and stuff the results back into the
-index.  For this reason, the output from the command omits
-entries that match the <branch1> tree.
+
+Performs a merge, but does not make any new commits and does not read
+from or write to either the working tree or index.
+
+The first form will merge the two branches, doing a real merge.  A real
+merge is distinguished from a trivial merge in that it includes:
+
+  * three way content merges of individual files
+  * rename detection
+  * proper directory/file conflict handling
+  * recursive ancestor consolidation (i.e. when there is more than one
+    merge base, creating a virtual merge base by merging the merge bases)
+  * etc.
+
+After the merge completes, the first form will create a new toplevel
+tree object.  See `OUTPUT` below for details.
+
+The second form is deprecated; it is kept for backward compatibility
+reasons but may be deleted in the future.  Other than the optional
+`--trivial-merge`, it accepts no options.  It can only do a trivial
+merge.  It reads three tree-ish, and outputs trivial merge results and
+conflicting stages to the standard output in a semi-diff format.
+Since this was designed for higher level scripts to consume and merge
+the results back into the index, it omits entries that match
+<branch1>.  The result of this second form is is similar to what
+three-way 'git read-tree -m' does, but instead of storing the results
+in the index, the command outputs the entries to the standard output.
+This form not only has limited applicability, the output format is
+also difficult to work with, and it will generally be less performant
+than the first form even on successful merges (especially if working
+in large repositories).  The remainder of this manual will only
+discuss the first form.
+
+OUTPUT
+------
+
+For either a successful or conflicted merge, the output from
+git-merge-tree is simply one line:
+
+	<OID of toplevel tree>
+
+The printed tree object corresponds to what would be checked out in
+the working tree at the end of `git merge`, and thus may have files
+with conflict markers in them.
+
+EXIT STATUS
+-----------
+
+For a successful, non-conflicted merge, the exit status is 0.  When the
+merge has conflicts, the exit status is 1.  If the merge is not able to
+complete (or start) due to some kind of error, the exit status is
+something other than 0 or 1 (and the output is unspecified).
+
+USAGE NOTES
+-----------
+
+git-merge-tree was written to be low-level plumbing, similar to
+hash-object, mktree, commit-tree, write-tree, update-ref, and mktag.
+Thus, it could be used as a part of a series of steps such as
+
+       NEWTREE=$(git merge-tree --write-tree $BRANCH1 $BRANCH2)
+       test $? -eq 0 || die "There were conflicts..."
+       NEWCOMMIT=$(git commit-tree $NEWTREE -p $BRANCH1 -p $BRANCH2)
+       git update-ref $BRANCH1 $NEWCOMMIT
 
 GIT
 ---
builtin/merge-tree.c +43 -1

56 lines not on this page · raw message

t/t4301-merge-tree-write-tree.sh new file +106 -0

106 lines not on this page · raw message

Signature · 1 line
gitgitgadget
#176 lore raw

Re: [PATCH v4 04/12] merge-tree: implement real merges

Quoted 1 line · Elijah Newren via GitGitGadget

+<branch1>. The result of this second form is is similar to what

I haven't read the rest of the patches, but this like somehow stood out in the interdiff.

#177 lore raw

Re: [PATCH v4 04/12] merge-tree: implement real merges

Quoted 7 lines · Junio C Hamano
Quoted 1 line · Elijah Newren via GitGitGadget

+<branch1>. The result of this second form is is similar to what

I haven't read the rest of the patches, but this like somehow stood out in the interdiff.

Are you highlighting the double "is" that I need to correct here? Or was there something else that stood out to you?

#178 lore raw

Re: [PATCH v4 04/12] merge-tree: implement real merges

In reply to Elijah Newren via GitGitGadget

t/t4301-merge-tree-write-tree.sh line 8

Quoted 2 lines · Elijah Newren via GitGitGadget
t/t4301-merge-tree-write-tree.sh new file +2 -0
+# This test is ort-specific
+if test "${GIT_TEST_MERGE_ALGORITHM}" != "ort"

Nit: Needless braces, left over from an earlier version where you used ${VAR:+...} ?

t/t4301-merge-tree-write-tree.sh line 58

Quoted 9 lines
t/t4301-merge-tree-write-tree.sh new file +9 -0
+test_expect_success 'Clean merge' '
+	git merge-tree --write-tree side1 side3 >RESULT &&
+	q_to_tab <<-EOF >expect &&
+	100644 blob $(git rev-parse side1:greeting)Qgreeting
+	100644 blob $(git rev-parse side1:numbers)Qsequence
+	100644 blob $(git rev-parse side1:whatever)Qwhatever
+	EOF
+
+	git ls-tree $(cat RESULT) >actual &&

Nit: to avoid the "cat":

    oid=$(git merge-tree ...) &&
    [...]
    git ls-tree $oid [...]

t/t4301-merge-tree-write-tree.sh line 65

Quoted 7 lines
t/t4301-merge-tree-write-tree.sh new file +7 -0
+	test_cmp expect actual
+'
+
+test_expect_success 'Content merge and a few conflicts' '
+	git checkout side1^0 &&
+	test_must_fail git merge side2 &&
+	expected_tree=$(cat .git/AUTO_MERGE) &&

Let's do "git rev-parse AUTO_MERGE", to avoid needing REFFILES here.

t/t4301-merge-tree-write-tree.sh line 88

Quoted 4 lines

[...]

t/t4301-merge-tree-write-tree.sh new file +3 -0
+	# greeting should have a merge conflict
+	git show ${expected_tree}:greeting >tmp &&
+	cat tmp | sed -e s/HEAD/side1/ >expect &&

Nit: More needless "cat", can just be: "sed ... <tmp >expect".

#179 lore raw

PATCH v4 5/12 merge-ort: split out a separate display_update_messages() function

In reply to Elijah Newren via GitGitGadget

From: Elijah Newren <[email protected]>

This patch includes no new code; it simply moves a bunch of lines into a new function. As such, there are no functional changes. This is just a preparatory step to allow the printed messages to be handled differently by other callers, such as in git merge-tree --write-tree.

(Patch best viewed with

     --color-moved --color-moved-ws=allow-indentation-change

to see that it is a simple code movement.)

---
 merge-ort.c | 78 ++++++++++++++++++++++++++++-------------------------
 merge-ort.h |  8 ++++++
 2 files changed, 49 insertions(+), 37 deletions(-)
merge-ort.c +41 -37
index 9bf15a01db8..ebaed98d53a 100644
@@ -4235,6 +4235,45 @@ static int record_conflicted_index_entries(struct merge_options *opt)
 	return errs;
 }
 
+void merge_display_update_messages(struct merge_options *opt,
+				   struct merge_result *result)
+{
+	struct merge_options_internal *opti = result->priv;
+	struct hashmap_iter iter;
+	struct strmap_entry *e;
+	struct string_list olist = STRING_LIST_INIT_NODUP;
+	int i;
+
+	if (opt->record_conflict_msgs_as_headers)
+		BUG("Either display conflict messages or record them as headers, not both");
+
+	trace2_region_enter("merge", "display messages", opt->repo);
+
+	/* Hack to pre-allocate olist to the desired size */
+	ALLOC_GROW(olist.items, strmap_get_size(&opti->output),
+		   olist.alloc);
+
+	/* Put every entry from output into olist, then sort */
+	strmap_for_each_entry(&opti->output, &iter, e) {
+		string_list_append(&olist, e->key)->util = e->value;
+	}
+	string_list_sort(&olist);
+
+	/* Iterate over the items, printing them */
+	for (i = 0; i < olist.nr; ++i) {
+		struct strbuf *sb = olist.items[i].util;
+
+		printf("%s", sb->buf);
+	}
+	string_list_clear(&olist, 0);
+
+	/* Also include needed rename limit adjustment now */
+	diff_warn_rename_limit("merge.renamelimit",
+			       opti->renames.needed_limit, 0);
+
+	trace2_region_leave("merge", "display messages", opt->repo);
+}
+
 void merge_switch_to_result(struct merge_options *opt,
 			    struct tree *head,
 			    struct merge_result *result,
@@ -4272,43 +4311,8 @@ void merge_switch_to_result(struct merge_options *opt,
 		fclose(fp);
 		trace2_region_leave("merge", "write_auto_merge", opt->repo);
 	}
-
-	if (display_update_msgs) {
-		struct merge_options_internal *opti = result->priv;
-		struct hashmap_iter iter;
-		struct strmap_entry *e;
-		struct string_list olist = STRING_LIST_INIT_NODUP;
-		int i;
-
-		if (opt->record_conflict_msgs_as_headers)
-			BUG("Either display conflict messages or record them as headers, not both");
-
-		trace2_region_enter("merge", "display messages", opt->repo);
-
-		/* Hack to pre-allocate olist to the desired size */
-		ALLOC_GROW(olist.items, strmap_get_size(&opti->output),
-			   olist.alloc);
-
-		/* Put every entry from output into olist, then sort */
-		strmap_for_each_entry(&opti->output, &iter, e) {
-			string_list_append(&olist, e->key)->util = e->value;
-		}
-		string_list_sort(&olist);
-
-		/* Iterate over the items, printing them */
-		for (i = 0; i < olist.nr; ++i) {
-			struct strbuf *sb = olist.items[i].util;
-
-			printf("%s", sb->buf);
-		}
-		string_list_clear(&olist, 0);
-
-		/* Also include needed rename limit adjustment now */
-		diff_warn_rename_limit("merge.renamelimit",
-				       opti->renames.needed_limit, 0);
-
-		trace2_region_leave("merge", "display messages", opt->repo);
-	}
+	if (display_update_msgs)
+		merge_display_update_messages(opt, result);
 
 	merge_finalize(opt, result);
 }
merge-ort.h +8 -0
index fe599b87868..e5aec45b18f 100644
@@ -80,6 +80,14 @@ void merge_switch_to_result(struct merge_options *opt,
 			    int update_worktree_and_index,
 			    int display_update_msgs);
 
+/*
+ * Display messages about conflicts and which files were 3-way merged.
+ * Automatically called by merge_switch_to_result() with stream == stdout,
+ * so only call this when bypassing merge_switch_to_result().
+ */
+void merge_display_update_messages(struct merge_options *opt,
+				   struct merge_result *result);
+
 /* Do needed cleanup when not calling merge_switch_to_result() */
 void merge_finalize(struct merge_options *opt,
 		    struct merge_result *result);
Signature · 1 line
gitgitgadget
#180 lore raw

PATCH v4 6/12 merge-tree: support including merge messages in output

In reply to Elijah Newren via GitGitGadget

From: Elijah Newren <[email protected]>

When running git merge-tree --write-tree, we previously would only return an exit status reflecting the cleanness of a merge, and print out the toplevel tree of the resulting merge. Merges also have informational messages, such as:
* "Auto-merging <PATH>"
* "CONFLICT (content): ..."
* "CONFLICT (file/directory)"
* etc.
In fact, when non-content conflicts occur (such as file/directory, modify/delete, add/add with differing modes, rename/rename (1to2), etc.), these informational messages may be the only notification the user gets since these conflicts are not representable in the contents of the file.

Add a --[no-]messages option so that callers can request these messages be included at the end of the output. Include such messages by default when there are conflicts, and omit them by default when the merge is clean.

---
 Documentation/git-merge-tree.txt | 45 +++++++++++++++++++++++++++-----
 builtin/merge-tree.c             | 21 +++++++++++++--
 t/t4301-merge-tree-write-tree.sh | 37 ++++++++++++++++++++++++++
 3 files changed, 95 insertions(+), 8 deletions(-)
Documentation/git-merge-tree.txt +39 -6
index 586733ea564..869c38f8223 100644
@@ -9,7 +9,7 @@ git-merge-tree - Perform merge without touching index or working tree
 SYNOPSIS
 --------
 [verse]
-'git merge-tree' [--write-tree] <branch1> <branch2>
+'git merge-tree' [--write-tree] [<options>] <branch1> <branch2>
 'git merge-tree' [--trivial-merge] <base-tree> <branch1> <branch2> (deprecated)
 
 DESCRIPTION
@@ -47,17 +47,47 @@ than the first form even on successful merges (especially if working
 in large repositories).  The remainder of this manual will only
 discuss the first form.
 
+OPTIONS
+-------
+
+--[no-]messages::
+	Write any informational messages such as "Auto-merging <path>"
+	or CONFLICT notices to the end of stdout.  If unspecified, the
+	default is to include these messages if there are merge
+	conflicts, and to omit them otherwise.
+
 OUTPUT
 ------
 
-For either a successful or conflicted merge, the output from
-git-merge-tree is simply one line:
+By default, for a successful merge, the output from git-merge-tree is
+simply one line:
+
+	<OID of toplevel tree>
+
+Whereas for a conflicted merge, the output is by default of the form:
 
 	<OID of toplevel tree>
+	<Informational messages>
+
+These are discussed individually below.
 
-The printed tree object corresponds to what would be checked out in
-the working tree at the end of `git merge`, and thus may have files
-with conflict markers in them.
+OID of toplevel tree
+~~~~~~~~~~~~~~~~~~~~
+
+This is a tree object that represents what would be checked out in the
+working tree at the end of `git merge`.  If there were conflicts, then
+files within this tree may have embedded conflict markers.
+
+Informational messages
+~~~~~~~~~~~~~~~~~~~~~~
+
+This always starts with a blank line to separate it from the previous
+section, and then has free-form messages about the merge, such as:
+
+  * "Auto-merging <file>"
+  * "CONFLICT (rename/delete): <oldfile> renamed...but deleted in..."
+  * "Failed to merge submodule <submodule> (<reason>)"
+  * "Warning: cannot merge binary files: <filename>"
 
 EXIT STATUS
 -----------
@@ -79,6 +109,9 @@ Thus, it could be used as a part of a series of steps such as
        NEWCOMMIT=$(git commit-tree $NEWTREE -p $BRANCH1 -p $BRANCH2)
        git update-ref $BRANCH1 $NEWCOMMIT
 
+Note that when the exit status is non-zero, NEWTREE in this sequence
+will contain a lot more output than just a tree.
+
 GIT
 ---
 Part of the linkgit:git[1] suite
builtin/merge-tree.c +19 -2
index af445cb1576..d44e8d087b1 100644
@@ -396,6 +396,7 @@ enum mode {
 
 struct merge_tree_options {
 	int mode;
+	int show_messages;
 };
 
 static int real_merge(struct merge_tree_options *o,
@@ -438,18 +439,27 @@ static int real_merge(struct merge_tree_options *o,
 	merge_incore_recursive(&opt, merge_bases, parent1, parent2, &result);
 	if (result.clean < 0)
 		die(_("failure to merge"));
+
+	if (o->show_messages == -1)
+		o->show_messages = !result.clean;
+
 	puts(oid_to_hex(&result.tree->object.oid));
+	if (o->show_messages) {
+		printf("\n");
+		merge_display_update_messages(&opt, &result);
+	}
 	merge_finalize(&opt, &result);
 	return !result.clean; /* result.clean < 0 handled above */
 }
 
 int cmd_merge_tree(int argc, const char **argv, const char *prefix)
 {
-	struct merge_tree_options o = { 0 };
+	struct merge_tree_options o = { .show_messages = -1 };
 	int expected_remaining_argc;
+	int original_argc;
 
 	const char * const merge_tree_usage[] = {
-		N_("git merge-tree [--write-tree] <branch1> <branch2>"),
+		N_("git merge-tree [--write-tree] [<options>] <branch1> <branch2>"),
 		N_("git merge-tree [--trivial-merge] <base-tree> <branch1> <branch2>"),
 		NULL
 	};
@@ -459,10 +469,13 @@ int cmd_merge_tree(int argc, const char **argv, const char *prefix)
 			    MODE_REAL),
 		OPT_CMDMODE(0, "trivial-merge", &o.mode,
 			    N_("do a trivial merge only"), MODE_TRIVIAL),
+		OPT_BOOL(0, "messages", &o.show_messages,
+			 N_("also show informational/conflict messages")),
 		OPT_END()
 	};
 
 	/* Parse arguments */
+	original_argc = argc - 1; /* ignoring argv[0] */
 	argc = parse_options(argc, argv, prefix, mt_options,
 			     merge_tree_usage, PARSE_OPT_STOP_AT_NON_OPTION);
 	switch (o.mode) {
@@ -486,8 +499,12 @@ int cmd_merge_tree(int argc, const char **argv, const char *prefix)
 		break;
 	case MODE_TRIVIAL:
 		expected_remaining_argc = 3;
+		/* Removal of `--trivial-merge` is expected */
+		original_argc--;
 		break;
 	}
+	if (o.mode == MODE_TRIVIAL && argc < original_argc)
+		die(_("--trivial-merge is incompatible with all other options"));
 
 	if (argc != expected_remaining_argc)
 		usage_with_options(merge_tree_usage, mt_options);
t/t4301-merge-tree-write-tree.sh +37 -0
index e4f9421c105..25adcf36bdf 100755
@@ -103,4 +103,41 @@ test_expect_success 'Barf on too many arguments' '
 	grep "^usage: git merge-tree" expect
 '
 
+anonymize_hash() {
+	sed -e "s/[0-9a-f]\{40,\}/HASH/g" "$@"
+}
+
+test_expect_success 'test conflict notices and such' '
+	test_expect_code 1 git merge-tree --write-tree side1 side2 >out &&
+	anonymize_hash out >actual &&
+
+	# Expected results:
+	#   "greeting" should merge with conflicts
+	#   "numbers" should merge cleanly
+	#   "whatever" has *both* a modify/delete and a file/directory conflict
+	cat <<-EOF >expect &&
+	HASH
+
+	Auto-merging greeting
+	CONFLICT (content): Merge conflict in greeting
+	Auto-merging numbers
+	CONFLICT (file/directory): directory in the way of whatever from side1; moving it to whatever~side1 instead.
+	CONFLICT (modify/delete): whatever~side1 deleted in side2 and modified in side1.  Version side1 of whatever~side1 left in tree.
+	EOF
+
+	test_cmp expect actual
+'
+
+for opt in $(git merge-tree --git-completion-helper-all)
+do
+	if test $opt = "--trivial-merge" || test $opt = "--write-tree"
+	then
+		continue
+	fi
+
+	test_expect_success "usage: --trivial-merge is incompatible with $opt" '
+		test_expect_code 128 git merge-tree --trivial-merge $opt side1 side2 side3
+	'
+done
+
 test_done
Signature · 1 line
gitgitgadget
#181 lore raw

PATCH v4 7/12 merge-ort: provide a merge_get_conflicted_files() helper function

In reply to Elijah Newren via GitGitGadget

From: Elijah Newren <[email protected]>

After a merge, this function allows the user to extract the same information that would be printed by ls-files -u, which means files with their mode, oid, and stage.

---
 merge-ort.c | 31 +++++++++++++++++++++++++++++++
 merge-ort.h | 21 +++++++++++++++++++++
 2 files changed, 52 insertions(+)
merge-ort.c +31 -0
index ebaed98d53a..e1b647b0a40 100644
@@ -4274,6 +4274,37 @@ void merge_display_update_messages(struct merge_options *opt,
 	trace2_region_leave("merge", "display messages", opt->repo);
 }
 
+void merge_get_conflicted_files(struct merge_result *result,
+				struct string_list *conflicted_files)
+{
+	struct hashmap_iter iter;
+	struct strmap_entry *e;
+	struct merge_options_internal *opti = result->priv;
+
+	strmap_for_each_entry(&opti->conflicted, &iter, e) {
+		const char *path = e->key;
+		struct conflict_info *ci = e->value;
+		int i;
+
+		VERIFY_CI(ci);
+
+		for (i = MERGE_BASE; i <= MERGE_SIDE2; i++) {
+			struct stage_info *si;
+
+			if (!(ci->filemask & (1ul << i)))
+				continue;
+
+			si = xmalloc(sizeof(*si));
+			si->stage = i+1;
+			si->mode = ci->stages[i].mode;
+			oidcpy(&si->oid, &ci->stages[i].oid);
+			string_list_append(conflicted_files, path)->util = si;
+		}
+	}
+	/* string_list_sort() uses a stable sort, so we're good */
+	string_list_sort(conflicted_files);
+}
+
 void merge_switch_to_result(struct merge_options *opt,
 			    struct tree *head,
 			    struct merge_result *result,
merge-ort.h +21 -0
index e5aec45b18f..ddcc39d7270 100644
@@ -2,6 +2,7 @@
 #define MERGE_ORT_H
 
 #include "merge-recursive.h"
+#include "hash.h"
 
 struct commit;
 struct tree;
@@ -88,6 +89,26 @@ void merge_switch_to_result(struct merge_options *opt,
 void merge_display_update_messages(struct merge_options *opt,
 				   struct merge_result *result);
 
+struct stage_info {
+	struct object_id oid;
+	int mode;
+	int stage;
+};
+
+/*
+ * Provide a list of path -> {struct stage_info*} mappings for
+ * all conflicted files.  Note that each path could appear up to three
+ * times in the list, corresponding to 3 different stage entries.  In short,
+ * this basically provides the info that would be printed by `ls-files -u`.
+ *
+ * result should have been populated by a call to
+ * one of the merge_incore_[non]recursive() functions.
+ *
+ * conflicted_files should be empty before calling this function.
+ */
+void merge_get_conflicted_files(struct merge_result *result,
+				struct string_list *conflicted_files);
+
 /* Do needed cleanup when not calling merge_switch_to_result() */
 void merge_finalize(struct merge_options *opt,
 		    struct merge_result *result);
Signature · 1 line
gitgitgadget
#182 lore raw

PATCH v4 8/12 merge-tree: provide a list of which files have conflicts

In reply to Elijah Newren via GitGitGadget

From: Elijah Newren <[email protected]>

Callers of git merge-tree --write-tree will often want to know which files had conflicts. While they could potentially attempt to parse the CONFLICT notices printed, those messages are not meant to be machine readable. Provide a simpler mechanism of just printing the files (in the same format as git ls-files with quoting, but restricted to unmerged files) in the output before the free-form messages.

---
 Documentation/git-merge-tree.txt |  8 ++++++++
 builtin/merge-tree.c             | 24 ++++++++++++++++++++++--
 t/t4301-merge-tree-write-tree.sh | 11 +++++++++++
 3 files changed, 41 insertions(+), 2 deletions(-)
Documentation/git-merge-tree.txt +8 -0
index 869c38f8223..9f7eb03c6eb 100644
@@ -67,6 +67,7 @@ simply one line:
 Whereas for a conflicted merge, the output is by default of the form:
 
 	<OID of toplevel tree>
+	<Conflicted file list>
 	<Informational messages>
 
 These are discussed individually below.
@@ -78,6 +79,13 @@ This is a tree object that represents what would be checked out in the
 working tree at the end of `git merge`.  If there were conflicts, then
 files within this tree may have embedded conflict markers.
 
+Conflicted file list
+~~~~~~~~~~~~~~~~~~~~
+
+This is a sequence of lines containing a filename on each line, quoted
+as explained for the configuration variable `core.quotePath` (see
+linkgit:git-config[1]).
+
 Informational messages
 ~~~~~~~~~~~~~~~~~~~~~~
 
builtin/merge-tree.c +22 -2
index d44e8d087b1..cb4169d2271 100644
@@ -11,6 +11,9 @@
 #include "blob.h"
 #include "exec-cmd.h"
 #include "merge-blobs.h"
+#include "quote.h"
+
+static int line_termination = '\n';
 
 struct merge_list {
 	struct merge_list *next;
@@ -400,7 +403,8 @@ struct merge_tree_options {
 };
 
 static int real_merge(struct merge_tree_options *o,
-		      const char *branch1, const char *branch2)
+		      const char *branch1, const char *branch2,
+		      const char *prefix)
 {
 	struct commit *parent1, *parent2;
 	struct commit_list *common;
@@ -444,6 +448,22 @@ static int real_merge(struct merge_tree_options *o,
 		o->show_messages = !result.clean;
 
 	puts(oid_to_hex(&result.tree->object.oid));
+	if (!result.clean) {
+		struct string_list conflicted_files = STRING_LIST_INIT_NODUP;
+		const char *last = NULL;
+		int i;
+
+		merge_get_conflicted_files(&result, &conflicted_files);
+		for (i = 0; i < conflicted_files.nr; i++) {
+			const char *name = conflicted_files.items[i].string;
+			if (last && !strcmp(last, name))
+				continue;
+			write_name_quoted_relative(
+				name, prefix, stdout, line_termination);
+			last = name;
+		}
+		string_list_clear(&conflicted_files, 1);
+	}
 	if (o->show_messages) {
 		printf("\n");
 		merge_display_update_messages(&opt, &result);
@@ -511,7 +531,7 @@ int cmd_merge_tree(int argc, const char **argv, const char *prefix)
 
 	/* Do the relevant type of merge */
 	if (o.mode == MODE_REAL)
-		return real_merge(&o, argv[0], argv[1]);
+		return real_merge(&o, argv[0], argv[1], prefix);
 	else
 		return trivial_merge(argv[0], argv[1], argv[2]);
 }
t/t4301-merge-tree-write-tree.sh +11 -0
index 25adcf36bdf..0964c1145e6 100755
@@ -117,6 +117,8 @@ test_expect_success 'test conflict notices and such' '
 	#   "whatever" has *both* a modify/delete and a file/directory conflict
 	cat <<-EOF >expect &&
 	HASH
+	greeting
+	whatever~side1
 
 	Auto-merging greeting
 	CONFLICT (content): Merge conflict in greeting
@@ -140,4 +142,13 @@ do
 	'
 done
 
+test_expect_success 'Just the conflicted files without the messages' '
+	test_expect_code 1 git merge-tree --write-tree --no-messages side1 side2 >out &&
+	anonymize_hash out >actual &&
+
+	test_write_lines HASH greeting whatever~side1 >expect &&
+
+	test_cmp expect actual
+'
+
 test_done
Signature · 1 line
gitgitgadget
#183 lore raw

PATCH v4 9/12 merge-tree: provide easy access to `ls-files -u` style info

In reply to Elijah Newren via GitGitGadget

From: Elijah Newren <[email protected]>

Much like git merge updates the index with information of the form
(mode, oid, stage, name)
provide this output for conflicted files for merge-tree as well. Provide a --name-only option for users to exclude the mode, oid, and stage and only get the list of conflicted filenames.

---
 Documentation/git-merge-tree.txt | 32 ++++++++++++++++++++++++++------
 builtin/merge-tree.c             | 11 ++++++++++-
 t/t4301-merge-tree-write-tree.sh | 26 ++++++++++++++++++++++++--
 3 files changed, 60 insertions(+), 9 deletions(-)
Documentation/git-merge-tree.txt +26 -6
index 9f7eb03c6eb..6502ee0669e 100644
@@ -50,6 +50,13 @@ discuss the first form.
 OPTIONS
 -------
 
+--name-only::
+	In the Conflicted file info section, instead of writing a list
+	of (mode, oid, stage, path) tuples to output for conflicted
+	files, just provide a list of filenames with conflicts (and
+	do not list filenames multiple times if they have multiple
+	conflicting stages).
+
 --[no-]messages::
 	Write any informational messages such as "Auto-merging <path>"
 	or CONFLICT notices to the end of stdout.  If unspecified, the
@@ -67,7 +74,7 @@ simply one line:
 Whereas for a conflicted merge, the output is by default of the form:
 
 	<OID of toplevel tree>
-	<Conflicted file list>
+	<Conflicted file info>
 	<Informational messages>
 
 These are discussed individually below.
@@ -79,18 +86,23 @@ This is a tree object that represents what would be checked out in the
 working tree at the end of `git merge`.  If there were conflicts, then
 files within this tree may have embedded conflict markers.
 
-Conflicted file list
+Conflicted file info
 ~~~~~~~~~~~~~~~~~~~~
 
-This is a sequence of lines containing a filename on each line, quoted
-as explained for the configuration variable `core.quotePath` (see
-linkgit:git-config[1]).
+This is a sequence of lines with the format
+
+	<mode> <object> <stage> <filename>
+
+The filename will be quoted as explained for the configuration
+variable `core.quotePath` (see linkgit:git-config[1]).  However, if
+the `--name-only` option is passed, the mode, object, and stage will
+be omitted.
 
 Informational messages
 ~~~~~~~~~~~~~~~~~~~~~~
 
 This always starts with a blank line to separate it from the previous
-section, and then has free-form messages about the merge, such as:
+sections, and then has free-form messages about the merge, such as:
 
   * "Auto-merging <file>"
   * "CONFLICT (rename/delete): <oldfile> renamed...but deleted in..."
@@ -120,6 +132,14 @@ Thus, it could be used as a part of a series of steps such as
 Note that when the exit status is non-zero, NEWTREE in this sequence
 will contain a lot more output than just a tree.
 
+git-merge-tree was written to provide users with the same information
+that they'd have access to if using `git merge`:
+  * what would be written to the working tree (the <OID of toplevel tree>)
+  * the higher order stages that would be written to the index (the
+    <Conflicted file info>)
+  * any messages that would have been printed to stdout (the <Informational
+    messages>)
+
 GIT
 ---
 Part of the linkgit:git[1] suite
builtin/merge-tree.c +10 -1
index cb4169d2271..1d4d6637b90 100644
@@ -400,6 +400,7 @@ enum mode {
 struct merge_tree_options {
 	int mode;
 	int show_messages;
+	int name_only;
 };
 
 static int real_merge(struct merge_tree_options *o,
@@ -456,7 +457,11 @@ static int real_merge(struct merge_tree_options *o,
 		merge_get_conflicted_files(&result, &conflicted_files);
 		for (i = 0; i < conflicted_files.nr; i++) {
 			const char *name = conflicted_files.items[i].string;
-			if (last && !strcmp(last, name))
+			struct stage_info *c = conflicted_files.items[i].util;
+			if (!o->name_only)
+				printf("%06o %s %d\t",
+				       c->mode, oid_to_hex(&c->oid), c->stage);
+			else if (last && !strcmp(last, name))
 				continue;
 			write_name_quoted_relative(
 				name, prefix, stdout, line_termination);
@@ -491,6 +496,10 @@ int cmd_merge_tree(int argc, const char **argv, const char *prefix)
 			    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, "name-only",
+			   &o.name_only,
+			   N_("list filenames without modes/oids/stages"),
+			   PARSE_OPT_NONEG),
 		OPT_END()
 	};
 
t/t4301-merge-tree-write-tree.sh +24 -2
index 0964c1145e6..4ee85439372 100755
@@ -65,6 +65,7 @@ test_expect_success 'Content merge and a few conflicts' '
 	expected_tree=$(cat .git/AUTO_MERGE) &&
 
 	# We will redo the merge, while we are still in a conflicted state!
+	git ls-files -u >conflicted-file-info &&
 	test_when_finished "git reset --hard" &&
 
 	test_expect_code 1 git merge-tree --write-tree side1 side2 >RESULT &&
@@ -108,7 +109,7 @@ anonymize_hash() {
 }
 
 test_expect_success 'test conflict notices and such' '
-	test_expect_code 1 git merge-tree --write-tree side1 side2 >out &&
+	test_expect_code 1 git merge-tree --write-tree --name-only side1 side2 >out &&
 	anonymize_hash out >actual &&
 
 	# Expected results:
@@ -143,7 +144,7 @@ do
 done
 
 test_expect_success 'Just the conflicted files without the messages' '
-	test_expect_code 1 git merge-tree --write-tree --no-messages side1 side2 >out &&
+	test_expect_code 1 git merge-tree --write-tree --no-messages --name-only side1 side2 >out &&
 	anonymize_hash out >actual &&
 
 	test_write_lines HASH greeting whatever~side1 >expect &&
@@ -151,4 +152,25 @@ test_expect_success 'Just the conflicted files without the messages' '
 	test_cmp expect actual
 '
 
+test_expect_success 'Check conflicted oids and modes without messages' '
+	test_expect_code 1 git merge-tree --write-tree --no-messages side1 side2 >out &&
+	anonymize_hash out >actual &&
+
+	# Compare the basic output format
+	q_to_tab >expect <<-\EOF &&
+	HASH
+	100644 HASH 1Qgreeting
+	100644 HASH 2Qgreeting
+	100644 HASH 3Qgreeting
+	100644 HASH 1Qwhatever~side1
+	100644 HASH 2Qwhatever~side1
+	EOF
+
+	test_cmp expect actual &&
+
+	# Check the actual hashes against the `ls-files -u` output too
+	tail -n +2 out | sed -e s/side1/HEAD/ >actual &&
+	test_cmp conflicted-file-info actual
+'
+
 test_done
Signature · 1 line
gitgitgadget
#184 lore raw

PATCH v4 10/12 merge-tree: allow `ls-files -u` style info to be NUL terminated

In reply to Elijah Newren via GitGitGadget

From: Elijah Newren <[email protected]>

Much as git ls-files has a -z option, let's add one to merge-tree so that the conflict-info section can be NUL terminated (and avoid quoting of unusual filenames).

---
 Documentation/git-merge-tree.txt | 21 +++++++++++++----
 builtin/merge-tree.c             |  6 +++--
 t/t4301-merge-tree-write-tree.sh | 40 ++++++++++++++++++++++++++++++++
 3 files changed, 61 insertions(+), 6 deletions(-)
Documentation/git-merge-tree.txt +17 -4
index 6502ee0669e..ada4595b4fc 100644
@@ -50,6 +50,12 @@ discuss the first form.
 OPTIONS
 -------
 
+-z::
+	Do not quote filenames in the <Conflicted file info> section,
+	and end each filename with a NUL character rather than
+	newline.  Also begin the messages section with a NUL character
+	instead of a newline.  See OUTPUT below for more information.
+
 --name-only::
 	In the Conflicted file info section, instead of writing a list
 	of (mode, oid, stage, path) tuples to output for conflicted
@@ -84,7 +90,8 @@ OID of toplevel tree
 
 This is a tree object that represents what would be checked out in the
 working tree at the end of `git merge`.  If there were conflicts, then
-files within this tree may have embedded conflict markers.
+files within this tree may have embedded conflict markers.  This section
+is always followed by a newline (or NUL if `-z` is passed).
 
 Conflicted file info
 ~~~~~~~~~~~~~~~~~~~~
@@ -96,19 +103,25 @@ This is a sequence of lines with the format
 The filename will be quoted as explained for the configuration
 variable `core.quotePath` (see linkgit:git-config[1]).  However, if
 the `--name-only` option is passed, the mode, object, and stage will
-be omitted.
+be omitted.  If `-z` is passed, the "lines" are terminated by a NUL
+character instead of a newline character.
 
 Informational messages
 ~~~~~~~~~~~~~~~~~~~~~~
 
-This always starts with a blank line to separate it from the previous
-sections, and then has free-form messages about the merge, such as:
+This always starts with a blank line (or NUL if `-z` is passed) to
+separate it from the previous sections, and then has free-form
+messages about the merge, such as:
 
   * "Auto-merging <file>"
   * "CONFLICT (rename/delete): <oldfile> renamed...but deleted in..."
   * "Failed to merge submodule <submodule> (<reason>)"
   * "Warning: cannot merge binary files: <filename>"
 
+Note that these free-form messages will never have a NUL character
+in or between them, even if -z is passed.  It is simply a large block
+of text taking up the remainder of the output.
+
 EXIT STATUS
 -----------
 
builtin/merge-tree.c +4 -2
index 1d4d6637b90..825255667b1 100644
@@ -448,7 +448,7 @@ static int real_merge(struct merge_tree_options *o,
 	if (o->show_messages == -1)
 		o->show_messages = !result.clean;
 
-	puts(oid_to_hex(&result.tree->object.oid));
+	printf("%s%c", oid_to_hex(&result.tree->object.oid), line_termination);
 	if (!result.clean) {
 		struct string_list conflicted_files = STRING_LIST_INIT_NODUP;
 		const char *last = NULL;
@@ -470,7 +470,7 @@ static int real_merge(struct merge_tree_options *o,
 		string_list_clear(&conflicted_files, 1);
 	}
 	if (o->show_messages) {
-		printf("\n");
+		putchar(line_termination);
 		merge_display_update_messages(&opt, &result);
 	}
 	merge_finalize(&opt, &result);
@@ -496,6 +496,8 @@ int cmd_merge_tree(int argc, const char **argv, const char *prefix)
 			    N_("do a trivial merge only"), MODE_TRIVIAL),
 		OPT_BOOL(0, "messages", &o.show_messages,
 			 N_("also show informational/conflict messages")),
+		OPT_SET_INT('z', NULL, &line_termination,
+			    N_("separate paths with the NUL character"), '\0'),
 		OPT_BOOL_F(0, "name-only",
 			   &o.name_only,
 			   N_("list filenames without modes/oids/stages"),
t/t4301-merge-tree-write-tree.sh +40 -0
index 4ee85439372..fe476ed1bcc 100755
@@ -173,4 +173,44 @@ test_expect_success 'Check conflicted oids and modes without messages' '
 	test_cmp conflicted-file-info actual
 '
 
+test_expect_success 'NUL terminated conflicted file "lines"' '
+	git checkout -b tweak1 side1 &&
+	test_write_lines zero 1 2 3 4 5 6 >numbers &&
+	git add numbers &&
+	git mv numbers "Αυτά μου φαίνονται κινέζικα" &&
+	git commit -m "Renamed numbers" &&
+
+	test_expect_code 1 git merge-tree --write-tree -z tweak1 side2 >out &&
+	anonymize_hash out >actual &&
+
+	# Expected results:
+	#   "greeting" should merge with conflicts
+	#   "whatever" has *both* a modify/delete and a file/directory conflict
+	#   "Αυτά μου φαίνονται κινέζικα" should have a conflict
+	echo HASH | lf_to_nul >expect &&
+
+	q_to_tab <<-EOF | lf_to_nul >>expect &&
+	100644 HASH 1Qgreeting
+	100644 HASH 2Qgreeting
+	100644 HASH 3Qgreeting
+	100644 HASH 1Qwhatever~tweak1
+	100644 HASH 2Qwhatever~tweak1
+	100644 HASH 1QΑυτά μου φαίνονται κινέζικα
+	100644 HASH 2QΑυτά μου φαίνονται κινέζικα
+	100644 HASH 3QΑυτά μου φαίνονται κινέζικα
+
+	EOF
+
+	cat <<-EOF >>expect &&
+	Auto-merging greeting
+	CONFLICT (content): Merge conflict in greeting
+	CONFLICT (file/directory): directory in the way of whatever from tweak1; moving it to whatever~tweak1 instead.
+	CONFLICT (modify/delete): whatever~tweak1 deleted in side2 and modified in tweak1.  Version tweak1 of whatever~tweak1 left in tree.
+	Auto-merging Αυτά μου φαίνονται κινέζικα
+	CONFLICT (content): Merge conflict in Αυτά μου φαίνονται κινέζικα
+	EOF
+
+	test_cmp expect actual
+'
+
 test_done
Signature · 1 line
gitgitgadget
#185 lore raw

PATCH v4 11/12 merge-tree: add a --allow-unrelated-histories flag

In reply to Elijah Newren via GitGitGadget

From: Elijah Newren <[email protected]>

Folks may want to merge histories that have no common ancestry; provide a flag with the same name as used by git merge to allow this.

---
 Documentation/git-merge-tree.txt |  5 +++++
 builtin/merge-tree.c             |  7 ++++++-
 t/t4301-merge-tree-write-tree.sh | 24 +++++++++++++++++++++++-
 3 files changed, 34 insertions(+), 2 deletions(-)
Documentation/git-merge-tree.txt +5 -0
index ada4595b4fc..3f566477dcb 100644
@@ -69,6 +69,11 @@ OPTIONS
 	default is to include these messages if there are merge
 	conflicts, and to omit them otherwise.
 
+--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
+	check and make the merge proceed anyway.
+
 OUTPUT
 ------
 
builtin/merge-tree.c +6 -1
index 825255667b1..911504ad694 100644
@@ -399,6 +399,7 @@ enum mode {
 
 struct merge_tree_options {
 	int mode;
+	int allow_unrelated_histories;
 	int show_messages;
 	int name_only;
 };
@@ -436,7 +437,7 @@ static int real_merge(struct merge_tree_options *o,
 	 * merge_incore_recursive in merge-ort.h
 	 */
 	common = get_merge_bases(parent1, parent2);
-	if (!common)
+	if (!common && !o->allow_unrelated_histories)
 		die(_("refusing to merge unrelated histories"));
 	for (j = common; j; j = j->next)
 		commit_list_insert(j->item, &merge_bases);
@@ -502,6 +503,10 @@ int cmd_merge_tree(int argc, const char **argv, const char *prefix)
 			   &o.name_only,
 			   N_("list filenames without modes/oids/stages"),
 			   PARSE_OPT_NONEG),
+		OPT_BOOL_F(0, "allow-unrelated-histories",
+			   &o.allow_unrelated_histories,
+			   N_("allow merging unrelated histories"),
+			   PARSE_OPT_NONEG),
 		OPT_END()
 	};
 
t/t4301-merge-tree-write-tree.sh +23 -1
index fe476ed1bcc..5cb546083d7 100755
@@ -44,7 +44,13 @@ test_expect_success setup '
 	git checkout side3 &&
 	git mv numbers sequence &&
 	test_tick &&
-	git commit -m rename-numbers
+	git commit -m rename-numbers &&
+
+	git switch --orphan unrelated &&
+	>something-else &&
+	git add something-else &&
+	test_tick &&
+	git commit -m first-commit
 '
 
 test_expect_success 'Clean merge' '
@@ -213,4 +219,20 @@ test_expect_success 'NUL terminated conflicted file "lines"' '
 	test_cmp expect actual
 '
 
+test_expect_success 'error out by default for unrelated histories' '
+	test_expect_code 128 git merge-tree --write-tree side1 unrelated 2>error &&
+
+	grep "refusing to merge unrelated histories" error
+'
+
+test_expect_success 'can override merge of unrelated histories' '
+	git merge-tree --write-tree --allow-unrelated-histories side1 unrelated >tree &&
+	TREE=$(cat tree) &&
+
+	git rev-parse side1:numbers side1:greeting side1:whatever unrelated:something-else >expect &&
+	git rev-parse $TREE:numbers $TREE:greeting $TREE:whatever $TREE:something-else >actual &&
+
+	test_cmp expect actual
+'
+
 test_done
Signature · 1 line
gitgitgadget
#186 lore raw

PATCH v4 12/12 git-merge-tree.txt: add a section on potentional usage mistakes

In reply to Elijah Newren via GitGitGadget

From: Elijah Newren <[email protected]>

---
 Documentation/git-merge-tree.txt | 46 ++++++++++++++++++++++++++++++++
 1 file changed, 46 insertions(+)
Documentation/git-merge-tree.txt +46 -0
index 3f566477dcb..4520bbf020a 100644
@@ -158,6 +158,52 @@ that they'd have access to if using `git merge`:
   * any messages that would have been printed to stdout (the <Informational
     messages>)
 
+MISTAKES TO AVOID
+-----------------
+
+Do NOT look through the resulting toplevel tree to try to find which
+files conflict; parse the <Conflicted file info> section instead.  Not
+only would parsing an entire tree be horrendously slow in large
+repositories, there are numerous types of conflicts not representable by
+conflict markers (modify/delete, mode conflict, binary file changed on
+both sides, file/directory conflicts, various rename conflict
+permutations, etc.)
+
+Do NOT interpret an empty <Conflicted file info> list as a clean merge;
+check the exit status.  A merge can have conflicts without having
+individual files conflict (there are a few types of directory rename
+conflicts that fall into this category, and others might also be added
+in the future).
+
+Do NOT attempt to guess or make the user guess the conflict types from
+the <Conflicted file info> list.  The information there is insufficient
+to do so.  For example: Rename/rename(1to2) conflicts (both sides
+renamed the same file differently) will result in three different file
+having higher order stages (but each only has one higher order stage),
+with no way (short of the <Informational messages> section) to determine
+which three files are related.  File/directory conflicts also result in
+a file with exactly one higher order stage.
+Possibly-involved-in-directory-rename conflicts (when
+"merge.directoryRenames" is unset or set to "conflicts") also result in
+a file with exactly one higher order stage.  In all cases, the
+<Informational messages> section has the necessary info, though it is
+not designed to be machine parseable.
+
+Do NOT assume all filenames listed in the <Informational messages>
+section had conflicts.  Messages can be included for files that have no
+conflicts, such as "Auto-merging <file>".
+
+AVOID taking the OIDS from the <Conflicted file info> and re-merging
+them to present the conflicts to the user.  This will lose information.
+Instead, look up the version of the file found within the <OID of
+toplevel tree> and show that instead.  In particular, the latter will
+have conflict markers annotated with the original branch/commit being
+merged and, if renames were involved, the original filename.  While you
+could include the original branch/commit in the conflict marker
+annotations when re-merging, the original filename is not available from
+the <Conflicted file info> and thus you would be losing information that
+might help the user resolve the conflict.
+
 GIT
 ---
 Part of the linkgit:git[1] suite
Signature · 1 line
gitgitgadget
#187 lore raw

v5 cover In-core git merge-tree ("Server side merges")

In reply to Elijah Newren via GitGitGadget

== Basic Summary ==

This series introduces a new mode to git merge-tree allowing it to perform real merges (three-way text content merges, recursive ancestor consolidation, rename detection, proper directory/file conflict handling, etc.) and write the result as a toplevel tree. It doesn't touch the working tree or index, and doesn't create any commits or update any refs. It could be used to do merges when in a bare repository (thus potentially making it of interest to Git hosting sites, i.e. "Server side merges"), or for doing a merge of branches that aren't checked out.

It does not handle similar functionality for cherry-picks, rebases, or reverts; that is also of interest, but is being deferred for a future series.

== Quick Overview ==

* Patches 1-2: preparatory cleanups
* Patches 3-4: implement basic real merges
* Patches 5-6: include informational messages ("CONFLICT" messages and such) in output
* Patches 7-10: add ability to include ls-files -u style of info in the output
* Patch 11: support --allow-unrelated-histories
* Patch 12: augment the manual with potential usage mistakes

== Updates Log ==

Many thanks to the many reviewers who provided good feedback on the most recent round -- Junio, Ævar, Josh, Emily, and perhaps some others I've forgotten from review club.

Updates since v4:

* Fixed double "is" in documentation.
* Fixed a few small items with testcases

Updates since v3 (or v5, if you include the rounds at
https://lore.kernel.org/git/[email protected]/):

* Dropped previous patches 5, 6, and 8 of the old series; they weren't being used and opened a can of worms[1]
* [Patch 3] Restructured argument checking, including using an enum
* [Patch 4] Restored the extended paragraph about the deprecated form of git-merge-tree, mentioned write-tree in plumbing commands, and a few other small fixups to the documentation
* [Patch 4] Also provide an example of a clean merge rather than just a conflicted one
* [Patch 6] Fix the incompatible arguments check and add some tests for it
* [Patch 6] Introduce an anonymize_hash() shell function to make tests easier to read (less repeated sed)
* [Patch 9] Rename --exclude-modes-oids-stages to --name-only; no short option for now
* [Patch 10] When -z passed, the tree in the first section should have a trailing NUL rather than trailing newline [1]
https://lore.kernel.org/git/CABPp-BEKuXHELVx4=5JJTj5HVOKZ=Y-4G4BK47BCZYYRSrkFsQ@mail.gmail.com/

Stuff NOT included that reviewers brought up in earlier rounds:

* Very generic (mode, oid, stage, filename) printing formatting[2]
* Always printing 3 stages for each filename with conflicts[3]
* Attempting to group conflict stages by logical conflict rather than by affected target filepath[4]
* Providing similar functionality for doing cherry-picks/rebases/reverts, i.e. a scheme for three-way merges with a specified merge-base[5]. That's being deferred to a future series. [2]
https://lore.kernel.org/git/CABPp-BGnOes7J_piDyBUeuLVm274w4-9G3k0vR-0it3z7TPn_w@mail.gmail.com/
[3]
https://lore.kernel.org/git/CABPp-BG2rMEYBLuBW=0wtpJe4aUFGCFa8D0NTSKz9Sm+CkXPxw@mail.gmail.com/
[4]
https://lore.kernel.org/git/CABPp-BGCL0onSmpgKuO1k2spYCkx=v27ed9TSSxFib=OdDcLbw@mail.gmail.com/
[5]
https://lore.kernel.org/git/CABPp-BEaemkGGm0cSofP0gau7YN-y6HFoi0yJbHA8+iGjxsYSA@mail.gmail.com/

Updates since v2:

* Improved patches from Dscho for the diff_warn_rename_limit() handling
* Add a -z option for NUL-terminated conflict info lines (so that filenames do not have to be quoted)

Updates since v1 (or v3 depending on how you count; thanks to René, Ævar, Christian, Dscho for very helpful feedback):

* New patch from Dscho allowing diff_warn_rename_limit() to print somewhere other than stdout (I hope he's okay with me including his Signed-off-by)
* Now prints filenames relative to prefix, much like ls-files
* Renamed --exclude-oids-and-modes to --exclude-modes-oids-stages and gave it a -l shorthand; I'm wondering if I should just drop this option, though.
* And numerous cleanups, in lots of areas:
* Multiple parse-options cleanups
* Lots of commit message cleanups
* Wording tweaks to the "Description" section of the manual
* Several small code cleanups
* I dropped the RFC label

Updates since original submission v2 (thanks to Christian, Dscho, Ramsay, and René for suggestions and comments):

* Significant changes to output format:
* Flags no longer take a filename for additional output; they write to stdout instead.
* More information included by default when there are conflicts (no need to request it with additional flags, instead flags can be used to suppress it).
* Provide (mode, oid, stage, file) tuples -- i.e. ls-files -u style of information -- when there are conflicts. Add a flag to only list conflicted files if that's preferred.
* Much more thorough manual for git-merge-tree.txt
* Renamed option from --real to --write-tree
* Accept an optional --trivial-merge option to get old style merge-tree behavior
* Allow both --write-tree and --trivial-merge to be omitted since we can deduce which from number of arguments
* Document exit code when the merge cannot be run (so we can distinguish other error cases from conflicts)
* testcase cleanups: test_tick, early skip of test when using recursive backend, variable renames, etc.
* various minor code cleanups
* Add a new --allow-unrelated-histories option (with same meaning as the one used in git merge)
* Rebased on top of en/remerge-diff to avoid a small conflict

Updates since original submission v1 (thanks to Johannes Altmanninger and Fabian for suggestions):

* Fixed a bad patch splitting, and a style issue pointed out by Johannes Altimanninger
* Fixed misleading commit messages in new test cases
* Fixed my comments about how commit-tree could be used to correctly use two -p flags

Elijah Newren (12):
merge-tree: rename merge_trees() to trivial_merge_trees()
merge-tree: move logic for existing merge into new function
merge-tree: add option parsing and initial shell for real merge
function
merge-tree: implement real merges
merge-ort: split out a separate display_update_messages() function
merge-tree: support including merge messages in output
merge-ort: provide a merge_get_conflicted_files() helper function
merge-tree: provide a list of which files have conflicts
merge-tree: provide easy access to ls-files -u style info
merge-tree: allow ls-files -u style info to be NUL terminated
merge-tree: add a --allow-unrelated-histories flag git-merge-tree.txt: add a section on potentional usage mistakes

 Documentation/git-merge-tree.txt | 204 ++++++++++++++++++++++++--
 builtin/merge-tree.c             | 189 ++++++++++++++++++++++--
 git.c                            |   2 +-
 merge-ort.c                      | 109 +++++++++-----
 merge-ort.h                      |  29 ++++
 t/t4301-merge-tree-write-tree.sh | 238 +++++++++++++++++++++++++++++++
 6 files changed, 709 insertions(+), 62 deletions(-)
 create mode 100755 t/t4301-merge-tree-write-tree.sh

base-commit: ea5df61cf358d3c831189e2f04863abc2157e3e1
Published-As: https://github.com/gitgitgadget/git/releases/tag/pr-1122%2Fnewren%2Fin-core-merge-tree-v5
Fetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-1122/newren/in-core-merge-tree-v5
Pull-Request: https://github.com/gitgitgadget/git/pull/1122

Range-diff vs v4:

  1:  4a7cd5542bb =  1:  4a7cd5542bb merge-tree: rename merge_trees() to trivial_merge_trees()
  2:  4780ff6784d =  2:  4780ff6784d merge-tree: move logic for existing merge into new function
  3:  60253745f5c =  3:  60253745f5c merge-tree: add option parsing and initial shell for real merge function
  4:  d7b51da94e6 !  4:  7994775a934 merge-tree: implement real merges
     @@ Documentation/git-merge-tree.txt: git-merge-tree(1)
      +conflicting stages to the standard output in a semi-diff format.
      +Since this was designed for higher level scripts to consume and merge
      +the results back into the index, it omits entries that match
     -+<branch1>.  The result of this second form is is similar to what
     ++<branch1>.  The result of this second form is similar to what
      +three-way 'git read-tree -m' does, but instead of storing the results
      +in the index, the command outputs the entries to the standard output.
      +This form not only has limited applicability, the output format is
     @@ t/t4301-merge-tree-write-tree.sh (new)
      +. ./test-lib.sh
      +
      +# This test is ort-specific
     -+if test "${GIT_TEST_MERGE_ALGORITHM}" != "ort"
     ++if test "$GIT_TEST_MERGE_ALGORITHM" != "ort"
      +then
      +	skip_all="GIT_TEST_MERGE_ALGORITHM != ort"
      +	test_done
     @@ t/t4301-merge-tree-write-tree.sh (new)
      +'
      +
      +test_expect_success 'Clean merge' '
     -+	git merge-tree --write-tree side1 side3 >RESULT &&
     ++	TREE_OID=$(git merge-tree --write-tree side1 side3) &&
      +	q_to_tab <<-EOF >expect &&
      +	100644 blob $(git rev-parse side1:greeting)Qgreeting
      +	100644 blob $(git rev-parse side1:numbers)Qsequence
      +	100644 blob $(git rev-parse side1:whatever)Qwhatever
      +	EOF
      +
     -+	git ls-tree $(cat RESULT) >actual &&
     ++	git ls-tree $TREE_OID >actual &&
      +	test_cmp expect actual
      +'
      +
      +test_expect_success 'Content merge and a few conflicts' '
      +	git checkout side1^0 &&
      +	test_must_fail git merge side2 &&
     -+	expected_tree=$(cat .git/AUTO_MERGE) &&
     ++	expected_tree=$(git rev-parse AUTO_MERGE) &&
      +
      +	# We will redo the merge, while we are still in a conflicted state!
      +	test_when_finished "git reset --hard" &&
     @@ t/t4301-merge-tree-write-tree.sh (new)
      +
      +	# greeting should have a merge conflict
      +	git show ${expected_tree}:greeting >tmp &&
     -+	cat tmp | sed -e s/HEAD/side1/ >expect &&
     ++	sed -e s/HEAD/side1/ tmp >expect &&
      +	git show ${actual_tree}:greeting >actual &&
      +	test_cmp expect actual
      +'
  5:  58a5594aeb6 =  5:  e0f95e094cf merge-ort: split out a separate display_update_messages() function
  6:  fa55cb4d644 =  6:  90c4adecb23 merge-tree: support including merge messages in output
  7:  f3ad7add515 =  7:  12e2351092a merge-ort: provide a merge_get_conflicted_files() helper function
  8:  6058190d1b1 =  8:  5bb7d3725ad merge-tree: provide a list of which files have conflicts
  9:  435f66ea699 !  9:  3c2ca198cec merge-tree: provide easy access to `ls-files -u` style info

@@ builtin/merge-tree.c: int cmd_merge_tree(int argc, const char **argv, const char

## t/t4301-merge-tree-write-tree.sh ##
@@ t/t4301-merge-tree-write-tree.sh: test_expect_success 'Content merge and a few conflicts' '
- expected_tree=$(cat .git/AUTO_MERGE) &&
+ expected_tree=$(git rev-parse AUTO_MERGE) &&

# We will redo the merge, while we are still in a conflicted state!
+ git ls-files -u >conflicted-file-info &&

 10:  5f253e298b3 = 10:  6e89e17693a merge-tree: allow `ls-files -u` style info to be NUL terminated
 11:  e706cf31c6e = 11:  6ddd5ffde9c merge-tree: add a --allow-unrelated-histories flag
 12:  c279236ab65 = 12:  7abf633b638 git-merge-tree.txt: add a section on potentional usage mistakes
Signature · 1 line
gitgitgadget
#188 lore raw

PATCH v5 1/12 merge-tree: rename merge_trees() to trivial_merge_trees()

From: Elijah Newren <[email protected]>

merge-recursive.h defined its own merge_trees() function, different than the one found in builtin/merge-tree.c. That was okay in the past, but we want merge-tree to be able to use the merge-ort functions, which will end up including merge-recursive.h. Rename the function found in builtin/merge-tree.c to avoid the conflict.

---
 builtin/merge-tree.c | 8 ++++----
 1 file changed, 4 insertions(+), 4 deletions(-)
builtin/merge-tree.c +4 -4
index 5dc94d6f880..06f9eee9f78 100644
@@ -28,7 +28,7 @@ static void add_merge_entry(struct merge_list *entry)
 	merge_result_end = &entry->next;
 }
 
-static void merge_trees(struct tree_desc t[3], const char *base);
+static void trivial_merge_trees(struct tree_desc t[3], const char *base);
 
 static const char *explanation(struct merge_list *entry)
 {
@@ -225,7 +225,7 @@ static void unresolved_directory(const struct traverse_info *info,
 	buf2 = fill_tree_descriptor(r, t + 2, ENTRY_OID(n + 2));
 #undef ENTRY_OID
 
-	merge_trees(t, newbase);
+	trivial_merge_trees(t, newbase);
 
 	free(buf0);
 	free(buf1);
@@ -342,7 +342,7 @@ static int threeway_callback(int n, unsigned long mask, unsigned long dirmask, s
 	return mask;
 }
 
-static void merge_trees(struct tree_desc t[3], const char *base)
+static void trivial_merge_trees(struct tree_desc t[3], const char *base)
 {
 	struct traverse_info info;
 
@@ -378,7 +378,7 @@ int cmd_merge_tree(int argc, const char **argv, const char *prefix)
 	buf1 = get_tree_descriptor(r, t+0, argv[1]);
 	buf2 = get_tree_descriptor(r, t+1, argv[2]);
 	buf3 = get_tree_descriptor(r, t+2, argv[3]);
-	merge_trees(t, "");
+	trivial_merge_trees(t, "");
 	free(buf1);
 	free(buf2);
 	free(buf3);
Signature · 1 line
gitgitgadget
#189 lore raw

PATCH v5 2/12 merge-tree: move logic for existing merge into new function

In reply to Elijah Newren via GitGitGadget

From: Elijah Newren <[email protected]>

In preparation for adding a non-trivial merge capability to merge-tree, move the existing merge logic for trivial merges into a new function.

---
 builtin/merge-tree.c | 12 ++++++++----
 1 file changed, 8 insertions(+), 4 deletions(-)
builtin/merge-tree.c +8 -4
index 06f9eee9f78..914ec960b7e 100644
@@ -366,15 +366,12 @@ static void *get_tree_descriptor(struct repository *r,
 	return buf;
 }
 
-int cmd_merge_tree(int argc, const char **argv, const char *prefix)
+static int trivial_merge(int argc, const char **argv)
 {
 	struct repository *r = the_repository;
 	struct tree_desc t[3];
 	void *buf1, *buf2, *buf3;
 
-	if (argc != 4)
-		usage(merge_tree_usage);
-
 	buf1 = get_tree_descriptor(r, t+0, argv[1]);
 	buf2 = get_tree_descriptor(r, t+1, argv[2]);
 	buf3 = get_tree_descriptor(r, t+2, argv[3]);
@@ -386,3 +383,10 @@ int cmd_merge_tree(int argc, const char **argv, const char *prefix)
 	show_result();
 	return 0;
 }
+
+int cmd_merge_tree(int argc, const char **argv, const char *prefix)
+{
+	if (argc != 4)
+		usage(merge_tree_usage);
+	return trivial_merge(argc, argv);
+}
Signature · 1 line
gitgitgadget
#190 lore raw

PATCH v5 3/12 merge-tree: add option parsing and initial shell for real merge function

In reply to Elijah Newren via GitGitGadget

From: Elijah Newren <[email protected]>

Let merge-tree accept a --write-tree parameter for choosing real merges instead of trivial merges, and accept an optional --trivial-merge option to get the traditional behavior. Note that these accept different numbers of arguments, though, so these names need not actually be used.

Note that real merges differ from trivial merges in that they handle:
- three way content merges
- recursive ancestor consolidation
- renames
- proper directory/file conflict handling
- etc.
Basically all the stuff you'd expect from git merge, just without updating the index and working tree. The initial shell added here does nothing more than die with "real merges are not yet implemented", but that will be fixed in subsequent commits.

---
 builtin/merge-tree.c | 84 +++++++++++++++++++++++++++++++++++++++-----
 git.c                |  2 +-
 2 files changed, 76 insertions(+), 10 deletions(-)
builtin/merge-tree.c +75 -9
index 914ec960b7e..0f9d928e862 100644
@@ -3,13 +3,12 @@
 #include "tree-walk.h"
 #include "xdiff-interface.h"
 #include "object-store.h"
+#include "parse-options.h"
 #include "repository.h"
 #include "blob.h"
 #include "exec-cmd.h"
 #include "merge-blobs.h"
 
-static const char merge_tree_usage[] = "git merge-tree <base-tree> <branch1> <branch2>";
-
 struct merge_list {
 	struct merge_list *next;
 	struct merge_list *link;	/* other stages for this object */
@@ -366,15 +365,17 @@ static void *get_tree_descriptor(struct repository *r,
 	return buf;
 }
 
-static int trivial_merge(int argc, const char **argv)
+static int trivial_merge(const char *base,
+			 const char *branch1,
+			 const char *branch2)
 {
 	struct repository *r = the_repository;
 	struct tree_desc t[3];
 	void *buf1, *buf2, *buf3;
 
-	buf1 = get_tree_descriptor(r, t+0, argv[1]);
-	buf2 = get_tree_descriptor(r, t+1, argv[2]);
-	buf3 = get_tree_descriptor(r, t+2, argv[3]);
+	buf1 = get_tree_descriptor(r, t+0, base);
+	buf2 = get_tree_descriptor(r, t+1, branch1);
+	buf3 = get_tree_descriptor(r, t+2, branch2);
 	trivial_merge_trees(t, "");
 	free(buf1);
 	free(buf2);
@@ -384,9 +385,74 @@ static int trivial_merge(int argc, const char **argv)
 	return 0;
 }
 
+enum mode {
+	MODE_UNKNOWN,
+	MODE_TRIVIAL,
+	MODE_REAL,
+};
+
+struct merge_tree_options {
+	int mode;
+};
+
+static int real_merge(struct merge_tree_options *o,
+		      const char *branch1, const char *branch2)
+{
+	die(_("real merges are not yet implemented"));
+}
+
 int cmd_merge_tree(int argc, const char **argv, const char *prefix)
 {
-	if (argc != 4)
-		usage(merge_tree_usage);
-	return trivial_merge(argc, argv);
+	struct merge_tree_options o = { 0 };
+	int expected_remaining_argc;
+
+	const char * const merge_tree_usage[] = {
+		N_("git merge-tree [--write-tree] <branch1> <branch2>"),
+		N_("git merge-tree [--trivial-merge] <base-tree> <branch1> <branch2>"),
+		NULL
+	};
+	struct option mt_options[] = {
+		OPT_CMDMODE(0, "write-tree", &o.mode,
+			    N_("do a real merge instead of a trivial merge"),
+			    MODE_REAL),
+		OPT_CMDMODE(0, "trivial-merge", &o.mode,
+			    N_("do a trivial merge only"), MODE_TRIVIAL),
+		OPT_END()
+	};
+
+	/* Parse arguments */
+	argc = parse_options(argc, argv, prefix, mt_options,
+			     merge_tree_usage, PARSE_OPT_STOP_AT_NON_OPTION);
+	switch (o.mode) {
+	default:
+		BUG("unexpected command mode %d", o.mode);
+	case MODE_UNKNOWN:
+		switch (argc) {
+		default:
+			usage_with_options(merge_tree_usage, mt_options);
+		case 2:
+			o.mode = MODE_REAL;
+			break;
+		case 3:
+			o.mode = MODE_TRIVIAL;
+			break;
+		}
+		expected_remaining_argc = argc;
+		break;
+	case MODE_REAL:
+		expected_remaining_argc = 2;
+		break;
+	case MODE_TRIVIAL:
+		expected_remaining_argc = 3;
+		break;
+	}
+
+	if (argc != expected_remaining_argc)
+		usage_with_options(merge_tree_usage, mt_options);
+
+	/* Do the relevant type of merge */
+	if (o.mode == MODE_REAL)
+		return real_merge(&o, argv[0], argv[1]);
+	else
+		return trivial_merge(argv[0], argv[1], argv[2]);
 }
git.c +1 -1
index 5ff21be21f3..6090a1289db 100644
@@ -558,7 +558,7 @@ static struct cmd_struct commands[] = {
 	{ "merge-recursive-ours", cmd_merge_recursive, RUN_SETUP | NEED_WORK_TREE | NO_PARSEOPT },
 	{ "merge-recursive-theirs", cmd_merge_recursive, RUN_SETUP | NEED_WORK_TREE | NO_PARSEOPT },
 	{ "merge-subtree", cmd_merge_recursive, RUN_SETUP | NEED_WORK_TREE | NO_PARSEOPT },
-	{ "merge-tree", cmd_merge_tree, RUN_SETUP | NO_PARSEOPT },
+	{ "merge-tree", cmd_merge_tree, RUN_SETUP },
 	{ "mktag", cmd_mktag, RUN_SETUP | NO_PARSEOPT },
 	{ "mktree", cmd_mktree, RUN_SETUP },
 	{ "multi-pack-index", cmd_multi_pack_index, RUN_SETUP },
Signature · 1 line
gitgitgadget
#191 lore raw

PATCH v5 4/12 merge-tree: implement real merges

In reply to Elijah Newren via GitGitGadget

From: Elijah Newren <[email protected]>

This adds the ability to perform real merges rather than just trivial merges (meaning handling three way content merges, recursive ancestor consolidation, renames, proper directory/file conflict handling, and so forth). However, unlike git merge, the working tree and index are left alone and no branch is updated.

The only output is:
- the toplevel resulting tree printed on stdout
- exit status of 0 (clean), 1 (conflicts present), anything else (merge could not be performed; unknown if clean or conflicted)

This output is meant to be used by some higher level script, perhaps in a sequence of steps like this:

   NEWTREE=$(git merge-tree --write-tree $BRANCH1 $BRANCH2)
   test $? -eq 0 || die "There were conflicts..."
   NEWCOMMIT=$(git commit-tree $NEWTREE -p $BRANCH1 -p $BRANCH2)
   git update-ref $BRANCH1 $NEWCOMMIT

Note that higher level scripts may also want to access the conflict/warning messages normally output during a merge, or have quick access to a list of files with conflicts. That is not available in this preliminary implementation, but subsequent commits will add that ability (meaning that NEWTREE would be a lot more than a tree in the case of conflicts).

This also marks the traditional trivial merge of merge-tree as deprecated. The trivial merge not only had limited applicability, the output format was also difficult to work with (and its format undocumented), and will generally be less performant than real merges.

---
 Documentation/git-merge-tree.txt |  79 +++++++++++++++++++----
 builtin/merge-tree.c             |  44 ++++++++++++-
 t/t4301-merge-tree-write-tree.sh | 106 +++++++++++++++++++++++++++++++
 3 files changed, 216 insertions(+), 13 deletions(-)
 create mode 100755 t/t4301-merge-tree-write-tree.sh
Documentation/git-merge-tree.txt +67 -12
index 58731c19422..589a83738ce 100644
@@ -3,26 +3,81 @@ git-merge-tree(1)
 
 NAME
 ----
-git-merge-tree - Show three-way merge without touching index
+git-merge-tree - Perform merge without touching index or working tree
 
 
 SYNOPSIS
 --------
 [verse]
-'git merge-tree' <base-tree> <branch1> <branch2>
+'git merge-tree' [--write-tree] <branch1> <branch2>
+'git merge-tree' [--trivial-merge] <base-tree> <branch1> <branch2> (deprecated)
 
 DESCRIPTION
 -----------
-Reads three tree-ish, and output trivial merge results and
-conflicting stages to the standard output.  This is similar to
-what three-way 'git read-tree -m' does, but instead of storing the
-results in the index, the command outputs the entries to the
-standard output.
-
-This is meant to be used by higher level scripts to compute
-merge results outside of the index, and stuff the results back into the
-index.  For this reason, the output from the command omits
-entries that match the <branch1> tree.
+
+Performs a merge, but does not make any new commits and does not read
+from or write to either the working tree or index.
+
+The first form will merge the two branches, doing a real merge.  A real
+merge is distinguished from a trivial merge in that it includes:
+
+  * three way content merges of individual files
+  * rename detection
+  * proper directory/file conflict handling
+  * recursive ancestor consolidation (i.e. when there is more than one
+    merge base, creating a virtual merge base by merging the merge bases)
+  * etc.
+
+After the merge completes, the first form will create a new toplevel
+tree object.  See `OUTPUT` below for details.
+
+The second form is deprecated; it is kept for backward compatibility
+reasons but may be deleted in the future.  Other than the optional
+`--trivial-merge`, it accepts no options.  It can only do a trivial
+merge.  It reads three tree-ish, and outputs trivial merge results and
+conflicting stages to the standard output in a semi-diff format.
+Since this was designed for higher level scripts to consume and merge
+the results back into the index, it omits entries that match
+<branch1>.  The result of this second form is similar to what
+three-way 'git read-tree -m' does, but instead of storing the results
+in the index, the command outputs the entries to the standard output.
+This form not only has limited applicability, the output format is
+also difficult to work with, and it will generally be less performant
+than the first form even on successful merges (especially if working
+in large repositories).  The remainder of this manual will only
+discuss the first form.
+
+OUTPUT
+------
+
+For either a successful or conflicted merge, the output from
+git-merge-tree is simply one line:
+
+	<OID of toplevel tree>
+
+The printed tree object corresponds to what would be checked out in
+the working tree at the end of `git merge`, and thus may have files
+with conflict markers in them.
+
+EXIT STATUS
+-----------
+
+For a successful, non-conflicted merge, the exit status is 0.  When the
+merge has conflicts, the exit status is 1.  If the merge is not able to
+complete (or start) due to some kind of error, the exit status is
+something other than 0 or 1 (and the output is unspecified).
+
+USAGE NOTES
+-----------
+
+git-merge-tree was written to be low-level plumbing, similar to
+hash-object, mktree, commit-tree, write-tree, update-ref, and mktag.
+Thus, it could be used as a part of a series of steps such as
+
+       NEWTREE=$(git merge-tree --write-tree $BRANCH1 $BRANCH2)
+       test $? -eq 0 || die "There were conflicts..."
+       NEWCOMMIT=$(git commit-tree $NEWTREE -p $BRANCH1 -p $BRANCH2)
+       git update-ref $BRANCH1 $NEWCOMMIT
 
 GIT
 ---
builtin/merge-tree.c +43 -1

56 lines not on this page · raw message

t/t4301-merge-tree-write-tree.sh new file +106 -0

106 lines not on this page · raw message

Signature · 1 line
gitgitgadget
#192 lore raw

Re: [PATCH v5 04/12] merge-tree: implement real merges

Am 20.02.22 um 07:54 schrieb Elijah Newren via GitGitGadget:

Quoted 181 lines

From: Elijah Newren <[email protected]>

This adds the ability to perform real merges rather than just trivial merges (meaning handling three way content merges, recursive ancestor consolidation, renames, proper directory/file conflict handling, and so forth). However, unlike git merge, the working tree and index are left alone and no branch is updated.

The only output is:
- the toplevel resulting tree printed on stdout
- exit status of 0 (clean), 1 (conflicts present), anything else (merge could not be performed; unknown if clean or conflicted)

This output is meant to be used by some higher level script, perhaps in a sequence of steps like this:

   NEWTREE=$(git merge-tree --write-tree $BRANCH1 $BRANCH2)
   test $? -eq 0 || die "There were conflicts..."
   NEWCOMMIT=$(git commit-tree $NEWTREE -p $BRANCH1 -p $BRANCH2)
   git update-ref $BRANCH1 $NEWCOMMIT

Note that higher level scripts may also want to access the conflict/warning messages normally output during a merge, or have quick access to a list of files with conflicts. That is not available in this preliminary implementation, but subsequent commits will add that ability (meaning that NEWTREE would be a lot more than a tree in the case of conflicts).

This also marks the traditional trivial merge of merge-tree as deprecated. The trivial merge not only had limited applicability, the output format was also difficult to work with (and its format undocumented), and will generally be less performant than real merges.

---
 Documentation/git-merge-tree.txt |  79 +++++++++++++++++++----
 builtin/merge-tree.c             |  44 ++++++++++++-
 t/t4301-merge-tree-write-tree.sh | 106 +++++++++++++++++++++++++++++++
 3 files changed, 216 insertions(+), 13 deletions(-)
 create mode 100755 t/t4301-merge-tree-write-tree.sh
Documentation/git-merge-tree.txt +67 -12
index 58731c19422..589a83738ce 100644
@@ -3,26 +3,81 @@ git-merge-tree(1)
 
 NAME
 ----
-git-merge-tree - Show three-way merge without touching index
+git-merge-tree - Perform merge without touching index or working tree
 
 
 SYNOPSIS
 --------
 [verse]
-'git merge-tree' <base-tree> <branch1> <branch2>
+'git merge-tree' [--write-tree] <branch1> <branch2>
+'git merge-tree' [--trivial-merge] <base-tree> <branch1> <branch2> (deprecated)
 
 DESCRIPTION
 -----------
-Reads three tree-ish, and output trivial merge results and
-conflicting stages to the standard output.  This is similar to
-what three-way 'git read-tree -m' does, but instead of storing the
-results in the index, the command outputs the entries to the
-standard output.
-
-This is meant to be used by higher level scripts to compute
-merge results outside of the index, and stuff the results back into the
-index.  For this reason, the output from the command omits
-entries that match the <branch1> tree.
+
+Performs a merge, but does not make any new commits and does not read
+from or write to either the working tree or index.
+
+The first form will merge the two branches, doing a real merge.  A real
+merge is distinguished from a trivial merge in that it includes:
+
+  * three way content merges of individual files
+  * rename detection
+  * proper directory/file conflict handling
+  * recursive ancestor consolidation (i.e. when there is more than one
+    merge base, creating a virtual merge base by merging the merge bases)
+  * etc.
+
+After the merge completes, the first form will create a new toplevel
+tree object.  See `OUTPUT` below for details.
+
+The second form is deprecated; it is kept for backward compatibility
+reasons but may be deleted in the future.  Other than the optional
+`--trivial-merge`, it accepts no options.  It can only do a trivial
+merge.  It reads three tree-ish, and outputs trivial merge results and
+conflicting stages to the standard output in a semi-diff format.
+Since this was designed for higher level scripts to consume and merge
+the results back into the index, it omits entries that match
+<branch1>.  The result of this second form is similar to what
+three-way 'git read-tree -m' does, but instead of storing the results
+in the index, the command outputs the entries to the standard output.
+This form not only has limited applicability, the output format is
+also difficult to work with, and it will generally be less performant
+than the first form even on successful merges (especially if working
+in large repositories).  The remainder of this manual will only
+discuss the first form.
+
+OUTPUT
+------
+
+For either a successful or conflicted merge, the output from
+git-merge-tree is simply one line:
+
+	<OID of toplevel tree>
+
+The printed tree object corresponds to what would be checked out in
+the working tree at the end of `git merge`, and thus may have files
+with conflict markers in them.
+
+EXIT STATUS
+-----------
+
+For a successful, non-conflicted merge, the exit status is 0.  When the
+merge has conflicts, the exit status is 1.  If the merge is not able to
+complete (or start) due to some kind of error, the exit status is
+something other than 0 or 1 (and the output is unspecified).
+
+USAGE NOTES
+-----------
+
+git-merge-tree was written to be low-level plumbing, similar to
+hash-object, mktree, commit-tree, write-tree, update-ref, and mktag.
+Thus, it could be used as a part of a series of steps such as
+
+       NEWTREE=$(git merge-tree --write-tree $BRANCH1 $BRANCH2)
+       test $? -eq 0 || die "There were conflicts..."
+       NEWCOMMIT=$(git commit-tree $NEWTREE -p $BRANCH1 -p $BRANCH2)
+       git update-ref $BRANCH1 $NEWCOMMIT
 
 GIT
 ---
builtin/merge-tree.c +26 -1
index 0f9d928e862..af445cb1576 100644
@@ -2,6 +2,9 @@
 #include "builtin.h"
 #include "tree-walk.h"
 #include "xdiff-interface.h"
+#include "help.h"
+#include "commit-reach.h"
+#include "merge-ort.h"
 #include "object-store.h"
 #include "parse-options.h"
 #include "repository.h"
@@ -398,7 +401,46 @@ struct merge_tree_options {
 static int real_merge(struct merge_tree_options *o,
 		      const char *branch1, const char *branch2)
 {
-	die(_("real merges are not yet implemented"));
+	struct commit *parent1, *parent2;
+	struct commit_list *common;
+	struct commit_list *merge_bases = NULL;
+	struct commit_list *j;
+	struct merge_options opt;
+	struct merge_result result = { 0 };
+
+	parent1 = get_merge_parent(branch1);
+	if (!parent1)
+		help_unknown_ref(branch1, "merge-tree",
+				 _("not something we can merge"));
+
+	parent2 = get_merge_parent(branch2);
+	if (!parent2)
+		help_unknown_ref(branch2, "merge-tree",
+				 _("not something we can merge"));
+
+	init_merge_options(&opt, the_repository);
+
+	opt.show_rename_progress = 0;
+
+	opt.branch1 = branch1;
+	opt.branch2 = branch2;

builtin/merge-tree.c struct merge_tree_options { · line 436

Quoted 10 lines
builtin/merge-tree.c +10 -0
+
+	/*
+	 * Get the merge bases, in reverse order; see comment above
+	 * merge_incore_recursive in merge-ort.h
+	 */
+	common = get_merge_bases(parent1, parent2);
+	if (!common)
+		die(_("refusing to merge unrelated histories"));
+	for (j = common; j; j = j->next)
+		commit_list_insert(j->item, &merge_bases);

This loop creates a reversed copy of "common". You could use reverse_commit_list() instead to do it in-place and avoid the allocations. Only the copy, "merge_bases", is used below.

Quoted 122 lines
builtin/merge-tree.c +7 -0
+
+	merge_incore_recursive(&opt, merge_bases, parent1, parent2, &result);
+	if (result.clean < 0)
+		die(_("failure to merge"));
+	puts(oid_to_hex(&result.tree->object.oid));
+	merge_finalize(&opt, &result);
+	return !result.clean; /* result.clean < 0 handled above */
 }
 
 int cmd_merge_tree(int argc, const char **argv, const char *prefix)
t/t4301-merge-tree-write-tree.sh new file +106 -0
new file mode 100755
index 00000000000..6d321652e21
@@ -0,0 +1,106 @@
+#!/bin/sh
+
+test_description='git merge-tree --write-tree'
+
+. ./test-lib.sh
+
+# This test is ort-specific
+if test "$GIT_TEST_MERGE_ALGORITHM" != "ort"
+then
+	skip_all="GIT_TEST_MERGE_ALGORITHM != ort"
+	test_done
+fi
+
+test_expect_success setup '
+	test_write_lines 1 2 3 4 5 >numbers &&
+	echo hello >greeting &&
+	echo foo >whatever &&
+	git add numbers greeting whatever &&
+	test_tick &&
+	git commit -m initial &&
+
+	git branch side1 &&
+	git branch side2 &&
+	git branch side3 &&
+
+	git checkout side1 &&
+	test_write_lines 1 2 3 4 5 6 >numbers &&
+	echo hi >greeting &&
+	echo bar >whatever &&
+	git add numbers greeting whatever &&
+	test_tick &&
+	git commit -m modify-stuff &&
+
+	git checkout side2 &&
+	test_write_lines 0 1 2 3 4 5 >numbers &&
+	echo yo >greeting &&
+	git rm whatever &&
+	mkdir whatever &&
+	>whatever/empty &&
+	git add numbers greeting whatever/empty &&
+	test_tick &&
+	git commit -m other-modifications &&
+
+	git checkout side3 &&
+	git mv numbers sequence &&
+	test_tick &&
+	git commit -m rename-numbers
+'
+
+test_expect_success 'Clean merge' '
+	TREE_OID=$(git merge-tree --write-tree side1 side3) &&
+	q_to_tab <<-EOF >expect &&
+	100644 blob $(git rev-parse side1:greeting)Qgreeting
+	100644 blob $(git rev-parse side1:numbers)Qsequence
+	100644 blob $(git rev-parse side1:whatever)Qwhatever
+	EOF
+
+	git ls-tree $TREE_OID >actual &&
+	test_cmp expect actual
+'
+
+test_expect_success 'Content merge and a few conflicts' '
+	git checkout side1^0 &&
+	test_must_fail git merge side2 &&
+	expected_tree=$(git rev-parse AUTO_MERGE) &&
+
+	# We will redo the merge, while we are still in a conflicted state!
+	test_when_finished "git reset --hard" &&
+
+	test_expect_code 1 git merge-tree --write-tree side1 side2 >RESULT &&
+	actual_tree=$(head -n 1 RESULT) &&
+
+	# Due to differences of e.g. "HEAD" vs "side1", the results will not
+	# exactly match.  Dig into individual files.
+
+	# Numbers should have three-way merged cleanly
+	test_write_lines 0 1 2 3 4 5 6 >expect &&
+	git show ${actual_tree}:numbers >actual &&
+	test_cmp expect actual &&
+
+	# whatever and whatever~<branch> should have same HASHES
+	git rev-parse ${expected_tree}:whatever ${expected_tree}:whatever~HEAD >expect &&
+	git rev-parse ${actual_tree}:whatever ${actual_tree}:whatever~side1 >actual &&
+	test_cmp expect actual &&
+
+	# greeting should have a merge conflict
+	git show ${expected_tree}:greeting >tmp &&
+	sed -e s/HEAD/side1/ tmp >expect &&
+	git show ${actual_tree}:greeting >actual &&
+	test_cmp expect actual
+'
+
+test_expect_success 'Barf on misspelled option, with exit code other than 0 or 1' '
+	# Mis-spell with single "s" instead of double "s"
+	test_expect_code 129 git merge-tree --write-tree --mesages FOOBAR side1 side2 2>expect &&
+
+	grep "error: unknown option.*mesages" expect
+'
+
+test_expect_success 'Barf on too many arguments' '
+	test_expect_code 129 git merge-tree --write-tree side1 side2 invalid 2>expect &&
+
+	grep "^usage: git merge-tree" expect
+'
+
+test_done
#193 lore raw

Re: [PATCH v5 04/12] merge-tree: implement real merges

Hi,

Quoted 15 lines · René Scharfe

Am 20.02.22 um 07:54 schrieb Elijah Newren via GitGitGadget:

builtin/merge-tree.c struct merge_tree_options { · line 436

Quoted 9 lines
builtin/merge-tree.c +9 -0
+	/*
+	 * Get the merge bases, in reverse order; see comment above
+	 * merge_incore_recursive in merge-ort.h
+	 */
+	common = get_merge_bases(parent1, parent2);
+	if (!common)
+		die(_("refusing to merge unrelated histories"));
+	for (j = common; j; j = j->next)
+		commit_list_insert(j->item, &merge_bases);

This loop creates a reversed copy of "common". You could use reverse_commit_list() instead to do it in-place and avoid the allocations. Only the copy, "merge_bases", is used below.

Curious. When I read this first, I immediately assumed this was copy-pasted from merge-recursive.c, but it wasn't
(https://github.com/git/git/blob/v2.35.1/merge-recursive.c#L3591-L3592):

		merge_bases = get_merge_bases(h1, h2);
		merge_bases = reverse_commit_list(merge_bases);

I tried to figure out where the manual reversal might have been copy-pasted from, but came up empty-handed. The comment in
https://github.com/git/git/blob/v2.35.1/merge-ort.h#L40-L48 did not shed any light on it (but took me down memory lane, all the way to 2006!).

Ciao,
Dscho

#194 lore raw

Re: [PATCH v5 04/12] merge-tree: implement real merges

In reply to René Scharfe
Quoted 2 lines · René Scharfe

Am 20.02.22 um 07:54 schrieb Elijah Newren via GitGitGadget:

[...]

Quoted 10 lines

builtin/merge-tree.c struct merge_tree_options { · line 436

Quoted 9 lines
builtin/merge-tree.c +9 -0
+     /*
+      * Get the merge bases, in reverse order; see comment above
+      * merge_incore_recursive in merge-ort.h
+      */
+     common = get_merge_bases(parent1, parent2);
+     if (!common)
+             die(_("refusing to merge unrelated histories"));
+     for (j = common; j; j = j->next)
+             commit_list_insert(j->item, &merge_bases);
Quoted 3 lines

This loop creates a reversed copy of "common". You could use reverse_commit_list() instead to do it in-place and avoid the allocations. Only the copy, "merge_bases", is used below.

Oh, good catch. I probably should have been aware of this since someone requested I move the reverse_commit_list() function from merge-recursive.c to commit.c as part of my merge-ort work, but looks like I forgot about it and copied this command snippet from builtin/merge.c instead. I have no excuse.

However, I wonder if that means we could also apply this simplification to the code snippets in builtin/merge.c and sequencer.c that you can find with
git grep commit_list_insert.*reversed
? Maybe #leftoverbits for that part?

#195 lore raw

Re: [PATCH v5 04/12] merge-tree: implement real merges

Hi Elijah,

Quoted 18 lines · Elijah Newren
Quoted 2 lines · René Scharfe

Am 20.02.22 um 07:54 schrieb Elijah Newren via GitGitGadget:

[...]

Quoted 10 lines

builtin/merge-tree.c struct merge_tree_options { · line 436

Quoted 9 lines
builtin/merge-tree.c +9 -0
+     /*
+      * Get the merge bases, in reverse order; see comment above
+      * merge_incore_recursive in merge-ort.h
+      */
+     common = get_merge_bases(parent1, parent2);
+     if (!common)
+             die(_("refusing to merge unrelated histories"));
+     for (j = common; j; j = j->next)
+             commit_list_insert(j->item, &merge_bases);
Quoted 3 lines

This loop creates a reversed copy of "common". You could use reverse_commit_list() instead to do it in-place and avoid the allocations. Only the copy, "merge_bases", is used below.

Quoted 5 lines

Oh, good catch. I probably should have been aware of this since someone requested I move the reverse_commit_list() function from merge-recursive.c to commit.c as part of my merge-ort work, but looks like I forgot about it and copied this command snippet from builtin/merge.c instead. I have no excuse.

Ooops! I missed that the reverse_commit_list() function was moved to commit.c by _you_, and had not been there all along (my fault, of course, see 8918b0c9c26 (merge-recur: try to merge older merge bases first, 2006-08-09)).

Quoted 5 lines

However, I wonder if that means we could also apply this simplification to the code snippets in builtin/merge.c and sequencer.c that you can find with
git grep commit_list_insert.*reversed
? Maybe #leftoverbits for that part?

Yes, that's a good idea. I summarized this left-over-bit in
https://github.com/gitgitgadget/git/issues/1156

Ciao,
Dscho

#196 lore raw

PATCH v5 5/12 merge-ort: split out a separate display_update_messages() function

In reply to Elijah Newren via GitGitGadget

From: Elijah Newren <[email protected]>

This patch includes no new code; it simply moves a bunch of lines into a new function. As such, there are no functional changes. This is just a preparatory step to allow the printed messages to be handled differently by other callers, such as in git merge-tree --write-tree.

(Patch best viewed with

     --color-moved --color-moved-ws=allow-indentation-change

to see that it is a simple code movement.)

---
 merge-ort.c | 78 ++++++++++++++++++++++++++++-------------------------
 merge-ort.h |  8 ++++++
 2 files changed, 49 insertions(+), 37 deletions(-)
merge-ort.c +41 -37
index 9bf15a01db8..ebaed98d53a 100644
@@ -4235,6 +4235,45 @@ static int record_conflicted_index_entries(struct merge_options *opt)
 	return errs;
 }
 
+void merge_display_update_messages(struct merge_options *opt,
+				   struct merge_result *result)
+{
+	struct merge_options_internal *opti = result->priv;
+	struct hashmap_iter iter;
+	struct strmap_entry *e;
+	struct string_list olist = STRING_LIST_INIT_NODUP;
+	int i;
+
+	if (opt->record_conflict_msgs_as_headers)
+		BUG("Either display conflict messages or record them as headers, not both");
+
+	trace2_region_enter("merge", "display messages", opt->repo);
+
+	/* Hack to pre-allocate olist to the desired size */
+	ALLOC_GROW(olist.items, strmap_get_size(&opti->output),
+		   olist.alloc);
+
+	/* Put every entry from output into olist, then sort */
+	strmap_for_each_entry(&opti->output, &iter, e) {
+		string_list_append(&olist, e->key)->util = e->value;
+	}
+	string_list_sort(&olist);
+
+	/* Iterate over the items, printing them */
+	for (i = 0; i < olist.nr; ++i) {
+		struct strbuf *sb = olist.items[i].util;
+
+		printf("%s", sb->buf);
+	}
+	string_list_clear(&olist, 0);
+
+	/* Also include needed rename limit adjustment now */
+	diff_warn_rename_limit("merge.renamelimit",
+			       opti->renames.needed_limit, 0);
+
+	trace2_region_leave("merge", "display messages", opt->repo);
+}
+
 void merge_switch_to_result(struct merge_options *opt,
 			    struct tree *head,
 			    struct merge_result *result,
@@ -4272,43 +4311,8 @@ void merge_switch_to_result(struct merge_options *opt,
 		fclose(fp);
 		trace2_region_leave("merge", "write_auto_merge", opt->repo);
 	}
-
-	if (display_update_msgs) {
-		struct merge_options_internal *opti = result->priv;
-		struct hashmap_iter iter;
-		struct strmap_entry *e;
-		struct string_list olist = STRING_LIST_INIT_NODUP;
-		int i;
-
-		if (opt->record_conflict_msgs_as_headers)
-			BUG("Either display conflict messages or record them as headers, not both");
-
-		trace2_region_enter("merge", "display messages", opt->repo);
-
-		/* Hack to pre-allocate olist to the desired size */
-		ALLOC_GROW(olist.items, strmap_get_size(&opti->output),
-			   olist.alloc);
-
-		/* Put every entry from output into olist, then sort */
-		strmap_for_each_entry(&opti->output, &iter, e) {
-			string_list_append(&olist, e->key)->util = e->value;
-		}
-		string_list_sort(&olist);
-
-		/* Iterate over the items, printing them */
-		for (i = 0; i < olist.nr; ++i) {
-			struct strbuf *sb = olist.items[i].util;
-
-			printf("%s", sb->buf);
-		}
-		string_list_clear(&olist, 0);
-
-		/* Also include needed rename limit adjustment now */
-		diff_warn_rename_limit("merge.renamelimit",
-				       opti->renames.needed_limit, 0);
-
-		trace2_region_leave("merge", "display messages", opt->repo);
-	}
+	if (display_update_msgs)
+		merge_display_update_messages(opt, result);
 
 	merge_finalize(opt, result);
 }
merge-ort.h +8 -0
index fe599b87868..e5aec45b18f 100644
@@ -80,6 +80,14 @@ void merge_switch_to_result(struct merge_options *opt,
 			    int update_worktree_and_index,
 			    int display_update_msgs);
 
+/*
+ * Display messages about conflicts and which files were 3-way merged.
+ * Automatically called by merge_switch_to_result() with stream == stdout,
+ * so only call this when bypassing merge_switch_to_result().
+ */
+void merge_display_update_messages(struct merge_options *opt,
+				   struct merge_result *result);
+
 /* Do needed cleanup when not calling merge_switch_to_result() */
 void merge_finalize(struct merge_options *opt,
 		    struct merge_result *result);
Signature · 1 line
gitgitgadget
#197 lore raw

PATCH v5 6/12 merge-tree: support including merge messages in output

In reply to Elijah Newren via GitGitGadget

From: Elijah Newren <[email protected]>

When running git merge-tree --write-tree, we previously would only return an exit status reflecting the cleanness of a merge, and print out the toplevel tree of the resulting merge. Merges also have informational messages, such as:
* "Auto-merging <PATH>"
* "CONFLICT (content): ..."
* "CONFLICT (file/directory)"
* etc.
In fact, when non-content conflicts occur (such as file/directory, modify/delete, add/add with differing modes, rename/rename (1to2), etc.), these informational messages may be the only notification the user gets since these conflicts are not representable in the contents of the file.

Add a --[no-]messages option so that callers can request these messages be included at the end of the output. Include such messages by default when there are conflicts, and omit them by default when the merge is clean.

---
 Documentation/git-merge-tree.txt | 45 +++++++++++++++++++++++++++-----
 builtin/merge-tree.c             | 21 +++++++++++++--
 t/t4301-merge-tree-write-tree.sh | 37 ++++++++++++++++++++++++++
 3 files changed, 95 insertions(+), 8 deletions(-)
Documentation/git-merge-tree.txt +39 -6
index 589a83738ce..f415d6de5a3 100644
@@ -9,7 +9,7 @@ git-merge-tree - Perform merge without touching index or working tree
 SYNOPSIS
 --------
 [verse]
-'git merge-tree' [--write-tree] <branch1> <branch2>
+'git merge-tree' [--write-tree] [<options>] <branch1> <branch2>
 'git merge-tree' [--trivial-merge] <base-tree> <branch1> <branch2> (deprecated)
 
 DESCRIPTION
@@ -47,17 +47,47 @@ than the first form even on successful merges (especially if working
 in large repositories).  The remainder of this manual will only
 discuss the first form.
 
+OPTIONS
+-------
+
+--[no-]messages::
+	Write any informational messages such as "Auto-merging <path>"
+	or CONFLICT notices to the end of stdout.  If unspecified, the
+	default is to include these messages if there are merge
+	conflicts, and to omit them otherwise.
+
 OUTPUT
 ------
 
-For either a successful or conflicted merge, the output from
-git-merge-tree is simply one line:
+By default, for a successful merge, the output from git-merge-tree is
+simply one line:
+
+	<OID of toplevel tree>
+
+Whereas for a conflicted merge, the output is by default of the form:
 
 	<OID of toplevel tree>
+	<Informational messages>
+
+These are discussed individually below.
 
-The printed tree object corresponds to what would be checked out in
-the working tree at the end of `git merge`, and thus may have files
-with conflict markers in them.
+OID of toplevel tree
+~~~~~~~~~~~~~~~~~~~~
+
+This is a tree object that represents what would be checked out in the
+working tree at the end of `git merge`.  If there were conflicts, then
+files within this tree may have embedded conflict markers.
+
+Informational messages
+~~~~~~~~~~~~~~~~~~~~~~
+
+This always starts with a blank line to separate it from the previous
+section, and then has free-form messages about the merge, such as:
+
+  * "Auto-merging <file>"
+  * "CONFLICT (rename/delete): <oldfile> renamed...but deleted in..."
+  * "Failed to merge submodule <submodule> (<reason>)"
+  * "Warning: cannot merge binary files: <filename>"
 
 EXIT STATUS
 -----------
@@ -79,6 +109,9 @@ Thus, it could be used as a part of a series of steps such as
        NEWCOMMIT=$(git commit-tree $NEWTREE -p $BRANCH1 -p $BRANCH2)
        git update-ref $BRANCH1 $NEWCOMMIT
 
+Note that when the exit status is non-zero, NEWTREE in this sequence
+will contain a lot more output than just a tree.
+
 GIT
 ---
 Part of the linkgit:git[1] suite
builtin/merge-tree.c +19 -2
index af445cb1576..d44e8d087b1 100644
@@ -396,6 +396,7 @@ enum mode {
 
 struct merge_tree_options {
 	int mode;
+	int show_messages;
 };
 
 static int real_merge(struct merge_tree_options *o,
@@ -438,18 +439,27 @@ static int real_merge(struct merge_tree_options *o,
 	merge_incore_recursive(&opt, merge_bases, parent1, parent2, &result);
 	if (result.clean < 0)
 		die(_("failure to merge"));
+
+	if (o->show_messages == -1)
+		o->show_messages = !result.clean;
+
 	puts(oid_to_hex(&result.tree->object.oid));
+	if (o->show_messages) {
+		printf("\n");
+		merge_display_update_messages(&opt, &result);
+	}
 	merge_finalize(&opt, &result);
 	return !result.clean; /* result.clean < 0 handled above */
 }
 
 int cmd_merge_tree(int argc, const char **argv, const char *prefix)
 {
-	struct merge_tree_options o = { 0 };
+	struct merge_tree_options o = { .show_messages = -1 };
 	int expected_remaining_argc;
+	int original_argc;
 
 	const char * const merge_tree_usage[] = {
-		N_("git merge-tree [--write-tree] <branch1> <branch2>"),
+		N_("git merge-tree [--write-tree] [<options>] <branch1> <branch2>"),
 		N_("git merge-tree [--trivial-merge] <base-tree> <branch1> <branch2>"),
 		NULL
 	};
@@ -459,10 +469,13 @@ int cmd_merge_tree(int argc, const char **argv, const char *prefix)
 			    MODE_REAL),
 		OPT_CMDMODE(0, "trivial-merge", &o.mode,
 			    N_("do a trivial merge only"), MODE_TRIVIAL),
+		OPT_BOOL(0, "messages", &o.show_messages,
+			 N_("also show informational/conflict messages")),
 		OPT_END()
 	};
 
 	/* Parse arguments */
+	original_argc = argc - 1; /* ignoring argv[0] */
 	argc = parse_options(argc, argv, prefix, mt_options,
 			     merge_tree_usage, PARSE_OPT_STOP_AT_NON_OPTION);
 	switch (o.mode) {
@@ -486,8 +499,12 @@ int cmd_merge_tree(int argc, const char **argv, const char *prefix)
 		break;
 	case MODE_TRIVIAL:
 		expected_remaining_argc = 3;
+		/* Removal of `--trivial-merge` is expected */
+		original_argc--;
 		break;
 	}
+	if (o.mode == MODE_TRIVIAL && argc < original_argc)
+		die(_("--trivial-merge is incompatible with all other options"));
 
 	if (argc != expected_remaining_argc)
 		usage_with_options(merge_tree_usage, mt_options);
t/t4301-merge-tree-write-tree.sh +37 -0
index 6d321652e21..719d81e7173 100755
@@ -103,4 +103,41 @@ test_expect_success 'Barf on too many arguments' '
 	grep "^usage: git merge-tree" expect
 '
 
+anonymize_hash() {
+	sed -e "s/[0-9a-f]\{40,\}/HASH/g" "$@"
+}
+
+test_expect_success 'test conflict notices and such' '
+	test_expect_code 1 git merge-tree --write-tree side1 side2 >out &&
+	anonymize_hash out >actual &&
+
+	# Expected results:
+	#   "greeting" should merge with conflicts
+	#   "numbers" should merge cleanly
+	#   "whatever" has *both* a modify/delete and a file/directory conflict
+	cat <<-EOF >expect &&
+	HASH
+
+	Auto-merging greeting
+	CONFLICT (content): Merge conflict in greeting
+	Auto-merging numbers
+	CONFLICT (file/directory): directory in the way of whatever from side1; moving it to whatever~side1 instead.
+	CONFLICT (modify/delete): whatever~side1 deleted in side2 and modified in side1.  Version side1 of whatever~side1 left in tree.
+	EOF
+
+	test_cmp expect actual
+'
+
+for opt in $(git merge-tree --git-completion-helper-all)
+do
+	if test $opt = "--trivial-merge" || test $opt = "--write-tree"
+	then
+		continue
+	fi
+
+	test_expect_success "usage: --trivial-merge is incompatible with $opt" '
+		test_expect_code 128 git merge-tree --trivial-merge $opt side1 side2 side3
+	'
+done
+
 test_done
Signature · 1 line
gitgitgadget
#198 lore raw

PATCH v5 7/12 merge-ort: provide a merge_get_conflicted_files() helper function

In reply to Elijah Newren via GitGitGadget

From: Elijah Newren <[email protected]>

After a merge, this function allows the user to extract the same information that would be printed by ls-files -u, which means files with their mode, oid, and stage.

---
 merge-ort.c | 31 +++++++++++++++++++++++++++++++
 merge-ort.h | 21 +++++++++++++++++++++
 2 files changed, 52 insertions(+)
merge-ort.c +31 -0
index ebaed98d53a..e1b647b0a40 100644
@@ -4274,6 +4274,37 @@ void merge_display_update_messages(struct merge_options *opt,
 	trace2_region_leave("merge", "display messages", opt->repo);
 }
 
+void merge_get_conflicted_files(struct merge_result *result,
+				struct string_list *conflicted_files)
+{
+	struct hashmap_iter iter;
+	struct strmap_entry *e;
+	struct merge_options_internal *opti = result->priv;
+
+	strmap_for_each_entry(&opti->conflicted, &iter, e) {
+		const char *path = e->key;
+		struct conflict_info *ci = e->value;
+		int i;
+
+		VERIFY_CI(ci);
+
+		for (i = MERGE_BASE; i <= MERGE_SIDE2; i++) {
+			struct stage_info *si;
+
+			if (!(ci->filemask & (1ul << i)))
+				continue;
+
+			si = xmalloc(sizeof(*si));
+			si->stage = i+1;
+			si->mode = ci->stages[i].mode;
+			oidcpy(&si->oid, &ci->stages[i].oid);
+			string_list_append(conflicted_files, path)->util = si;
+		}
+	}
+	/* string_list_sort() uses a stable sort, so we're good */
+	string_list_sort(conflicted_files);
+}
+
 void merge_switch_to_result(struct merge_options *opt,
 			    struct tree *head,
 			    struct merge_result *result,
merge-ort.h +21 -0
index e5aec45b18f..ddcc39d7270 100644
@@ -2,6 +2,7 @@
 #define MERGE_ORT_H
 
 #include "merge-recursive.h"
+#include "hash.h"
 
 struct commit;
 struct tree;
@@ -88,6 +89,26 @@ void merge_switch_to_result(struct merge_options *opt,
 void merge_display_update_messages(struct merge_options *opt,
 				   struct merge_result *result);
 
+struct stage_info {
+	struct object_id oid;
+	int mode;
+	int stage;
+};
+
+/*
+ * Provide a list of path -> {struct stage_info*} mappings for
+ * all conflicted files.  Note that each path could appear up to three
+ * times in the list, corresponding to 3 different stage entries.  In short,
+ * this basically provides the info that would be printed by `ls-files -u`.
+ *
+ * result should have been populated by a call to
+ * one of the merge_incore_[non]recursive() functions.
+ *
+ * conflicted_files should be empty before calling this function.
+ */
+void merge_get_conflicted_files(struct merge_result *result,
+				struct string_list *conflicted_files);
+
 /* Do needed cleanup when not calling merge_switch_to_result() */
 void merge_finalize(struct merge_options *opt,
 		    struct merge_result *result);
Signature · 1 line
gitgitgadget
#199 lore raw

PATCH v5 8/12 merge-tree: provide a list of which files have conflicts

In reply to Elijah Newren via GitGitGadget

From: Elijah Newren <[email protected]>

Callers of git merge-tree --write-tree will often want to know which files had conflicts. While they could potentially attempt to parse the CONFLICT notices printed, those messages are not meant to be machine readable. Provide a simpler mechanism of just printing the files (in the same format as git ls-files with quoting, but restricted to unmerged files) in the output before the free-form messages.

---
 Documentation/git-merge-tree.txt |  8 ++++++++
 builtin/merge-tree.c             | 24 ++++++++++++++++++++++--
 t/t4301-merge-tree-write-tree.sh | 11 +++++++++++
 3 files changed, 41 insertions(+), 2 deletions(-)
Documentation/git-merge-tree.txt +8 -0
index f415d6de5a3..deaeb49ae05 100644
@@ -67,6 +67,7 @@ simply one line:
 Whereas for a conflicted merge, the output is by default of the form:
 
 	<OID of toplevel tree>
+	<Conflicted file list>
 	<Informational messages>
 
 These are discussed individually below.
@@ -78,6 +79,13 @@ This is a tree object that represents what would be checked out in the
 working tree at the end of `git merge`.  If there were conflicts, then
 files within this tree may have embedded conflict markers.
 
+Conflicted file list
+~~~~~~~~~~~~~~~~~~~~
+
+This is a sequence of lines containing a filename on each line, quoted
+as explained for the configuration variable `core.quotePath` (see
+linkgit:git-config[1]).
+
 Informational messages
 ~~~~~~~~~~~~~~~~~~~~~~
 
builtin/merge-tree.c +22 -2
index d44e8d087b1..cb4169d2271 100644
@@ -11,6 +11,9 @@
 #include "blob.h"
 #include "exec-cmd.h"
 #include "merge-blobs.h"
+#include "quote.h"
+
+static int line_termination = '\n';
 
 struct merge_list {
 	struct merge_list *next;
@@ -400,7 +403,8 @@ struct merge_tree_options {
 };
 
 static int real_merge(struct merge_tree_options *o,
-		      const char *branch1, const char *branch2)
+		      const char *branch1, const char *branch2,
+		      const char *prefix)
 {
 	struct commit *parent1, *parent2;
 	struct commit_list *common;
@@ -444,6 +448,22 @@ static int real_merge(struct merge_tree_options *o,
 		o->show_messages = !result.clean;
 
 	puts(oid_to_hex(&result.tree->object.oid));
+	if (!result.clean) {
+		struct string_list conflicted_files = STRING_LIST_INIT_NODUP;
+		const char *last = NULL;
+		int i;
+
+		merge_get_conflicted_files(&result, &conflicted_files);
+		for (i = 0; i < conflicted_files.nr; i++) {
+			const char *name = conflicted_files.items[i].string;
+			if (last && !strcmp(last, name))
+				continue;
+			write_name_quoted_relative(
+				name, prefix, stdout, line_termination);
+			last = name;
+		}
+		string_list_clear(&conflicted_files, 1);
+	}
 	if (o->show_messages) {
 		printf("\n");
 		merge_display_update_messages(&opt, &result);
@@ -511,7 +531,7 @@ int cmd_merge_tree(int argc, const char **argv, const char *prefix)
 
 	/* Do the relevant type of merge */
 	if (o.mode == MODE_REAL)
-		return real_merge(&o, argv[0], argv[1]);
+		return real_merge(&o, argv[0], argv[1], prefix);
 	else
 		return trivial_merge(argv[0], argv[1], argv[2]);
 }
t/t4301-merge-tree-write-tree.sh +11 -0
index 719d81e7173..8e6dba44288 100755
@@ -117,6 +117,8 @@ test_expect_success 'test conflict notices and such' '
 	#   "whatever" has *both* a modify/delete and a file/directory conflict
 	cat <<-EOF >expect &&
 	HASH
+	greeting
+	whatever~side1
 
 	Auto-merging greeting
 	CONFLICT (content): Merge conflict in greeting
@@ -140,4 +142,13 @@ do
 	'
 done
 
+test_expect_success 'Just the conflicted files without the messages' '
+	test_expect_code 1 git merge-tree --write-tree --no-messages side1 side2 >out &&
+	anonymize_hash out >actual &&
+
+	test_write_lines HASH greeting whatever~side1 >expect &&
+
+	test_cmp expect actual
+'
+
 test_done
Signature · 1 line
gitgitgadget
#200 lore raw

PATCH v5 9/12 merge-tree: provide easy access to `ls-files -u` style info

In reply to Elijah Newren via GitGitGadget

From: Elijah Newren <[email protected]>

Much like git merge updates the index with information of the form
(mode, oid, stage, name)
provide this output for conflicted files for merge-tree as well. Provide a --name-only option for users to exclude the mode, oid, and stage and only get the list of conflicted filenames.

---
 Documentation/git-merge-tree.txt | 32 ++++++++++++++++++++++++++------
 builtin/merge-tree.c             | 11 ++++++++++-
 t/t4301-merge-tree-write-tree.sh | 26 ++++++++++++++++++++++++--
 3 files changed, 60 insertions(+), 9 deletions(-)
Documentation/git-merge-tree.txt +26 -6
index deaeb49ae05..67a135e8f5d 100644
@@ -50,6 +50,13 @@ discuss the first form.
 OPTIONS
 -------
 
+--name-only::
+	In the Conflicted file info section, instead of writing a list
+	of (mode, oid, stage, path) tuples to output for conflicted
+	files, just provide a list of filenames with conflicts (and
+	do not list filenames multiple times if they have multiple
+	conflicting stages).
+
 --[no-]messages::
 	Write any informational messages such as "Auto-merging <path>"
 	or CONFLICT notices to the end of stdout.  If unspecified, the
@@ -67,7 +74,7 @@ simply one line:
 Whereas for a conflicted merge, the output is by default of the form:
 
 	<OID of toplevel tree>
-	<Conflicted file list>
+	<Conflicted file info>
 	<Informational messages>
 
 These are discussed individually below.
@@ -79,18 +86,23 @@ This is a tree object that represents what would be checked out in the
 working tree at the end of `git merge`.  If there were conflicts, then
 files within this tree may have embedded conflict markers.
 
-Conflicted file list
+Conflicted file info
 ~~~~~~~~~~~~~~~~~~~~
 
-This is a sequence of lines containing a filename on each line, quoted
-as explained for the configuration variable `core.quotePath` (see
-linkgit:git-config[1]).
+This is a sequence of lines with the format
+
+	<mode> <object> <stage> <filename>
+
+The filename will be quoted as explained for the configuration
+variable `core.quotePath` (see linkgit:git-config[1]).  However, if
+the `--name-only` option is passed, the mode, object, and stage will
+be omitted.
 
 Informational messages
 ~~~~~~~~~~~~~~~~~~~~~~
 
 This always starts with a blank line to separate it from the previous
-section, and then has free-form messages about the merge, such as:
+sections, and then has free-form messages about the merge, such as:
 
   * "Auto-merging <file>"
   * "CONFLICT (rename/delete): <oldfile> renamed...but deleted in..."
@@ -120,6 +132,14 @@ Thus, it could be used as a part of a series of steps such as
 Note that when the exit status is non-zero, NEWTREE in this sequence
 will contain a lot more output than just a tree.
 
+git-merge-tree was written to provide users with the same information
+that they'd have access to if using `git merge`:
+  * what would be written to the working tree (the <OID of toplevel tree>)
+  * the higher order stages that would be written to the index (the
+    <Conflicted file info>)
+  * any messages that would have been printed to stdout (the <Informational
+    messages>)
+
 GIT
 ---
 Part of the linkgit:git[1] suite
builtin/merge-tree.c +10 -1
index cb4169d2271..1d4d6637b90 100644
@@ -400,6 +400,7 @@ enum mode {
 struct merge_tree_options {
 	int mode;
 	int show_messages;
+	int name_only;
 };
 
 static int real_merge(struct merge_tree_options *o,
@@ -456,7 +457,11 @@ static int real_merge(struct merge_tree_options *o,
 		merge_get_conflicted_files(&result, &conflicted_files);
 		for (i = 0; i < conflicted_files.nr; i++) {
 			const char *name = conflicted_files.items[i].string;
-			if (last && !strcmp(last, name))
+			struct stage_info *c = conflicted_files.items[i].util;
+			if (!o->name_only)
+				printf("%06o %s %d\t",
+				       c->mode, oid_to_hex(&c->oid), c->stage);
+			else if (last && !strcmp(last, name))
 				continue;
 			write_name_quoted_relative(
 				name, prefix, stdout, line_termination);
@@ -491,6 +496,10 @@ int cmd_merge_tree(int argc, const char **argv, const char *prefix)
 			    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, "name-only",
+			   &o.name_only,
+			   N_("list filenames without modes/oids/stages"),
+			   PARSE_OPT_NONEG),
 		OPT_END()
 	};
 
t/t4301-merge-tree-write-tree.sh +24 -2
index 8e6dba44288..0ec5f0d3f7e 100755
@@ -65,6 +65,7 @@ test_expect_success 'Content merge and a few conflicts' '
 	expected_tree=$(git rev-parse AUTO_MERGE) &&
 
 	# We will redo the merge, while we are still in a conflicted state!
+	git ls-files -u >conflicted-file-info &&
 	test_when_finished "git reset --hard" &&
 
 	test_expect_code 1 git merge-tree --write-tree side1 side2 >RESULT &&
@@ -108,7 +109,7 @@ anonymize_hash() {
 }
 
 test_expect_success 'test conflict notices and such' '
-	test_expect_code 1 git merge-tree --write-tree side1 side2 >out &&
+	test_expect_code 1 git merge-tree --write-tree --name-only side1 side2 >out &&
 	anonymize_hash out >actual &&
 
 	# Expected results:
@@ -143,7 +144,7 @@ do
 done
 
 test_expect_success 'Just the conflicted files without the messages' '
-	test_expect_code 1 git merge-tree --write-tree --no-messages side1 side2 >out &&
+	test_expect_code 1 git merge-tree --write-tree --no-messages --name-only side1 side2 >out &&
 	anonymize_hash out >actual &&
 
 	test_write_lines HASH greeting whatever~side1 >expect &&
@@ -151,4 +152,25 @@ test_expect_success 'Just the conflicted files without the messages' '
 	test_cmp expect actual
 '
 
+test_expect_success 'Check conflicted oids and modes without messages' '
+	test_expect_code 1 git merge-tree --write-tree --no-messages side1 side2 >out &&
+	anonymize_hash out >actual &&
+
+	# Compare the basic output format
+	q_to_tab >expect <<-\EOF &&
+	HASH
+	100644 HASH 1Qgreeting
+	100644 HASH 2Qgreeting
+	100644 HASH 3Qgreeting
+	100644 HASH 1Qwhatever~side1
+	100644 HASH 2Qwhatever~side1
+	EOF
+
+	test_cmp expect actual &&
+
+	# Check the actual hashes against the `ls-files -u` output too
+	tail -n +2 out | sed -e s/side1/HEAD/ >actual &&
+	test_cmp conflicted-file-info actual
+'
+
 test_done
Signature · 1 line
gitgitgadget
#201 lore raw

PATCH v5 10/12 merge-tree: allow `ls-files -u` style info to be NUL terminated

In reply to Elijah Newren via GitGitGadget

From: Elijah Newren <[email protected]>

Much as git ls-files has a -z option, let's add one to merge-tree so that the conflict-info section can be NUL terminated (and avoid quoting of unusual filenames).

---
 Documentation/git-merge-tree.txt | 21 +++++++++++++----
 builtin/merge-tree.c             |  6 +++--
 t/t4301-merge-tree-write-tree.sh | 40 ++++++++++++++++++++++++++++++++
 3 files changed, 61 insertions(+), 6 deletions(-)
Documentation/git-merge-tree.txt +17 -4
index 67a135e8f5d..44024b11b1c 100644
@@ -50,6 +50,12 @@ discuss the first form.
 OPTIONS
 -------
 
+-z::
+	Do not quote filenames in the <Conflicted file info> section,
+	and end each filename with a NUL character rather than
+	newline.  Also begin the messages section with a NUL character
+	instead of a newline.  See OUTPUT below for more information.
+
 --name-only::
 	In the Conflicted file info section, instead of writing a list
 	of (mode, oid, stage, path) tuples to output for conflicted
@@ -84,7 +90,8 @@ OID of toplevel tree
 
 This is a tree object that represents what would be checked out in the
 working tree at the end of `git merge`.  If there were conflicts, then
-files within this tree may have embedded conflict markers.
+files within this tree may have embedded conflict markers.  This section
+is always followed by a newline (or NUL if `-z` is passed).
 
 Conflicted file info
 ~~~~~~~~~~~~~~~~~~~~
@@ -96,19 +103,25 @@ This is a sequence of lines with the format
 The filename will be quoted as explained for the configuration
 variable `core.quotePath` (see linkgit:git-config[1]).  However, if
 the `--name-only` option is passed, the mode, object, and stage will
-be omitted.
+be omitted.  If `-z` is passed, the "lines" are terminated by a NUL
+character instead of a newline character.
 
 Informational messages
 ~~~~~~~~~~~~~~~~~~~~~~
 
-This always starts with a blank line to separate it from the previous
-sections, and then has free-form messages about the merge, such as:
+This always starts with a blank line (or NUL if `-z` is passed) to
+separate it from the previous sections, and then has free-form
+messages about the merge, such as:
 
   * "Auto-merging <file>"
   * "CONFLICT (rename/delete): <oldfile> renamed...but deleted in..."
   * "Failed to merge submodule <submodule> (<reason>)"
   * "Warning: cannot merge binary files: <filename>"
 
+Note that these free-form messages will never have a NUL character
+in or between them, even if -z is passed.  It is simply a large block
+of text taking up the remainder of the output.
+
 EXIT STATUS
 -----------
 
builtin/merge-tree.c +4 -2
index 1d4d6637b90..825255667b1 100644
@@ -448,7 +448,7 @@ static int real_merge(struct merge_tree_options *o,
 	if (o->show_messages == -1)
 		o->show_messages = !result.clean;
 
-	puts(oid_to_hex(&result.tree->object.oid));
+	printf("%s%c", oid_to_hex(&result.tree->object.oid), line_termination);
 	if (!result.clean) {
 		struct string_list conflicted_files = STRING_LIST_INIT_NODUP;
 		const char *last = NULL;
@@ -470,7 +470,7 @@ static int real_merge(struct merge_tree_options *o,
 		string_list_clear(&conflicted_files, 1);
 	}
 	if (o->show_messages) {
-		printf("\n");
+		putchar(line_termination);
 		merge_display_update_messages(&opt, &result);
 	}
 	merge_finalize(&opt, &result);
@@ -496,6 +496,8 @@ int cmd_merge_tree(int argc, const char **argv, const char *prefix)
 			    N_("do a trivial merge only"), MODE_TRIVIAL),
 		OPT_BOOL(0, "messages", &o.show_messages,
 			 N_("also show informational/conflict messages")),
+		OPT_SET_INT('z', NULL, &line_termination,
+			    N_("separate paths with the NUL character"), '\0'),
 		OPT_BOOL_F(0, "name-only",
 			   &o.name_only,
 			   N_("list filenames without modes/oids/stages"),
t/t4301-merge-tree-write-tree.sh +40 -0
index 0ec5f0d3f7e..22e03f0939c 100755
@@ -173,4 +173,44 @@ test_expect_success 'Check conflicted oids and modes without messages' '
 	test_cmp conflicted-file-info actual
 '
 
+test_expect_success 'NUL terminated conflicted file "lines"' '
+	git checkout -b tweak1 side1 &&
+	test_write_lines zero 1 2 3 4 5 6 >numbers &&
+	git add numbers &&
+	git mv numbers "Αυτά μου φαίνονται κινέζικα" &&
+	git commit -m "Renamed numbers" &&
+
+	test_expect_code 1 git merge-tree --write-tree -z tweak1 side2 >out &&
+	anonymize_hash out >actual &&
+
+	# Expected results:
+	#   "greeting" should merge with conflicts
+	#   "whatever" has *both* a modify/delete and a file/directory conflict
+	#   "Αυτά μου φαίνονται κινέζικα" should have a conflict
+	echo HASH | lf_to_nul >expect &&
+
+	q_to_tab <<-EOF | lf_to_nul >>expect &&
+	100644 HASH 1Qgreeting
+	100644 HASH 2Qgreeting
+	100644 HASH 3Qgreeting
+	100644 HASH 1Qwhatever~tweak1
+	100644 HASH 2Qwhatever~tweak1
+	100644 HASH 1QΑυτά μου φαίνονται κινέζικα
+	100644 HASH 2QΑυτά μου φαίνονται κινέζικα
+	100644 HASH 3QΑυτά μου φαίνονται κινέζικα
+
+	EOF
+
+	cat <<-EOF >>expect &&
+	Auto-merging greeting
+	CONFLICT (content): Merge conflict in greeting
+	CONFLICT (file/directory): directory in the way of whatever from tweak1; moving it to whatever~tweak1 instead.
+	CONFLICT (modify/delete): whatever~tweak1 deleted in side2 and modified in tweak1.  Version tweak1 of whatever~tweak1 left in tree.
+	Auto-merging Αυτά μου φαίνονται κινέζικα
+	CONFLICT (content): Merge conflict in Αυτά μου φαίνονται κινέζικα
+	EOF
+
+	test_cmp expect actual
+'
+
 test_done
Signature · 1 line
gitgitgadget
#202 lore raw

PATCH v5 11/12 merge-tree: add a --allow-unrelated-histories flag

In reply to Elijah Newren via GitGitGadget

From: Elijah Newren <[email protected]>

Folks may want to merge histories that have no common ancestry; provide a flag with the same name as used by git merge to allow this.

---
 Documentation/git-merge-tree.txt |  5 +++++
 builtin/merge-tree.c             |  7 ++++++-
 t/t4301-merge-tree-write-tree.sh | 24 +++++++++++++++++++++++-
 3 files changed, 34 insertions(+), 2 deletions(-)
Documentation/git-merge-tree.txt +5 -0
index 44024b11b1c..d2ff2fa3035 100644
@@ -69,6 +69,11 @@ OPTIONS
 	default is to include these messages if there are merge
 	conflicts, and to omit them otherwise.
 
+--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
+	check and make the merge proceed anyway.
+
 OUTPUT
 ------
 
builtin/merge-tree.c +6 -1
index 825255667b1..911504ad694 100644
@@ -399,6 +399,7 @@ enum mode {
 
 struct merge_tree_options {
 	int mode;
+	int allow_unrelated_histories;
 	int show_messages;
 	int name_only;
 };
@@ -436,7 +437,7 @@ static int real_merge(struct merge_tree_options *o,
 	 * merge_incore_recursive in merge-ort.h
 	 */
 	common = get_merge_bases(parent1, parent2);
-	if (!common)
+	if (!common && !o->allow_unrelated_histories)
 		die(_("refusing to merge unrelated histories"));
 	for (j = common; j; j = j->next)
 		commit_list_insert(j->item, &merge_bases);
@@ -502,6 +503,10 @@ int cmd_merge_tree(int argc, const char **argv, const char *prefix)
 			   &o.name_only,
 			   N_("list filenames without modes/oids/stages"),
 			   PARSE_OPT_NONEG),
+		OPT_BOOL_F(0, "allow-unrelated-histories",
+			   &o.allow_unrelated_histories,
+			   N_("allow merging unrelated histories"),
+			   PARSE_OPT_NONEG),
 		OPT_END()
 	};
 
t/t4301-merge-tree-write-tree.sh +23 -1
index 22e03f0939c..bd1769c624b 100755
@@ -44,7 +44,13 @@ test_expect_success setup '
 	git checkout side3 &&
 	git mv numbers sequence &&
 	test_tick &&
-	git commit -m rename-numbers
+	git commit -m rename-numbers &&
+
+	git switch --orphan unrelated &&
+	>something-else &&
+	git add something-else &&
+	test_tick &&
+	git commit -m first-commit
 '
 
 test_expect_success 'Clean merge' '
@@ -213,4 +219,20 @@ test_expect_success 'NUL terminated conflicted file "lines"' '
 	test_cmp expect actual
 '
 
+test_expect_success 'error out by default for unrelated histories' '
+	test_expect_code 128 git merge-tree --write-tree side1 unrelated 2>error &&
+
+	grep "refusing to merge unrelated histories" error
+'
+
+test_expect_success 'can override merge of unrelated histories' '
+	git merge-tree --write-tree --allow-unrelated-histories side1 unrelated >tree &&
+	TREE=$(cat tree) &&
+
+	git rev-parse side1:numbers side1:greeting side1:whatever unrelated:something-else >expect &&
+	git rev-parse $TREE:numbers $TREE:greeting $TREE:whatever $TREE:something-else >actual &&
+
+	test_cmp expect actual
+'
+
 test_done
Signature · 1 line
gitgitgadget
#203 lore raw

PATCH v5 12/12 git-merge-tree.txt: add a section on potentional usage mistakes

In reply to Elijah Newren via GitGitGadget

From: Elijah Newren <[email protected]>

---
 Documentation/git-merge-tree.txt | 46 ++++++++++++++++++++++++++++++++
 1 file changed, 46 insertions(+)
Documentation/git-merge-tree.txt +46 -0
index d2ff2fa3035..306149fa0e2 100644
@@ -158,6 +158,52 @@ that they'd have access to if using `git merge`:
   * any messages that would have been printed to stdout (the <Informational
     messages>)
 
+MISTAKES TO AVOID
+-----------------
+
+Do NOT look through the resulting toplevel tree to try to find which
+files conflict; parse the <Conflicted file info> section instead.  Not
+only would parsing an entire tree be horrendously slow in large
+repositories, there are numerous types of conflicts not representable by
+conflict markers (modify/delete, mode conflict, binary file changed on
+both sides, file/directory conflicts, various rename conflict
+permutations, etc.)
+
+Do NOT interpret an empty <Conflicted file info> list as a clean merge;
+check the exit status.  A merge can have conflicts without having
+individual files conflict (there are a few types of directory rename
+conflicts that fall into this category, and others might also be added
+in the future).
+
+Do NOT attempt to guess or make the user guess the conflict types from
+the <Conflicted file info> list.  The information there is insufficient
+to do so.  For example: Rename/rename(1to2) conflicts (both sides
+renamed the same file differently) will result in three different file
+having higher order stages (but each only has one higher order stage),
+with no way (short of the <Informational messages> section) to determine
+which three files are related.  File/directory conflicts also result in
+a file with exactly one higher order stage.
+Possibly-involved-in-directory-rename conflicts (when
+"merge.directoryRenames" is unset or set to "conflicts") also result in
+a file with exactly one higher order stage.  In all cases, the
+<Informational messages> section has the necessary info, though it is
+not designed to be machine parseable.
+
+Do NOT assume all filenames listed in the <Informational messages>
+section had conflicts.  Messages can be included for files that have no
+conflicts, such as "Auto-merging <file>".
+
+AVOID taking the OIDS from the <Conflicted file info> and re-merging
+them to present the conflicts to the user.  This will lose information.
+Instead, look up the version of the file found within the <OID of
+toplevel tree> and show that instead.  In particular, the latter will
+have conflict markers annotated with the original branch/commit being
+merged and, if renames were involved, the original filename.  While you
+could include the original branch/commit in the conflict marker
+annotations when re-merging, the original filename is not available from
+the <Conflicted file info> and thus you would be losing information that
+might help the user resolve the conflict.
+
 GIT
 ---
 Part of the linkgit:git[1] suite
Signature · 1 line
gitgitgadget
#204 lore raw

Re: [PATCH v5 12/12] git-merge-tree.txt: add a section on potentional usage mistakes

Hi Elijah,

Quoted 6 lines · Elijah Newren via GitGitGadget

From: Elijah Newren <[email protected]>

---
 Documentation/git-merge-tree.txt | 46 ++++++++++++++++++++++++++++++++
 1 file changed, 46 insertions(+)

Thank you for this. It addresses the concern I had about printing out the tree object ID in the conflicted case.

Thanks,
Dscho

Quoted 61 lines
Documentation/git-merge-tree.txt +46 -1
index d2ff2fa3035..306149fa0e2 100644
@@ -158,6 +158,52 @@ that they'd have access to if using `git merge`:
   * any messages that would have been printed to stdout (the <Informational
     messages>)
 
+MISTAKES TO AVOID
+-----------------
+
+Do NOT look through the resulting toplevel tree to try to find which
+files conflict; parse the <Conflicted file info> section instead.  Not
+only would parsing an entire tree be horrendously slow in large
+repositories, there are numerous types of conflicts not representable by
+conflict markers (modify/delete, mode conflict, binary file changed on
+both sides, file/directory conflicts, various rename conflict
+permutations, etc.)
+
+Do NOT interpret an empty <Conflicted file info> list as a clean merge;
+check the exit status.  A merge can have conflicts without having
+individual files conflict (there are a few types of directory rename
+conflicts that fall into this category, and others might also be added
+in the future).
+
+Do NOT attempt to guess or make the user guess the conflict types from
+the <Conflicted file info> list.  The information there is insufficient
+to do so.  For example: Rename/rename(1to2) conflicts (both sides
+renamed the same file differently) will result in three different file
+having higher order stages (but each only has one higher order stage),
+with no way (short of the <Informational messages> section) to determine
+which three files are related.  File/directory conflicts also result in
+a file with exactly one higher order stage.
+Possibly-involved-in-directory-rename conflicts (when
+"merge.directoryRenames" is unset or set to "conflicts") also result in
+a file with exactly one higher order stage.  In all cases, the
+<Informational messages> section has the necessary info, though it is
+not designed to be machine parseable.
+
+Do NOT assume all filenames listed in the <Informational messages>
+section had conflicts.  Messages can be included for files that have no
+conflicts, such as "Auto-merging <file>".
+
+AVOID taking the OIDS from the <Conflicted file info> and re-merging
+them to present the conflicts to the user.  This will lose information.
+Instead, look up the version of the file found within the <OID of
+toplevel tree> and show that instead.  In particular, the latter will
+have conflict markers annotated with the original branch/commit being
+merged and, if renames were involved, the original filename.  While you
+could include the original branch/commit in the conflict marker
+annotations when re-merging, the original filename is not available from
+the <Conflicted file info> and thus you would be losing information that
+might help the user resolve the conflict.
+
 GIT
 ---
 Part of the linkgit:git[1] suite
--

gitgitgadget

#205 lore raw

Re: [PATCH v5 00/12] In-core git merge-tree ("Server side merges")

In reply to Elijah Newren via GitGitGadget
Quoted 33 lines · Elijah Newren via GitGitGadget

== Updates Log ==

Many thanks to the many reviewers who provided good feedback on the most recent round -- Junio, Ævar, Josh, Emily, and perhaps some others I've forgotten from review club.

Updates since v4:

* Fixed double "is" in documentation.
* Fixed a few small items with testcases

Updates since v3 (or v5, if you include the rounds at
https://lore.kernel.org/git/[email protected]/):

* Dropped previous patches 5, 6, and 8 of the old series; they weren't being used and opened a can of worms[1]
* [Patch 3] Restructured argument checking, including using an enum
* [Patch 4] Restored the extended paragraph about the deprecated form of git-merge-tree, mentioned write-tree in plumbing commands, and a few other small fixups to the documentation
* [Patch 4] Also provide an example of a clean merge rather than just a conflicted one
* [Patch 6] Fix the incompatible arguments check and add some tests for it
* [Patch 6] Introduce an anonymize_hash() shell function to make tests easier to read (less repeated sed)
* [Patch 9] Rename --exclude-modes-oids-stages to --name-only; no short option for now
* [Patch 10] When -z passed, the tree in the first section should have a trailing NUL rather than trailing newline [1]
https://lore.kernel.org/git/CABPp-BEKuXHELVx4=5JJTj5HVOKZ=Y-4G4BK47BCZYYRSrkFsQ@mail.gmail.com/

Stuff NOT included that reviewers brought up in earlier rounds:

Quoted 14 lines

* Very generic (mode, oid, stage, filename) printing formatting[2]
* Always printing 3 stages for each filename with conflicts[3]
* Attempting to group conflict stages by logical conflict rather than by affected target filepath[4]
* Providing similar functionality for doing cherry-picks/rebases/reverts, i.e. a scheme for three-way merges with a specified merge-base[5]. That's being deferred to a future series. [2]
https://lore.kernel.org/git/CABPp-BGnOes7J_piDyBUeuLVm274w4-9G3k0vR-0it3z7TPn_w@mail.gmail.com/
[3]
https://lore.kernel.org/git/CABPp-BG2rMEYBLuBW=0wtpJe4aUFGCFa8D0NTSKz9Sm+CkXPxw@mail.gmail.com/
[4]
https://lore.kernel.org/git/CABPp-BGCL0onSmpgKuO1k2spYCkx=v27ed9TSSxFib=OdDcLbw@mail.gmail.com/
[5]
https://lore.kernel.org/git/CABPp-BEaemkGGm0cSofP0gau7YN-y6HFoi0yJbHA8+iGjxsYSA@mail.gmail.com/

I've looked through this, I think it all looks good overall & that the things that needed to be addressed (as opposed to my --format rambling) have been.

I think all the code should be ready for "next".

I suggested (I think around getopts discussion) in an earlier that the code would have been easier with a new built-in, but if we're deprecating the existing "mode" I think using the name is probably better in the end.

I find the resulting documentation to be really hard to grok though because we're effectively describing two different commands. The current docs are small: https://git-scm.com/docs/git-merge-tree

I built the tip of this series and read the manpage, and found myself needing to carefully squint to see what referred to what mode in the docs.

E.g. by the time it's discussing "-z" and other options the reader needs to be astute really aware of the context, and infer from the lack of "<options>" on the "--trivial-merge" that these options refer to the "--write-tree" only.

The same goes for the rest of "--trivial-merge". I.e. I found myself needing to read the whole docs word-by-word (no skimming!) to see if OUTPUT etc. was going to describe its output, or just the "new" mode.

It is my NSHO that man pages should be structured for the impatient reader :)

Then when you say "git-merge-tree was written to be[...]" I thought "ah ha! surely this will discuss the since-2005 implemented mode", but "was written to be" is referring to code new in this series.

The below patch-on-top addresses all those concerns. Basically I just added a line to the top of the DESCRIPTION saying that you should read a "DEPRECATED DESCRIPTION" section at the end for "--trivial-merge", and that all of the rest is talking about the "--write-tree" mode.

I then edited various prose to do away with the now-unnecessary "the first form" etc.

This diff is better against "master" in that you'll see that the current merge-tree DESCRIPTION section isn't touched at all (it's now just under a new heading), but this diff is against the tip of your series.

There's various other small fixes while I was it it here, e.g. all your cross-section links were using some pseudo-not-quite-ASCIIDOC syntax that doesn't work. Now it uses the right syntax. Ditto link-ifying references to "mktag" etc.

I don't know if you'd consider this for a v6, or if I should just submit this on top myself, but in any case here it is. I'll leave it to you how you'd like to proceed with it:

Documentation/git-merge-tree.txt +64 -47
index 306149fa0e2..723b1995426 100644
@@ -9,17 +9,24 @@ git-merge-tree - Perform merge without touching index or working tree
 SYNOPSIS
 --------
 [verse]
-'git merge-tree' [--write-tree] [<options>] <branch1> <branch2>
-'git merge-tree' [--trivial-merge] <base-tree> <branch1> <branch2> (deprecated)
+'git merge-tree' --write-tree [<options>] <branch1> <branch2>
+'git merge-tree' --trivial-merge <base-tree> <branch1> <branch2>
 
+[[NEWMERGE]]
 DESCRIPTION
 -----------
 
-Performs a merge, but does not make any new commits and does not read
-from or write to either the working tree or index.
+This command has a modern `--write-tree` mode and a deprecated
+`--trivial-merge` mode. The rest of this documentation describes
+modern `--write-tree` mode unless otherwise specified. see
+<<DEPMERGE,DEPRECATED DESCRIPTION>> below for a summary of the
+`--trivial-merge` mode.
 
-The first form will merge the two branches, doing a real merge.  A real
-merge is distinguished from a trivial merge in that it includes:
+Performs a "real" merge, but does not make any new commits and does
+not read from or write to either the working tree or index.
+
+The performed merge will use the same feature as the "real"
+linkgit:git-merge[1], including:
 
   * three way content merges of individual files
   * rename detection
@@ -28,24 +35,8 @@ merge is distinguished from a trivial merge in that it includes:
     merge base, creating a virtual merge base by merging the merge bases)
   * etc.
 
-After the merge completes, the first form will create a new toplevel
-tree object.  See `OUTPUT` below for details.
-
-The second form is deprecated; it is kept for backward compatibility
-reasons but may be deleted in the future.  Other than the optional
-`--trivial-merge`, it accepts no options.  It can only do a trivial
-merge.  It reads three tree-ish, and outputs trivial merge results and
-conflicting stages to the standard output in a semi-diff format.
-Since this was designed for higher level scripts to consume and merge
-the results back into the index, it omits entries that match
-<branch1>.  The result of this second form is similar to what
-three-way 'git read-tree -m' does, but instead of storing the results
-in the index, the command outputs the entries to the standard output.
-This form not only has limited applicability, the output format is
-also difficult to work with, and it will generally be less performant
-than the first form even on successful merges (especially if working
-in large repositories).  The remainder of this manual will only
-discuss the first form.
+After the merge completes, a newtoplevel tree object is created.  See
+`OUTPUT` below for details.
 
 OPTIONS
 -------
@@ -54,7 +45,7 @@ OPTIONS
 	Do not quote filenames in the <Conflicted file info> section,
 	and end each filename with a NUL character rather than
 	newline.  Also begin the messages section with a NUL character
-	instead of a newline.  See OUTPUT below for more information.
+	instead of a newline.  See <<OUTPUT>> below for more information.
 
 --name-only::
 	In the Conflicted file info section, instead of writing a list
@@ -74,11 +65,12 @@ OPTIONS
 	share no common history.  This flag can be given to override that
 	check and make the merge proceed anyway.
 
+[[OUTPUT]]
 OUTPUT
 ------
 
-By default, for a successful merge, the output from git-merge-tree is
-simply one line:
+For a successful merge, the output from git-merge-tree is simply one
+line:
 
 	<OID of toplevel tree>
 
@@ -90,6 +82,7 @@ Whereas for a conflicted merge, the output is by default of the form:
 
 These are discussed individually below.
 
+[[OIDTLT]]
 OID of toplevel tree
 ~~~~~~~~~~~~~~~~~~~~
 
@@ -98,6 +91,7 @@ working tree at the end of `git merge`.  If there were conflicts, then
 files within this tree may have embedded conflict markers.  This section
 is always followed by a newline (or NUL if `-z` is passed).
 
+[[CFI]]
 Conflicted file info
 ~~~~~~~~~~~~~~~~~~~~
 
@@ -111,6 +105,7 @@ the `--name-only` option is passed, the mode, object, and stage will
 be omitted.  If `-z` is passed, the "lines" are terminated by a NUL
 character instead of a newline character.
 
+[[IM]]
 Informational messages
 ~~~~~~~~~~~~~~~~~~~~~~
 
@@ -138,72 +133,94 @@ something other than 0 or 1 (and the output is unspecified).
 USAGE NOTES
 -----------
 
-git-merge-tree was written to be low-level plumbing, similar to
-hash-object, mktree, commit-tree, write-tree, update-ref, and mktag.
-Thus, it could be used as a part of a series of steps such as
+This command is intended as low-level plumbing, similar to
+linkgit:git-hash-object[1], linkgit:git-mktree[1],
+linkgit:git-commit-tree[1], linkgit:git-write-tree[1],
+linkgit:git-update-ref[1], and linkgit:git-mktag[1].  Thus, it can be
+used as a part of a series of steps such as:
 
        NEWTREE=$(git merge-tree --write-tree $BRANCH1 $BRANCH2)
        test $? -eq 0 || die "There were conflicts..."
        NEWCOMMIT=$(git commit-tree $NEWTREE -p $BRANCH1 -p $BRANCH2)
        git update-ref $BRANCH1 $NEWCOMMIT
 
-Note that when the exit status is non-zero, NEWTREE in this sequence
+Note that when the exit status is non-zero, `NEWTREE` in this sequence
 will contain a lot more output than just a tree.
 
-git-merge-tree was written to provide users with the same information
-that they'd have access to if using `git merge`:
-  * what would be written to the working tree (the <OID of toplevel tree>)
+The output will include the same information that you'd get with
+linkgit:git-merge[1]:
+
+  * what would be written to the working tree (the <<OIDTLT,OID of toplevel tree>>)
   * the higher order stages that would be written to the index (the
-    <Conflicted file info>)
-  * any messages that would have been printed to stdout (the <Informational
-    messages>)
+    <<CFI,Conflicted file info>>)
+  * any messages that would have been printed to stdout (the <<IM,Informational
+    messages>>)
 
 MISTAKES TO AVOID
 -----------------
 
 Do NOT look through the resulting toplevel tree to try to find which
-files conflict; parse the <Conflicted file info> section instead.  Not
+files conflict; parse the <<CFI,Conflicted file info>> section instead.  Not
 only would parsing an entire tree be horrendously slow in large
 repositories, there are numerous types of conflicts not representable by
 conflict markers (modify/delete, mode conflict, binary file changed on
 both sides, file/directory conflicts, various rename conflict
 permutations, etc.)
 
-Do NOT interpret an empty <Conflicted file info> list as a clean merge;
+Do NOT interpret an empty <<CFI,Conflicted file info>> list as a clean merge;
 check the exit status.  A merge can have conflicts without having
 individual files conflict (there are a few types of directory rename
 conflicts that fall into this category, and others might also be added
 in the future).
 
 Do NOT attempt to guess or make the user guess the conflict types from
-the <Conflicted file info> list.  The information there is insufficient
+the <<CFI,Conflicted file info>> list.  The information there is insufficient
 to do so.  For example: Rename/rename(1to2) conflicts (both sides
 renamed the same file differently) will result in three different file
 having higher order stages (but each only has one higher order stage),
-with no way (short of the <Informational messages> section) to determine
+with no way (short of the <<IM,Informational messages>> section) to determine
 which three files are related.  File/directory conflicts also result in
 a file with exactly one higher order stage.
 Possibly-involved-in-directory-rename conflicts (when
 "merge.directoryRenames" is unset or set to "conflicts") also result in
 a file with exactly one higher order stage.  In all cases, the
-<Informational messages> section has the necessary info, though it is
+<<IM,Informational messages>> section has the necessary info, though it is
 not designed to be machine parseable.
 
-Do NOT assume all filenames listed in the <Informational messages>
+Do NOT assume all filenames listed in the <<IM,Informational messages>>
 section had conflicts.  Messages can be included for files that have no
 conflicts, such as "Auto-merging <file>".
 
-AVOID taking the OIDS from the <Conflicted file info> and re-merging
+AVOID taking the OIDS from the <<CFI,Conflicted file info>> and re-merging
 them to present the conflicts to the user.  This will lose information.
-Instead, look up the version of the file found within the <OID of
-toplevel tree> and show that instead.  In particular, the latter will
+Instead, look up the version of the file found within the <<OIDTLT,OID of
+toplevel tree>> and show that instead.  In particular, the latter will
 have conflict markers annotated with the original branch/commit being
 merged and, if renames were involved, the original filename.  While you
 could include the original branch/commit in the conflict marker
 annotations when re-merging, the original filename is not available from
-the <Conflicted file info> and thus you would be losing information that
+the <<CFI,Conflicted file info>> and thus you would be losing information that
 might help the user resolve the conflict.
 
+[[DEPMERGE]]
+DEPRECATED DESCRIPTION
+----------------------
+
+Per the <<NEWMERGE,DESCRIPTION>> and unlike the rest of this
+documentation this section describes describes the deprecated
+`--trivial-merge` mode.
+
+Reads three tree-ish, and output trivial merge results and
+conflicting stages to the standard output.  This is similar to
+what three-way 'git read-tree -m' does, but instead of storing the
+results in the index, the command outputs the entries to the
+standard output.
+
+This is meant to be used by higher level scripts to compute
+merge results outside of the index, and stuff the results back into the
+index.  For this reason, the output from the command omits
+entries that match the <branch1> tree.
+
 GIT
 ---
 Part of the linkgit:git[1] suite
#206 lore raw

Re: [PATCH v5 00/12] In-core git merge-tree ("Server side merges")

Hi,

Documentation/git-merge-tree.txt git-merge-tree - Perform merge without touching index or working tree · line 13

Quoted 12 lines · Ævar Arnfjörð Bjarmason
Documentation/git-merge-tree.txt +2 -2
index 306149fa0e2..723b1995426 100644
@@ -9,17 +9,24 @@ git-merge-tree - Perform merge without touching index or working tree
 SYNOPSIS
 --------
 [verse]
-'git merge-tree' [--write-tree] [<options>] <branch1> <branch2>
-'git merge-tree' [--trivial-merge] <base-tree> <branch1> <branch2> (deprecated)
+'git merge-tree' --write-tree [<options>] <branch1> <branch2>
+'git merge-tree' --trivial-merge <base-tree> <branch1> <branch2>

Given that we want to get away from --trivial-merge (and probably even deprecating and then dropping it), this direction makes no sense.

Ciao,
Johannes

#207 lore raw

Re: [PATCH v5 00/12] In-core git merge-tree ("Server side merges")

In reply to Ævar Arnfjörð Bjarmason
Quoted 98 lines · Ævar Arnfjörð Bjarmason
Quoted 33 lines · Elijah Newren via GitGitGadget

== Updates Log ==

Many thanks to the many reviewers who provided good feedback on the most recent round -- Junio, Ævar, Josh, Emily, and perhaps some others I've forgotten from review club.

Updates since v4:

* Fixed double "is" in documentation.
* Fixed a few small items with testcases

Updates since v3 (or v5, if you include the rounds at
https://lore.kernel.org/git/[email protected]/):

* Dropped previous patches 5, 6, and 8 of the old series; they weren't being used and opened a can of worms[1]
* [Patch 3] Restructured argument checking, including using an enum
* [Patch 4] Restored the extended paragraph about the deprecated form of git-merge-tree, mentioned write-tree in plumbing commands, and a few other small fixups to the documentation
* [Patch 4] Also provide an example of a clean merge rather than just a conflicted one
* [Patch 6] Fix the incompatible arguments check and add some tests for it
* [Patch 6] Introduce an anonymize_hash() shell function to make tests easier to read (less repeated sed)
* [Patch 9] Rename --exclude-modes-oids-stages to --name-only; no short option for now
* [Patch 10] When -z passed, the tree in the first section should have a trailing NUL rather than trailing newline [1]
https://lore.kernel.org/git/CABPp-BEKuXHELVx4=5JJTj5HVOKZ=Y-4G4BK47BCZYYRSrkFsQ@mail.gmail.com/

Stuff NOT included that reviewers brought up in earlier rounds:

Quoted 14 lines

* Very generic (mode, oid, stage, filename) printing formatting[2]
* Always printing 3 stages for each filename with conflicts[3]
* Attempting to group conflict stages by logical conflict rather than by affected target filepath[4]
* Providing similar functionality for doing cherry-picks/rebases/reverts, i.e. a scheme for three-way merges with a specified merge-base[5]. That's being deferred to a future series. [2]
https://lore.kernel.org/git/CABPp-BGnOes7J_piDyBUeuLVm274w4-9G3k0vR-0it3z7TPn_w@mail.gmail.com/
[3]
https://lore.kernel.org/git/CABPp-BG2rMEYBLuBW=0wtpJe4aUFGCFa8D0NTSKz9Sm+CkXPxw@mail.gmail.com/
[4]
https://lore.kernel.org/git/CABPp-BGCL0onSmpgKuO1k2spYCkx=v27ed9TSSxFib=OdDcLbw@mail.gmail.com/
[5]
https://lore.kernel.org/git/CABPp-BEaemkGGm0cSofP0gau7YN-y6HFoi0yJbHA8+iGjxsYSA@mail.gmail.com/

I've looked through this, I think it all looks good overall & that the things that needed to be addressed (as opposed to my --format rambling) have been.

I think all the code should be ready for "next".

I suggested (I think around getopts discussion) in an earlier that the code would have been easier with a new built-in, but if we're deprecating the existing "mode" I think using the name is probably better in the end.

I find the resulting documentation to be really hard to grok though because we're effectively describing two different commands. The current docs are small: https://git-scm.com/docs/git-merge-tree

I built the tip of this series and read the manpage, and found myself needing to carefully squint to see what referred to what mode in the docs.

E.g. by the time it's discussing "-z" and other options the reader needs to be astute really aware of the context, and infer from the lack of "<options>" on the "--trivial-merge" that these options refer to the "--write-tree" only.

The same goes for the rest of "--trivial-merge". I.e. I found myself needing to read the whole docs word-by-word (no skimming!) to see if OUTPUT etc. was going to describe its output, or just the "new" mode.

It is my NSHO that man pages should be structured for the impatient reader :)

Then when you say "git-merge-tree was written to be[...]" I thought "ah ha! surely this will discuss the since-2005 implemented mode", but "was written to be" is referring to code new in this series.

The below patch-on-top addresses all those concerns. Basically I just added a line to the top of the DESCRIPTION saying that you should read a "DEPRECATED DESCRIPTION" section at the end for "--trivial-merge", and that all of the rest is talking about the "--write-tree" mode.

I then edited various prose to do away with the now-unnecessary "the first form" etc.

This diff is better against "master" in that you'll see that the current merge-tree DESCRIPTION section isn't touched at all (it's now just under a new heading), but this diff is against the tip of your series.

Quoted 8 lines

There's various other small fixes while I was it it here, e.g. all your cross-section links were using some pseudo-not-quite-ASCIIDOC syntax that doesn't work. Now it uses the right syntax. Ditto link-ifying references to "mktag" etc.

I don't know if you'd consider this for a v6, or if I should just submit this on top myself, but in any case here it is. I'll leave it to you how you'd like to proceed with it:

Overall the diff below looks good; thanks! I'll include it in a v6. Two exceptions, listed below:

Documentation/git-merge-tree.txt git-merge-tree - Perform merge without touching index or working tree · line 13

Quoted 12 lines
Documentation/git-merge-tree.txt +2 -2
index 306149fa0e2..723b1995426 100644
@@ -9,17 +9,24 @@ git-merge-tree - Perform merge without touching index or working tree
 SYNOPSIS
 --------
 [verse]
-'git merge-tree' [--write-tree] [<options>] <branch1> <branch2>
-'git merge-tree' [--trivial-merge] <base-tree> <branch1> <branch2> (deprecated)
+'git merge-tree' --write-tree [<options>] <branch1> <branch2>
+'git merge-tree' --trivial-merge <base-tree> <branch1> <branch2>

I dislike all three changes to the synopsis; I'll discard these.

Quoted 198 lines
Documentation/git-merge-tree.txt +51 -45
+[[NEWMERGE]]
 DESCRIPTION
 -----------
 
-Performs a merge, but does not make any new commits and does not read
-from or write to either the working tree or index.
+This command has a modern `--write-tree` mode and a deprecated
+`--trivial-merge` mode. The rest of this documentation describes
+modern `--write-tree` mode unless otherwise specified. see
+<<DEPMERGE,DEPRECATED DESCRIPTION>> below for a summary of the
+`--trivial-merge` mode.
 
-The first form will merge the two branches, doing a real merge.  A real
-merge is distinguished from a trivial merge in that it includes:
+Performs a "real" merge, but does not make any new commits and does
+not read from or write to either the working tree or index.
+
+The performed merge will use the same feature as the "real"
+linkgit:git-merge[1], including:
 
   * three way content merges of individual files
   * rename detection
@@ -28,24 +35,8 @@ merge is distinguished from a trivial merge in that it includes:
     merge base, creating a virtual merge base by merging the merge bases)
   * etc.
 
-After the merge completes, the first form will create a new toplevel
-tree object.  See `OUTPUT` below for details.
-
-The second form is deprecated; it is kept for backward compatibility
-reasons but may be deleted in the future.  Other than the optional
-`--trivial-merge`, it accepts no options.  It can only do a trivial
-merge.  It reads three tree-ish, and outputs trivial merge results and
-conflicting stages to the standard output in a semi-diff format.
-Since this was designed for higher level scripts to consume and merge
-the results back into the index, it omits entries that match
-<branch1>.  The result of this second form is similar to what
-three-way 'git read-tree -m' does, but instead of storing the results
-in the index, the command outputs the entries to the standard output.
-This form not only has limited applicability, the output format is
-also difficult to work with, and it will generally be less performant
-than the first form even on successful merges (especially if working
-in large repositories).  The remainder of this manual will only
-discuss the first form.
+After the merge completes, a newtoplevel tree object is created.  See
+`OUTPUT` below for details.
 
 OPTIONS
 -------
@@ -54,7 +45,7 @@ OPTIONS
        Do not quote filenames in the <Conflicted file info> section,
        and end each filename with a NUL character rather than
        newline.  Also begin the messages section with a NUL character
-       instead of a newline.  See OUTPUT below for more information.
+       instead of a newline.  See <<OUTPUT>> below for more information.
 
 --name-only::
        In the Conflicted file info section, instead of writing a list
@@ -74,11 +65,12 @@ OPTIONS
        share no common history.  This flag can be given to override that
        check and make the merge proceed anyway.
 
+[[OUTPUT]]
 OUTPUT
 ------
 
-By default, for a successful merge, the output from git-merge-tree is
-simply one line:
+For a successful merge, the output from git-merge-tree is simply one
+line:
 
        <OID of toplevel tree>
 
@@ -90,6 +82,7 @@ Whereas for a conflicted merge, the output is by default of the form:
 
 These are discussed individually below.
 
+[[OIDTLT]]
 OID of toplevel tree
 ~~~~~~~~~~~~~~~~~~~~
 
@@ -98,6 +91,7 @@ working tree at the end of `git merge`.  If there were conflicts, then
 files within this tree may have embedded conflict markers.  This section
 is always followed by a newline (or NUL if `-z` is passed).
 
+[[CFI]]
 Conflicted file info
 ~~~~~~~~~~~~~~~~~~~~
 
@@ -111,6 +105,7 @@ the `--name-only` option is passed, the mode, object, and stage will
 be omitted.  If `-z` is passed, the "lines" are terminated by a NUL
 character instead of a newline character.
 
+[[IM]]
 Informational messages
 ~~~~~~~~~~~~~~~~~~~~~~
 
@@ -138,72 +133,94 @@ something other than 0 or 1 (and the output is unspecified).
 USAGE NOTES
 -----------
 
-git-merge-tree was written to be low-level plumbing, similar to
-hash-object, mktree, commit-tree, write-tree, update-ref, and mktag.
-Thus, it could be used as a part of a series of steps such as
+This command is intended as low-level plumbing, similar to
+linkgit:git-hash-object[1], linkgit:git-mktree[1],
+linkgit:git-commit-tree[1], linkgit:git-write-tree[1],
+linkgit:git-update-ref[1], and linkgit:git-mktag[1].  Thus, it can be
+used as a part of a series of steps such as:
 
        NEWTREE=$(git merge-tree --write-tree $BRANCH1 $BRANCH2)
        test $? -eq 0 || die "There were conflicts..."
        NEWCOMMIT=$(git commit-tree $NEWTREE -p $BRANCH1 -p $BRANCH2)
        git update-ref $BRANCH1 $NEWCOMMIT
 
-Note that when the exit status is non-zero, NEWTREE in this sequence
+Note that when the exit status is non-zero, `NEWTREE` in this sequence
 will contain a lot more output than just a tree.
 
-git-merge-tree was written to provide users with the same information
-that they'd have access to if using `git merge`:
-  * what would be written to the working tree (the <OID of toplevel tree>)
+The output will include the same information that you'd get with
+linkgit:git-merge[1]:
+
+  * what would be written to the working tree (the <<OIDTLT,OID of toplevel tree>>)
   * the higher order stages that would be written to the index (the
-    <Conflicted file info>)
-  * any messages that would have been printed to stdout (the <Informational
-    messages>)
+    <<CFI,Conflicted file info>>)
+  * any messages that would have been printed to stdout (the <<IM,Informational
+    messages>>)
 
 MISTAKES TO AVOID
 -----------------
 
 Do NOT look through the resulting toplevel tree to try to find which
-files conflict; parse the <Conflicted file info> section instead.  Not
+files conflict; parse the <<CFI,Conflicted file info>> section instead.  Not
 only would parsing an entire tree be horrendously slow in large
 repositories, there are numerous types of conflicts not representable by
 conflict markers (modify/delete, mode conflict, binary file changed on
 both sides, file/directory conflicts, various rename conflict
 permutations, etc.)
 
-Do NOT interpret an empty <Conflicted file info> list as a clean merge;
+Do NOT interpret an empty <<CFI,Conflicted file info>> list as a clean merge;
 check the exit status.  A merge can have conflicts without having
 individual files conflict (there are a few types of directory rename
 conflicts that fall into this category, and others might also be added
 in the future).
 
 Do NOT attempt to guess or make the user guess the conflict types from
-the <Conflicted file info> list.  The information there is insufficient
+the <<CFI,Conflicted file info>> list.  The information there is insufficient
 to do so.  For example: Rename/rename(1to2) conflicts (both sides
 renamed the same file differently) will result in three different file
 having higher order stages (but each only has one higher order stage),
-with no way (short of the <Informational messages> section) to determine
+with no way (short of the <<IM,Informational messages>> section) to determine
 which three files are related.  File/directory conflicts also result in
 a file with exactly one higher order stage.
 Possibly-involved-in-directory-rename conflicts (when
 "merge.directoryRenames" is unset or set to "conflicts") also result in
 a file with exactly one higher order stage.  In all cases, the
-<Informational messages> section has the necessary info, though it is
+<<IM,Informational messages>> section has the necessary info, though it is
 not designed to be machine parseable.
 
-Do NOT assume all filenames listed in the <Informational messages>
+Do NOT assume all filenames listed in the <<IM,Informational messages>>
 section had conflicts.  Messages can be included for files that have no
 conflicts, such as "Auto-merging <file>".
 
-AVOID taking the OIDS from the <Conflicted file info> and re-merging
+AVOID taking the OIDS from the <<CFI,Conflicted file info>> and re-merging
 them to present the conflicts to the user.  This will lose information.
-Instead, look up the version of the file found within the <OID of
-toplevel tree> and show that instead.  In particular, the latter will
+Instead, look up the version of the file found within the <<OIDTLT,OID of
+toplevel tree>> and show that instead.  In particular, the latter will
 have conflict markers annotated with the original branch/commit being
 merged and, if renames were involved, the original filename.  While you
 could include the original branch/commit in the conflict marker
 annotations when re-merging, the original filename is not available from
-the <Conflicted file info> and thus you would be losing information that
+the <<CFI,Conflicted file info>> and thus you would be losing information that
 might help the user resolve the conflict.
 
+[[DEPMERGE]]
+DEPRECATED DESCRIPTION
+----------------------
+
+Per the <<NEWMERGE,DESCRIPTION>> and unlike the rest of this
+documentation this section describes describes the deprecated
+`--trivial-merge` mode.
+

Documentation/git-merge-tree.txt something other than 0 or 1 (and the output is unspecified). · line 222

Quoted 10 lines
Documentation/git-merge-tree.txt +10 -0
+Reads three tree-ish, and output trivial merge results and
+conflicting stages to the standard output.  This is similar to
+what three-way 'git read-tree -m' does, but instead of storing the
+results in the index, the command outputs the entries to the
+standard output.
+
+This is meant to be used by higher level scripts to compute
+merge results outside of the index, and stuff the results back into the
+index.  For this reason, the output from the command omits
+entries that match the <branch1> tree.

What you have in the new section is fine, but what you've deleted should probably be included here. In particular, the brief explanation about why this mode is deprecated.

Quoted 5 lines
Documentation/git-merge-tree.txt +1 -0
+
 GIT
 ---
 Part of the linkgit:git[1] suite
#208 lore raw

Re: [PATCH v5 00/12] In-core git merge-tree ("Server side merges")

Quoted 105 lines · Elijah Newren
Quoted 103 lines · Ævar Arnfjörð Bjarmason
Quoted 33 lines · Elijah Newren via GitGitGadget

== Updates Log ==

Many thanks to the many reviewers who provided good feedback on the most recent round -- Junio, Ævar, Josh, Emily, and perhaps some others I've forgotten from review club.

Updates since v4:

* Fixed double "is" in documentation.
* Fixed a few small items with testcases

Updates since v3 (or v5, if you include the rounds at
https://lore.kernel.org/git/[email protected]/):

* Dropped previous patches 5, 6, and 8 of the old series; they weren't being used and opened a can of worms[1]
* [Patch 3] Restructured argument checking, including using an enum
* [Patch 4] Restored the extended paragraph about the deprecated form of git-merge-tree, mentioned write-tree in plumbing commands, and a few other small fixups to the documentation
* [Patch 4] Also provide an example of a clean merge rather than just a conflicted one
* [Patch 6] Fix the incompatible arguments check and add some tests for it
* [Patch 6] Introduce an anonymize_hash() shell function to make tests easier to read (less repeated sed)
* [Patch 9] Rename --exclude-modes-oids-stages to --name-only; no short option for now
* [Patch 10] When -z passed, the tree in the first section should have a trailing NUL rather than trailing newline [1]
https://lore.kernel.org/git/CABPp-BEKuXHELVx4=5JJTj5HVOKZ=Y-4G4BK47BCZYYRSrkFsQ@mail.gmail.com/

Stuff NOT included that reviewers brought up in earlier rounds:

Quoted 14 lines

* Very generic (mode, oid, stage, filename) printing formatting[2]
* Always printing 3 stages for each filename with conflicts[3]
* Attempting to group conflict stages by logical conflict rather than by affected target filepath[4]
* Providing similar functionality for doing cherry-picks/rebases/reverts, i.e. a scheme for three-way merges with a specified merge-base[5]. That's being deferred to a future series. [2]
https://lore.kernel.org/git/CABPp-BGnOes7J_piDyBUeuLVm274w4-9G3k0vR-0it3z7TPn_w@mail.gmail.com/
[3]
https://lore.kernel.org/git/CABPp-BG2rMEYBLuBW=0wtpJe4aUFGCFa8D0NTSKz9Sm+CkXPxw@mail.gmail.com/
[4]
https://lore.kernel.org/git/CABPp-BGCL0onSmpgKuO1k2spYCkx=v27ed9TSSxFib=OdDcLbw@mail.gmail.com/
[5]
https://lore.kernel.org/git/CABPp-BEaemkGGm0cSofP0gau7YN-y6HFoi0yJbHA8+iGjxsYSA@mail.gmail.com/

I've looked through this, I think it all looks good overall & that the things that needed to be addressed (as opposed to my --format rambling) have been.

I think all the code should be ready for "next".

I suggested (I think around getopts discussion) in an earlier that the code would have been easier with a new built-in, but if we're deprecating the existing "mode" I think using the name is probably better in the end.

I find the resulting documentation to be really hard to grok though because we're effectively describing two different commands. The current docs are small: https://git-scm.com/docs/git-merge-tree

I built the tip of this series and read the manpage, and found myself needing to carefully squint to see what referred to what mode in the docs.

E.g. by the time it's discussing "-z" and other options the reader needs to be astute really aware of the context, and infer from the lack of "<options>" on the "--trivial-merge" that these options refer to the "--write-tree" only.

The same goes for the rest of "--trivial-merge". I.e. I found myself needing to read the whole docs word-by-word (no skimming!) to see if OUTPUT etc. was going to describe its output, or just the "new" mode.

It is my NSHO that man pages should be structured for the impatient reader :)

Then when you say "git-merge-tree was written to be[...]" I thought "ah ha! surely this will discuss the since-2005 implemented mode", but "was written to be" is referring to code new in this series.

The below patch-on-top addresses all those concerns. Basically I just added a line to the top of the DESCRIPTION saying that you should read a "DEPRECATED DESCRIPTION" section at the end for "--trivial-merge", and that all of the rest is talking about the "--write-tree" mode.

I then edited various prose to do away with the now-unnecessary "the first form" etc.

This diff is better against "master" in that you'll see that the current merge-tree DESCRIPTION section isn't touched at all (it's now just under a new heading), but this diff is against the tip of your series.

There's various other small fixes while I was it it here, e.g. all your cross-section links were using some pseudo-not-quite-ASCIIDOC syntax that doesn't work. Now it uses the right syntax. Ditto link-ifying references to "mktag" etc.

Quoted 6 lines
Quoted 3 lines

I don't know if you'd consider this for a v6, or if I should just submit this on top myself, but in any case here it is. I'll leave it to you how you'd like to proceed with it:

Overall the diff below looks good; thanks! I'll include it in a v6. Two exceptions, listed below:

Thanks.

Quoted 6 lines
Quoted 6 lines
Documentation/git-merge-tree.txt +0 -0
index 306149fa0e2..723b1995426 100644
@@ -9,17 +9,24 @@ git-merge-tree - Perform merge without touching index or working tree
 SYNOPSIS
Quoted 8 lines

Documentation/git-merge-tree.txt git-merge-tree - Perform merge without touching index or working tree · line 13

Quoted 6 lines
Documentation/git-merge-tree.txt +2 -2
 --------
 [verse]
-'git merge-tree' [--write-tree] [<options>] <branch1> <branch2>
-'git merge-tree' [--trivial-merge] <base-tree> <branch1> <branch2> (deprecated)
+'git merge-tree' --write-tree [<options>] <branch1> <branch2>
+'git merge-tree' --trivial-merge <base-tree> <branch1> <branch2>

I dislike all three changes to the synopsis; I'll discard these.

I don't know why I changed --write-tree to [--write-tree], I think that's just incorrect.

But what I was going for was removing "(deprecated)" there.

Maybe it's confusing to noone else, but it is ambiguous in the way we usually use parens in SYNOPSIS. I.e. per that syntax it means that the string "deprecated" is a mandotory paramater after <branch2>.

Maybe just having a "DEPRECATED SYNOPSIS" section after "SYNOPSIS" would be better? That would also allow for more lines of examples without re-wording everything that comes after with "the first form" etc.

Quoted 204 lines
Quoted 204 lines
Documentation/git-merge-tree.txt +57 -45
+[[NEWMERGE]]
 DESCRIPTION
 -----------
 
-Performs a merge, but does not make any new commits and does not read
-from or write to either the working tree or index.
+This command has a modern `--write-tree` mode and a deprecated
+`--trivial-merge` mode. The rest of this documentation describes
+modern `--write-tree` mode unless otherwise specified. see
+<<DEPMERGE,DEPRECATED DESCRIPTION>> below for a summary of the
+`--trivial-merge` mode.
 
-The first form will merge the two branches, doing a real merge.  A real
-merge is distinguished from a trivial merge in that it includes:
+Performs a "real" merge, but does not make any new commits and does
+not read from or write to either the working tree or index.
+
+The performed merge will use the same feature as the "real"
+linkgit:git-merge[1], including:
 
   * three way content merges of individual files
   * rename detection
@@ -28,24 +35,8 @@ merge is distinguished from a trivial merge in that it includes:
     merge base, creating a virtual merge base by merging the merge bases)
   * etc.
 
-After the merge completes, the first form will create a new toplevel
-tree object.  See `OUTPUT` below for details.
-
-The second form is deprecated; it is kept for backward compatibility
-reasons but may be deleted in the future.  Other than the optional
-`--trivial-merge`, it accepts no options.  It can only do a trivial
-merge.  It reads three tree-ish, and outputs trivial merge results and
-conflicting stages to the standard output in a semi-diff format.
-Since this was designed for higher level scripts to consume and merge
-the results back into the index, it omits entries that match
-<branch1>.  The result of this second form is similar to what
-three-way 'git read-tree -m' does, but instead of storing the results
-in the index, the command outputs the entries to the standard output.
-This form not only has limited applicability, the output format is
-also difficult to work with, and it will generally be less performant
-than the first form even on successful merges (especially if working
-in large repositories).  The remainder of this manual will only
-discuss the first form.
+After the merge completes, a newtoplevel tree object is created.  See
+`OUTPUT` below for details.
 
 OPTIONS
 -------
@@ -54,7 +45,7 @@ OPTIONS
        Do not quote filenames in the <Conflicted file info> section,
        and end each filename with a NUL character rather than
        newline.  Also begin the messages section with a NUL character
-       instead of a newline.  See OUTPUT below for more information.
+       instead of a newline.  See <<OUTPUT>> below for more information.
 
 --name-only::
        In the Conflicted file info section, instead of writing a list
@@ -74,11 +65,12 @@ OPTIONS
        share no common history.  This flag can be given to override that
        check and make the merge proceed anyway.
 
+[[OUTPUT]]
 OUTPUT
 ------
 
-By default, for a successful merge, the output from git-merge-tree is
-simply one line:
+For a successful merge, the output from git-merge-tree is simply one
+line:
 
        <OID of toplevel tree>
 
@@ -90,6 +82,7 @@ Whereas for a conflicted merge, the output is by default of the form:
 
 These are discussed individually below.
 
+[[OIDTLT]]
 OID of toplevel tree
 ~~~~~~~~~~~~~~~~~~~~
 
@@ -98,6 +91,7 @@ working tree at the end of `git merge`.  If there were conflicts, then
 files within this tree may have embedded conflict markers.  This section
 is always followed by a newline (or NUL if `-z` is passed).
 
+[[CFI]]
 Conflicted file info
 ~~~~~~~~~~~~~~~~~~~~
 
@@ -111,6 +105,7 @@ the `--name-only` option is passed, the mode, object, and stage will
 be omitted.  If `-z` is passed, the "lines" are terminated by a NUL
 character instead of a newline character.
 
+[[IM]]
 Informational messages
 ~~~~~~~~~~~~~~~~~~~~~~
 
@@ -138,72 +133,94 @@ something other than 0 or 1 (and the output is unspecified).
 USAGE NOTES
 -----------
 
-git-merge-tree was written to be low-level plumbing, similar to
-hash-object, mktree, commit-tree, write-tree, update-ref, and mktag.
-Thus, it could be used as a part of a series of steps such as
+This command is intended as low-level plumbing, similar to
+linkgit:git-hash-object[1], linkgit:git-mktree[1],
+linkgit:git-commit-tree[1], linkgit:git-write-tree[1],
+linkgit:git-update-ref[1], and linkgit:git-mktag[1].  Thus, it can be
+used as a part of a series of steps such as:
 
        NEWTREE=$(git merge-tree --write-tree $BRANCH1 $BRANCH2)
        test $? -eq 0 || die "There were conflicts..."
        NEWCOMMIT=$(git commit-tree $NEWTREE -p $BRANCH1 -p $BRANCH2)
        git update-ref $BRANCH1 $NEWCOMMIT
 
-Note that when the exit status is non-zero, NEWTREE in this sequence
+Note that when the exit status is non-zero, `NEWTREE` in this sequence
 will contain a lot more output than just a tree.
 
-git-merge-tree was written to provide users with the same information
-that they'd have access to if using `git merge`:
-  * what would be written to the working tree (the <OID of toplevel tree>)
+The output will include the same information that you'd get with
+linkgit:git-merge[1]:
+
+  * what would be written to the working tree (the <<OIDTLT,OID of toplevel tree>>)
   * the higher order stages that would be written to the index (the
-    <Conflicted file info>)
-  * any messages that would have been printed to stdout (the <Informational
-    messages>)
+    <<CFI,Conflicted file info>>)
+  * any messages that would have been printed to stdout (the <<IM,Informational
+    messages>>)
 
 MISTAKES TO AVOID
 -----------------
 
 Do NOT look through the resulting toplevel tree to try to find which
-files conflict; parse the <Conflicted file info> section instead.  Not
+files conflict; parse the <<CFI,Conflicted file info>> section instead.  Not
 only would parsing an entire tree be horrendously slow in large
 repositories, there are numerous types of conflicts not representable by
 conflict markers (modify/delete, mode conflict, binary file changed on
 both sides, file/directory conflicts, various rename conflict
 permutations, etc.)
 
-Do NOT interpret an empty <Conflicted file info> list as a clean merge;
+Do NOT interpret an empty <<CFI,Conflicted file info>> list as a clean merge;
 check the exit status.  A merge can have conflicts without having
 individual files conflict (there are a few types of directory rename
 conflicts that fall into this category, and others might also be added
 in the future).
 
 Do NOT attempt to guess or make the user guess the conflict types from
-the <Conflicted file info> list.  The information there is insufficient
+the <<CFI,Conflicted file info>> list.  The information there is insufficient
 to do so.  For example: Rename/rename(1to2) conflicts (both sides
 renamed the same file differently) will result in three different file
 having higher order stages (but each only has one higher order stage),
-with no way (short of the <Informational messages> section) to determine
+with no way (short of the <<IM,Informational messages>> section) to determine
 which three files are related.  File/directory conflicts also result in
 a file with exactly one higher order stage.
 Possibly-involved-in-directory-rename conflicts (when
 "merge.directoryRenames" is unset or set to "conflicts") also result in
 a file with exactly one higher order stage.  In all cases, the
-<Informational messages> section has the necessary info, though it is
+<<IM,Informational messages>> section has the necessary info, though it is
 not designed to be machine parseable.
 
-Do NOT assume all filenames listed in the <Informational messages>
+Do NOT assume all filenames listed in the <<IM,Informational messages>>
 section had conflicts.  Messages can be included for files that have no
 conflicts, such as "Auto-merging <file>".
 
-AVOID taking the OIDS from the <Conflicted file info> and re-merging
+AVOID taking the OIDS from the <<CFI,Conflicted file info>> and re-merging
 them to present the conflicts to the user.  This will lose information.
-Instead, look up the version of the file found within the <OID of
-toplevel tree> and show that instead.  In particular, the latter will
+Instead, look up the version of the file found within the <<OIDTLT,OID of
+toplevel tree>> and show that instead.  In particular, the latter will
 have conflict markers annotated with the original branch/commit being
 merged and, if renames were involved, the original filename.  While you
 could include the original branch/commit in the conflict marker
 annotations when re-merging, the original filename is not available from
-the <Conflicted file info> and thus you would be losing information that
+the <<CFI,Conflicted file info>> and thus you would be losing information that
 might help the user resolve the conflict.
 
+[[DEPMERGE]]
+DEPRECATED DESCRIPTION
+----------------------
+
+Per the <<NEWMERGE,DESCRIPTION>> and unlike the rest of this
+documentation this section describes describes the deprecated
+`--trivial-merge` mode.
+
+Reads three tree-ish, and output trivial merge results and
+conflicting stages to the standard output.  This is similar to
+what three-way 'git read-tree -m' does, but instead of storing the
+results in the index, the command outputs the entries to the
+standard output.
+
Quoted 8 lines

Documentation/git-merge-tree.txt something other than 0 or 1 (and the output is unspecified). · line 222

Quoted 4 lines
Documentation/git-merge-tree.txt +4 -0
+This is meant to be used by higher level scripts to compute
+merge results outside of the index, and stuff the results back into the
+index.  For this reason, the output from the command omits
+entries that match the <branch1> tree.

What you have in the new section is fine, but what you've deleted should probably be included here. In particular, the brief explanation about why this mode is deprecated.

Ah, yes. That was overzelous. Sorry. I skimmed that and IIRC thought it was a re-flowing of the old DESCRIPTION section.

#209 lore raw

v6 cover In-core git merge-tree ("Server side merges")

In reply to Elijah Newren via GitGitGadget

== Basic Summary ==

This series introduces a new mode to git merge-tree allowing it to perform real merges (three-way text content merges, recursive ancestor consolidation, rename detection, proper directory/file conflict handling, etc.) and write the result as a toplevel tree. It doesn't touch the working tree or index, and doesn't create any commits or update any refs. It could be used to do merges when in a bare repository (thus potentially making it of interest to Git hosting sites, i.e. "Server side merges"), or for doing a merge of branches that aren't checked out.

It does not handle similar functionality for cherry-picks, rebases, or reverts; that is also of interest, but is being deferred for a future series.

== Quick Overview ==

* Patches 1-2: preparatory cleanups
* Patches 3-4: implement basic real merges
* Patches 5-6: include informational messages ("CONFLICT" messages and such) in output
* Patches 7-10: add ability to include ls-files -u style of info in the output
* Patch 11: support --allow-unrelated-histories
* Patch 12: augment the manual with potential usage mistakes

== Updates Log ==

Stuff NOT included that reviewers brought up in various rounds (and which might still be an open question):

* Having -z affect the informational messages section[1]
* Having a machine-parseable variant of information from the informational messages section[2]
* Very generic (mode, oid, stage, filename) printing formatting[3]
* Providing similar functionality for doing cherry-picks/rebases/reverts, i.e. a scheme for three-way merges with a specified merge-base[4]. That's being deferred to a future series. [1]
https://lore.kernel.org/git/CABPp-BG7id0GfpDee_7ETZ_94BC_i-e_=-u=PrYJeD7d4sVbiw@mail.gmail.com/
[2]
https://lore.kernel.org/git/[email protected]/,
https://lore.kernel.org/git/[email protected]/
[3]
https://lore.kernel.org/git/CABPp-BGnOes7J_piDyBUeuLVm274w4-9G3k0vR-0it3z7TPn_w@mail.gmail.com/
[4]
https://lore.kernel.org/git/CABPp-BEaemkGGm0cSofP0gau7YN-y6HFoi0yJbHA8+iGjxsYSA@mail.gmail.com/

Updates since v5:

* Used reverse_commit_list() to reverse a commit_list without extra allocations
* Several documentation updates

Updates since v4:

* Fixed double "is" in documentation.
* Fixed a few small items with testcases

Updates since v3:

* Dropped previous patches 5, 6, and 8 of the old series; they weren't being used and opened a can of worms[1]
* [Patch 3] Restructured argument checking, including using an enum
* [Patch 4] Restored the extended paragraph about the deprecated form of git-merge-tree, mentioned write-tree in plumbing commands, and a few other small fixups to the documentation
* [Patch 4] Also provide an example of a clean merge rather than just a conflicted one
* [Patch 6] Fix the incompatible arguments check and add some tests for it
* [Patch 6] Introduce an anonymize_hash() shell function to make tests easier to read (less repeated sed)
* [Patch 9] Rename --exclude-modes-oids-stages to --name-only; no short option for now
* [Patch 10] When -z passed, the tree in the first section should have a trailing NUL rather than trailing newline [1]
https://lore.kernel.org/git/CABPp-BEKuXHELVx4=5JJTj5HVOKZ=Y-4G4BK47BCZYYRSrkFsQ@mail.gmail.com/

Updates since v2:

* Improved patches from Dscho for the diff_warn_rename_limit() handling
* Add a -z option for NUL-terminated conflict info lines (so that filenames do not have to be quoted)

Updates since v1 (or v3 depending on how you count; thanks to René, Ævar, Christian, Dscho for very helpful feedback):

* New patch from Dscho allowing diff_warn_rename_limit() to print somewhere other than stdout (I hope he's okay with me including his Signed-off-by)
* Now prints filenames relative to prefix, much like ls-files
* Renamed --exclude-oids-and-modes to --exclude-modes-oids-stages and gave it a -l shorthand; I'm wondering if I should just drop this option, though.
* And numerous cleanups, in lots of areas:
* Multiple parse-options cleanups
* Lots of commit message cleanups
* Wording tweaks to the "Description" section of the manual
* Several small code cleanups
* I dropped the RFC label

[There were also two submissions of a previous series; see
https://lore.kernel.org/git/[email protected]/]
[https://lore.kernel.org/git/[email protected]/%5D]

Updates since original submission v2 (thanks to Christian, Dscho, Ramsay, and René for suggestions and comments):

* Significant changes to output format:
* Flags no longer take a filename for additional output; they write to stdout instead.
* More information included by default when there are conflicts (no need to request it with additional flags, instead flags can be used to suppress it).
* Provide (mode, oid, stage, file) tuples -- i.e. ls-files -u style of information -- when there are conflicts. Add a flag to only list conflicted files if that's preferred.
* Much more thorough manual for git-merge-tree.txt
* Renamed option from --real to --write-tree
* Accept an optional --trivial-merge option to get old style merge-tree behavior
* Allow both --write-tree and --trivial-merge to be omitted since we can deduce which from number of arguments
* Document exit code when the merge cannot be run (so we can distinguish other error cases from conflicts)
* testcase cleanups: test_tick, early skip of test when using recursive backend, variable renames, etc.
* various minor code cleanups
* Add a new --allow-unrelated-histories option (with same meaning as the one used in git merge)
* Rebased on top of en/remerge-diff to avoid a small conflict

Updates since original submission v1 (thanks to Johannes Altmanninger and Fabian for suggestions):

* Fixed a bad patch splitting, and a style issue pointed out by Johannes Altimanninger
* Fixed misleading commit messages in new test cases
* Fixed my comments about how commit-tree could be used to correctly use two -p flags

Elijah Newren (12):
merge-tree: rename merge_trees() to trivial_merge_trees()
merge-tree: move logic for existing merge into new function
merge-tree: add option parsing and initial shell for real merge
function
merge-tree: implement real merges
merge-ort: split out a separate display_update_messages() function
merge-tree: support including merge messages in output
merge-ort: provide a merge_get_conflicted_files() helper function
merge-tree: provide a list of which files have conflicts
merge-tree: provide easy access to ls-files -u style info
merge-tree: allow ls-files -u style info to be NUL terminated
merge-tree: add a --allow-unrelated-histories flag git-merge-tree.txt: add a section on potentional usage mistakes

 Documentation/git-merge-tree.txt | 228 +++++++++++++++++++++++++++--
 builtin/merge-tree.c             | 186 ++++++++++++++++++++++--
 git.c                            |   2 +-
 merge-ort.c                      | 109 +++++++++-----
 merge-ort.h                      |  29 ++++
 t/t4301-merge-tree-write-tree.sh | 238 +++++++++++++++++++++++++++++++
 6 files changed, 730 insertions(+), 62 deletions(-)
 create mode 100755 t/t4301-merge-tree-write-tree.sh

base-commit: ea5df61cf358d3c831189e2f04863abc2157e3e1
Published-As: https://github.com/gitgitgadget/git/releases/tag/pr-1122%2Fnewren%2Fin-core-merge-tree-v6
Fetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-1122/newren/in-core-merge-tree-v6
Pull-Request: https://github.com/gitgitgadget/git/pull/1122

Range-diff vs v5:

  1:  4a7cd5542bb =  1:  4a7cd5542bb merge-tree: rename merge_trees() to trivial_merge_trees()
  2:  4780ff6784d =  2:  4780ff6784d merge-tree: move logic for existing merge into new function
  3:  60253745f5c =  3:  60253745f5c merge-tree: add option parsing and initial shell for real merge function
  4:  7994775a934 !  4:  f8266d39c1b merge-tree: implement real merges

@@ Documentation/git-merge-tree.txt: git-merge-tree(1)
+'git merge-tree' [--write-tree] <branch1> <branch2>
+'git merge-tree' [--trivial-merge] <base-tree> <branch1> <branch2> (deprecated)

++[[NEWMERGE]]
DESCRIPTION
-----------
-Reads three tree-ish, and output trivial merge results and
@@ Documentation/git-merge-tree.txt: git-merge-tree(1)
-index. For this reason, the output from the command omits -entries that match the <branch1> tree.
+
++This command has a modern --write-tree mode and a deprecated ++--trivial-merge mode. With the exception of the ++<<DEPMERGE,DEPRECATED DESCRIPTION>> section at the end, the rest of ++this documentation describes modern --write-tree mode. ++
+Performs a merge, but does not make any new commits and does not read +from or write to either the working tree or index.
+
-+The first form will merge the two branches, doing a real merge. A real -+merge is distinguished from a trivial merge in that it includes:
++The performed merge will use the same feature as the "real" ++linkgit:git-merge[1], including:
+
+ * three way content merges of individual files
+ * rename detection
@@ Documentation/git-merge-tree.txt: git-merge-tree(1)
+ merge base, creating a virtual merge base by merging the merge bases)
+ * etc.
+
-+After the merge completes, the first form will create a new toplevel -+tree object. See OUTPUT below for details.
-+
-+The second form is deprecated; it is kept for backward compatibility -+reasons but may be deleted in the future. Other than the optional -+--trivial-merge, it accepts no options. It can only do a trivial -+merge. It reads three tree-ish, and outputs trivial merge results and -+conflicting stages to the standard output in a semi-diff format. -+Since this was designed for higher level scripts to consume and merge -+the results back into the index, it omits entries that match -+<branch1>. The result of this second form is similar to what -+three-way 'git read-tree -m' does, but instead of storing the results -+in the index, the command outputs the entries to the standard output. -+This form not only has limited applicability, the output format is -+also difficult to work with, and it will generally be less performant -+than the first form even on successful merges (especially if working -+in large repositories). The remainder of this manual will only -+discuss the first form.
++After the merge completes, a new toplevel tree object is created. See ++OUTPUT below for details.
+
++[[OUTPUT]]
+OUTPUT
+------
+
@@ Documentation/git-merge-tree.txt: git-merge-tree(1)
+USAGE NOTES
+-----------
+
-+git-merge-tree was written to be low-level plumbing, similar to -+hash-object, mktree, commit-tree, write-tree, update-ref, and mktag. -+Thus, it could be used as a part of a series of steps such as ++This command is intended as low-level plumbing, similar to ++linkgit:git-hash-object[1], linkgit:git-mktree[1], ++linkgit:git-commit-tree[1], linkgit:git-write-tree[1], ++linkgit:git-update-ref[1], and linkgit:git-mktag[1]. Thus, it can be ++used as a part of a series of steps such as:
+
+ NEWTREE=$(git merge-tree --write-tree $BRANCH1 $BRANCH2)
+ test $? -eq 0 || die "There were conflicts..."
+ NEWCOMMIT=$(git commit-tree $NEWTREE -p $BRANCH1 -p $BRANCH2)
+ git update-ref $BRANCH1 $NEWCOMMIT
++
++[[DEPMERGE]]
++DEPRECATED DESCRIPTION
++----------------------
++
++Per the <<NEWMERGE,DESCRIPTION>> and unlike the rest of this ++documentation, this section describes the deprecated --trivial-merge ++mode.
++
++Other than the optional --trivial-merge, this mode accepts no ++options.
++
++This mode reads three tree-ish, and outputs trivial merge results and ++conflicting stages to the standard output in a semi-diff format. ++Since this was designed for higher level scripts to consume and merge ++the results back into the index, it omits entries that match ++<branch1>. The result of this second form is similar to what ++three-way 'git read-tree -m' does, but instead of storing the results ++in the index, the command outputs the entries to the standard output. ++
++This form not only has limited applicability (a trivial merge cannot ++handle content merges of individual files, rename detection, proper ++directory/file conflict handling, etc.), the output format is also ++difficult to work with, and it will generally be less performant than ++the first form even on successful merges (especially if working in ++large repositories).

GIT
---
@@ builtin/merge-tree.c: struct merge_tree_options {
{
- die(_("real merges are not yet implemented"));
+ struct commit *parent1, *parent2;
-+ struct commit_list *common;
+ struct commit_list *merge_bases = NULL;
-+ struct commit_list *j;
+ struct merge_options opt;
+ struct merge_result result = { 0 };
+
@@ builtin/merge-tree.c: struct merge_tree_options {
+ * Get the merge bases, in reverse order; see comment above
+ * merge_incore_recursive in merge-ort.h
+ */
-+ common = get_merge_bases(parent1, parent2);
-+ if (!common)
++ merge_bases = get_merge_bases(parent1, parent2);
++ if (!merge_bases)
+ die(_("refusing to merge unrelated histories"));
-+ for (j = common; j; j = j->next)
-+ commit_list_insert(j->item, &merge_bases);
++ merge_bases = reverse_commit_list(merge_bases);
+
+ merge_incore_recursive(&opt, merge_bases, parent1, parent2, &result);
+ if (result.clean < 0)

  5:  e0f95e094cf =  5:  6629af14919 merge-ort: split out a separate display_update_messages() function
  6:  90c4adecb23 !  6:  17b57efb714 merge-tree: support including merge messages in output

@@ Documentation/git-merge-tree.txt: git-merge-tree - Perform merge without touchin
+'git merge-tree' [--write-tree] [<options>] <branch1> <branch2>
'git merge-tree' [--trivial-merge] <base-tree> <branch1> <branch2> (deprecated)

- DESCRIPTION
-@@ Documentation/git-merge-tree.txt: than the first form even on successful merges (especially if working
- in large repositories). The remainder of this manual will only
- discuss the first form.
+ [[NEWMERGE]]
+@@ Documentation/git-merge-tree.txt: linkgit:git-merge[1], including:
+ After the merge completes, a new toplevel tree object is created. See
+ OUTPUT below for details.

+OPTIONS
+-------
@@ Documentation/git-merge-tree.txt: than the first form even on successful merges
+ default is to include these messages if there are merge
+ conflicts, and to omit them otherwise.
+
+ [[OUTPUT]]
OUTPUT
------

-For either a successful or conflicted merge, the output from -git-merge-tree is simply one line:
-+By default, for a successful merge, the output from git-merge-tree is -+simply one line:
++For a successful merge, the output from git-merge-tree is simply one ++line:
+
+ <OID of toplevel tree>
+
@@ Documentation/git-merge-tree.txt: than the first form even on successful merges
-The printed tree object corresponds to what would be checked out in -the working tree at the end of git merge, and thus may have files -with conflict markers in them.
++[[OIDTLT]]
+OID of toplevel tree
+~~~~~~~~~~~~~~~~~~~~
+
@@ Documentation/git-merge-tree.txt: than the first form even on successful merges
+working tree at the end of git merge. If there were conflicts, then +files within this tree may have embedded conflict markers.
+
++[[IM]]
+Informational messages
+~~~~~~~~~~~~~~~~~~~~~~
+
@@ Documentation/git-merge-tree.txt: than the first form even on successful merges

EXIT STATUS
-----------
-@@ Documentation/git-merge-tree.txt: Thus, it could be used as a part of a series of steps such as +@@ Documentation/git-merge-tree.txt: used as a part of a series of steps such as:
NEWCOMMIT=$(git commit-tree $NEWTREE -p $BRANCH1 -p $BRANCH2)
git update-ref $BRANCH1 $NEWCOMMIT

-+Note that when the exit status is non-zero, NEWTREE in this sequence ++Note that when the exit status is non-zero, NEWTREE in this sequence
+will contain a lot more output than just a tree.
+
- GIT
- ---
- Part of the linkgit:git[1] suite
+ [[DEPMERGE]]
+ DEPRECATED DESCRIPTION
+ ----------------------

## builtin/merge-tree.c ##
@@ builtin/merge-tree.c: enum mode {

  7:  12e2351092a =  7:  4c8f42372dd merge-ort: provide a merge_get_conflicted_files() helper function
  8:  5bb7d3725ad !  8:  7b1ee417f3d merge-tree: provide a list of which files have conflicts

@@ Commit message

## Documentation/git-merge-tree.txt ##
-@@ Documentation/git-merge-tree.txt: simply one line:
+@@ Documentation/git-merge-tree.txt: line:
Whereas for a conflicted merge, the output is by default of the form:

<OID of toplevel tree>
@@ Documentation/git-merge-tree.txt: This is a tree object that represents what wou
working tree at the end of git merge. If there were conflicts, then files within this tree may have embedded conflict markers.

++[[CFI]]
+Conflicted file list
+~~~~~~~~~~~~~~~~~~~~
+
@@ Documentation/git-merge-tree.txt: This is a tree object that represents what wou
+as explained for the configuration variable core.quotePath (see +linkgit:git-config[1]).
+
+ [[IM]]
Informational messages
~~~~~~~~~~~~~~~~~~~~~~
-

## builtin/merge-tree.c ##
@@
@@ builtin/merge-tree.c: struct merge_tree_options {
+ const char *prefix)
{
struct commit *parent1, *parent2;
- struct commit_list *common;
+ struct commit_list *merge_bases = NULL;
@@ builtin/merge-tree.c: static int real_merge(struct merge_tree_options *o,
o->show_messages = !result.clean;

9: 3c2ca198cec ! 9: f1231a8fbc8 merge-tree: provide easy access to ls-files -u style info
@@ Commit message

## Documentation/git-merge-tree.txt ##
-@@ Documentation/git-merge-tree.txt: discuss the first form.
+@@ Documentation/git-merge-tree.txt: After the merge completes, a new toplevel tree object is created. See
OPTIONS
-------

@@ Documentation/git-merge-tree.txt: discuss the first form.
--[no-]messages::
Write any informational messages such as "Auto-merging <path>" or CONFLICT notices to the end of stdout. If unspecified, the
-@@ Documentation/git-merge-tree.txt: simply one line:
+@@ Documentation/git-merge-tree.txt: line:
Whereas for a conflicted merge, the output is by default of the form:

<OID of toplevel tree>
@@ Documentation/git-merge-tree.txt: simply one line:
<Informational messages>

These are discussed individually below.
-@@ Documentation/git-merge-tree.txt: This is a tree object that represents what would be checked out in the
- working tree at the end of git merge. If there were conflicts, then
+@@ Documentation/git-merge-tree.txt: working tree at the end of git merge. If there were conflicts, then
files within this tree may have embedded conflict markers.

+ [[CFI]]
-Conflicted file list
+Conflicted file info
~~~~~~~~~~~~~~~~~~~~
@@ Documentation/git-merge-tree.txt: This is a tree object that represents what wou
+the --name-only option is passed, the mode, object, and stage will +be omitted.

+ [[IM]]
Informational messages
~~~~~~~~~~~~~~~~~~~~~~

@@ Documentation/git-merge-tree.txt: This is a tree object that represents what wou

* "Auto-merging <file>"
* "CONFLICT (rename/delete): <oldfile> renamed...but deleted in..."
-@@ Documentation/git-merge-tree.txt: Thus, it could be used as a part of a series of steps such as
- Note that when the exit status is non-zero, NEWTREE in this sequence
+@@ Documentation/git-merge-tree.txt: used as a part of a series of steps such as:
+ Note that when the exit status is non-zero, NEWTREE in this sequence
will contain a lot more output than just a tree.

-+git-merge-tree was written to provide users with the same information -+that they'd have access to if using git merge:
-+ * what would be written to the working tree (the <OID of toplevel tree>) ++For conflicts, the output includes the same information that you'd get ++with linkgit:git-merge[1]:
++
++ * what would be written to the working tree (the
++ <<OIDTLT,OID of toplevel tree>>)
+ * the higher order stages that would be written to the index (the
-+ <Conflicted file info>)
-+ * any messages that would have been printed to stdout (the <Informational
-+ messages>)
++ <<CFI,Conflicted file info>>)
++ * any messages that would have been printed to stdout (the
++ <<IM,Informational messages>>)
+
- GIT
- ---
- Part of the linkgit:git[1] suite
+ [[DEPMERGE]]
+ DEPRECATED DESCRIPTION
+ ----------------------

## builtin/merge-tree.c ##
@@ builtin/merge-tree.c: enum mode {
10: 6e89e17693a ! 10: 22297e6ce75 merge-tree: allow ls-files -u style info to be NUL terminated
@@ Commit message

## Documentation/git-merge-tree.txt ##
-@@ Documentation/git-merge-tree.txt: discuss the first form.
+@@ Documentation/git-merge-tree.txt: After the merge completes, a new toplevel tree object is created. See
OPTIONS
-------

@@ Documentation/git-merge-tree.txt: discuss the first form.
+ Do not quote filenames in the <Conflicted file info> section,
+ and end each filename with a NUL character rather than
+ newline. Also begin the messages section with a NUL character
-+ instead of a newline. See OUTPUT below for more information.
++ instead of a newline. See <<OUTPUT>> below for more information.
+
--name-only::
In the Conflicted file info section, instead of writing a list
@@ Documentation/git-merge-tree.txt: OID of toplevel tree
+files within this tree may have embedded conflict markers. This section +is always followed by a newline (or NUL if -z is passed).

+ [[CFI]]
Conflicted file info
- ~~~~~~~~~~~~~~~~~~~~
@@ Documentation/git-merge-tree.txt: This is a sequence of lines with the format
The filename will be quoted as explained for the configuration variable core.quotePath (see linkgit:git-config[1]). However, if
@@ Documentation/git-merge-tree.txt: This is a sequence of lines with the format
+be omitted. If -z is passed, the "lines" are terminated by a NUL +character instead of a newline character.

+ [[IM]]
Informational messages
~~~~~~~~~~~~~~~~~~~~~~

11: 6ddd5ffde9c ! 11: db73c6dd823 merge-tree: add a --allow-unrelated-histories flag

     @@ Documentation/git-merge-tree.txt: OPTIONS
      +	share no common history.  This flag can be given to override that
      +	check and make the merge proceed anyway.
      +
     + [[OUTPUT]]
       OUTPUT
       ------
     - 
      
       ## builtin/merge-tree.c ##
      @@ builtin/merge-tree.c: enum mode {
     @@ builtin/merge-tree.c: enum mode {
      @@ builtin/merge-tree.c: static int real_merge(struct merge_tree_options *o,
       	 * merge_incore_recursive in merge-ort.h
       	 */
     - 	common = get_merge_bases(parent1, parent2);
     --	if (!common)
     -+	if (!common && !o->allow_unrelated_histories)
     + 	merge_bases = get_merge_bases(parent1, parent2);
     +-	if (!merge_bases)
     ++	if (!merge_bases && !o->allow_unrelated_histories)
       		die(_("refusing to merge unrelated histories"));
     - 	for (j = common; j; j = j->next)
     - 		commit_list_insert(j->item, &merge_bases);
     + 	merge_bases = reverse_commit_list(merge_bases);
     + 
      @@ builtin/merge-tree.c: int cmd_merge_tree(int argc, const char **argv, const char *prefix)
       			   &o.name_only,
       			   N_("list filenames without modes/oids/stages"),

12: 7abf633b638 ! 12: d58a7c7a9f6 git-merge-tree.txt: add a section on potentional usage mistakes
@@ Commit message

## Documentation/git-merge-tree.txt ##
-@@ Documentation/git-merge-tree.txt: that they'd have access to if using git merge:
- * any messages that would have been printed to stdout (the <Informational
- messages>)
+@@ Documentation/git-merge-tree.txt: with linkgit:git-merge[1]:
+ * any messages that would have been printed to stdout (the
+ <<IM,Informational messages>>)

+MISTAKES TO AVOID
+-----------------
+
+Do NOT look through the resulting toplevel tree to try to find which
-+files conflict; parse the <Conflicted file info> section instead. Not -+only would parsing an entire tree be horrendously slow in large ++files conflict; parse the <<CFI,Conflicted file info>> section instead. ++Not only would parsing an entire tree be horrendously slow in large
+repositories, there are numerous types of conflicts not representable by +conflict markers (modify/delete, mode conflict, binary file changed on +both sides, file/directory conflicts, various rename conflict +permutations, etc.)
+
-+Do NOT interpret an empty <Conflicted file info> list as a clean merge; -+check the exit status. A merge can have conflicts without having ++Do NOT interpret an empty <<CFI,Conflicted file info>> list as a clean ++merge; check the exit status. A merge can have conflicts without having
+individual files conflict (there are a few types of directory rename +conflicts that fall into this category, and others might also be added +in the future).
+
+Do NOT attempt to guess or make the user guess the conflict types from
-+the <Conflicted file info> list. The information there is insufficient -+to do so. For example: Rename/rename(1to2) conflicts (both sides -+renamed the same file differently) will result in three different file -+having higher order stages (but each only has one higher order stage), -+with no way (short of the <Informational messages> section) to determine -+which three files are related. File/directory conflicts also result in -+a file with exactly one higher order stage.
++the <<CFI,Conflicted file info>> list. The information there is ++insufficient to do so. For example: Rename/rename(1to2) conflicts (both ++sides renamed the same file differently) will result in three different ++file having higher order stages (but each only has one higher order ++stage), with no way (short of the <<IM,Informational messages>> section) ++to determine which three files are related. File/directory conflicts ++also result in a file with exactly one higher order stage.
+Possibly-involved-in-directory-rename conflicts (when +"merge.directoryRenames" is unset or set to "conflicts") also result in +a file with exactly one higher order stage. In all cases, the
-+<Informational messages> section has the necessary info, though it is -+not designed to be machine parseable.
++<<IM,Informational messages>> section has the necessary info, though it ++is not designed to be machine parseable.
+
-+Do NOT assume all filenames listed in the <Informational messages> ++Do NOT assume all filenames listed in the <<IM,Informational messages>>
+section had conflicts. Messages can be included for files that have no +conflicts, such as "Auto-merging <file>".
+
-+AVOID taking the OIDS from the <Conflicted file info> and re-merging -+them to present the conflicts to the user. This will lose information. -+Instead, look up the version of the file found within the <OID of -+toplevel tree> and show that instead. In particular, the latter will -+have conflict markers annotated with the original branch/commit being -+merged and, if renames were involved, the original filename. While you -+could include the original branch/commit in the conflict marker -+annotations when re-merging, the original filename is not available from -+the <Conflicted file info> and thus you would be losing information that -+might help the user resolve the conflict.
++AVOID taking the OIDS from the <<CFI,Conflicted file info>> and ++re-merging them to present the conflicts to the user. This will lose ++information. Instead, look up the version of the file found within the ++<<OIDTLT,OID of toplevel tree>> and show that instead. In particular, ++the latter will have conflict markers annotated with the original ++branch/commit being merged and, if renames were involved, the original ++filename. While you could include the original branch/commit in the ++conflict marker annotations when re-merging, the original filename is ++not available from the <<CFI,Conflicted file info>> and thus you would ++be losing information that might help the user resolve the conflict.
+
- GIT
- ---
- Part of the linkgit:git[1] suite
+ [[DEPMERGE]]
+ DEPRECATED DESCRIPTION
+ ----------------------

Signature · 1 line
gitgitgadget
#210 lore raw

PATCH v6 1/12 merge-tree: rename merge_trees() to trivial_merge_trees()

From: Elijah Newren <[email protected]>

merge-recursive.h defined its own merge_trees() function, different than the one found in builtin/merge-tree.c. That was okay in the past, but we want merge-tree to be able to use the merge-ort functions, which will end up including merge-recursive.h. Rename the function found in builtin/merge-tree.c to avoid the conflict.

---
 builtin/merge-tree.c | 8 ++++----
 1 file changed, 4 insertions(+), 4 deletions(-)
builtin/merge-tree.c +4 -4
index 5dc94d6f880..06f9eee9f78 100644
@@ -28,7 +28,7 @@ static void add_merge_entry(struct merge_list *entry)
 	merge_result_end = &entry->next;
 }
 
-static void merge_trees(struct tree_desc t[3], const char *base);
+static void trivial_merge_trees(struct tree_desc t[3], const char *base);
 
 static const char *explanation(struct merge_list *entry)
 {
@@ -225,7 +225,7 @@ static void unresolved_directory(const struct traverse_info *info,
 	buf2 = fill_tree_descriptor(r, t + 2, ENTRY_OID(n + 2));
 #undef ENTRY_OID
 
-	merge_trees(t, newbase);
+	trivial_merge_trees(t, newbase);
 
 	free(buf0);
 	free(buf1);
@@ -342,7 +342,7 @@ static int threeway_callback(int n, unsigned long mask, unsigned long dirmask, s
 	return mask;
 }
 
-static void merge_trees(struct tree_desc t[3], const char *base)
+static void trivial_merge_trees(struct tree_desc t[3], const char *base)
 {
 	struct traverse_info info;
 
@@ -378,7 +378,7 @@ int cmd_merge_tree(int argc, const char **argv, const char *prefix)
 	buf1 = get_tree_descriptor(r, t+0, argv[1]);
 	buf2 = get_tree_descriptor(r, t+1, argv[2]);
 	buf3 = get_tree_descriptor(r, t+2, argv[3]);
-	merge_trees(t, "");
+	trivial_merge_trees(t, "");
 	free(buf1);
 	free(buf2);
 	free(buf3);
Signature · 1 line
gitgitgadget
#211 lore raw

PATCH v6 2/12 merge-tree: move logic for existing merge into new function

In reply to Elijah Newren via GitGitGadget

From: Elijah Newren <[email protected]>

In preparation for adding a non-trivial merge capability to merge-tree, move the existing merge logic for trivial merges into a new function.

---
 builtin/merge-tree.c | 12 ++++++++----
 1 file changed, 8 insertions(+), 4 deletions(-)
builtin/merge-tree.c +8 -4
index 06f9eee9f78..914ec960b7e 100644
@@ -366,15 +366,12 @@ static void *get_tree_descriptor(struct repository *r,
 	return buf;
 }
 
-int cmd_merge_tree(int argc, const char **argv, const char *prefix)
+static int trivial_merge(int argc, const char **argv)
 {
 	struct repository *r = the_repository;
 	struct tree_desc t[3];
 	void *buf1, *buf2, *buf3;
 
-	if (argc != 4)
-		usage(merge_tree_usage);
-
 	buf1 = get_tree_descriptor(r, t+0, argv[1]);
 	buf2 = get_tree_descriptor(r, t+1, argv[2]);
 	buf3 = get_tree_descriptor(r, t+2, argv[3]);
@@ -386,3 +383,10 @@ int cmd_merge_tree(int argc, const char **argv, const char *prefix)
 	show_result();
 	return 0;
 }
+
+int cmd_merge_tree(int argc, const char **argv, const char *prefix)
+{
+	if (argc != 4)
+		usage(merge_tree_usage);
+	return trivial_merge(argc, argv);
+}
Signature · 1 line
gitgitgadget
#212 lore raw

PATCH v6 3/12 merge-tree: add option parsing and initial shell for real merge function

In reply to Elijah Newren via GitGitGadget

From: Elijah Newren <[email protected]>

Let merge-tree accept a --write-tree parameter for choosing real merges instead of trivial merges, and accept an optional --trivial-merge option to get the traditional behavior. Note that these accept different numbers of arguments, though, so these names need not actually be used.

Note that real merges differ from trivial merges in that they handle:
- three way content merges
- recursive ancestor consolidation
- renames
- proper directory/file conflict handling
- etc.
Basically all the stuff you'd expect from git merge, just without updating the index and working tree. The initial shell added here does nothing more than die with "real merges are not yet implemented", but that will be fixed in subsequent commits.

---
 builtin/merge-tree.c | 84 +++++++++++++++++++++++++++++++++++++++-----
 git.c                |  2 +-
 2 files changed, 76 insertions(+), 10 deletions(-)
builtin/merge-tree.c +75 -9
index 914ec960b7e..0f9d928e862 100644
@@ -3,13 +3,12 @@
 #include "tree-walk.h"
 #include "xdiff-interface.h"
 #include "object-store.h"
+#include "parse-options.h"
 #include "repository.h"
 #include "blob.h"
 #include "exec-cmd.h"
 #include "merge-blobs.h"
 
-static const char merge_tree_usage[] = "git merge-tree <base-tree> <branch1> <branch2>";
-
 struct merge_list {
 	struct merge_list *next;
 	struct merge_list *link;	/* other stages for this object */
@@ -366,15 +365,17 @@ static void *get_tree_descriptor(struct repository *r,
 	return buf;
 }
 
-static int trivial_merge(int argc, const char **argv)
+static int trivial_merge(const char *base,
+			 const char *branch1,
+			 const char *branch2)
 {
 	struct repository *r = the_repository;
 	struct tree_desc t[3];
 	void *buf1, *buf2, *buf3;
 
-	buf1 = get_tree_descriptor(r, t+0, argv[1]);
-	buf2 = get_tree_descriptor(r, t+1, argv[2]);
-	buf3 = get_tree_descriptor(r, t+2, argv[3]);
+	buf1 = get_tree_descriptor(r, t+0, base);
+	buf2 = get_tree_descriptor(r, t+1, branch1);
+	buf3 = get_tree_descriptor(r, t+2, branch2);
 	trivial_merge_trees(t, "");
 	free(buf1);
 	free(buf2);
@@ -384,9 +385,74 @@ static int trivial_merge(int argc, const char **argv)
 	return 0;
 }
 
+enum mode {
+	MODE_UNKNOWN,
+	MODE_TRIVIAL,
+	MODE_REAL,
+};
+
+struct merge_tree_options {
+	int mode;
+};
+
+static int real_merge(struct merge_tree_options *o,
+		      const char *branch1, const char *branch2)
+{
+	die(_("real merges are not yet implemented"));
+}
+
 int cmd_merge_tree(int argc, const char **argv, const char *prefix)
 {
-	if (argc != 4)
-		usage(merge_tree_usage);
-	return trivial_merge(argc, argv);
+	struct merge_tree_options o = { 0 };
+	int expected_remaining_argc;
+
+	const char * const merge_tree_usage[] = {
+		N_("git merge-tree [--write-tree] <branch1> <branch2>"),
+		N_("git merge-tree [--trivial-merge] <base-tree> <branch1> <branch2>"),
+		NULL
+	};
+	struct option mt_options[] = {
+		OPT_CMDMODE(0, "write-tree", &o.mode,
+			    N_("do a real merge instead of a trivial merge"),
+			    MODE_REAL),
+		OPT_CMDMODE(0, "trivial-merge", &o.mode,
+			    N_("do a trivial merge only"), MODE_TRIVIAL),
+		OPT_END()
+	};
+
+	/* Parse arguments */
+	argc = parse_options(argc, argv, prefix, mt_options,
+			     merge_tree_usage, PARSE_OPT_STOP_AT_NON_OPTION);
+	switch (o.mode) {
+	default:
+		BUG("unexpected command mode %d", o.mode);
+	case MODE_UNKNOWN:
+		switch (argc) {
+		default:
+			usage_with_options(merge_tree_usage, mt_options);
+		case 2:
+			o.mode = MODE_REAL;
+			break;
+		case 3:
+			o.mode = MODE_TRIVIAL;
+			break;
+		}
+		expected_remaining_argc = argc;
+		break;
+	case MODE_REAL:
+		expected_remaining_argc = 2;
+		break;
+	case MODE_TRIVIAL:
+		expected_remaining_argc = 3;
+		break;
+	}
+
+	if (argc != expected_remaining_argc)
+		usage_with_options(merge_tree_usage, mt_options);
+
+	/* Do the relevant type of merge */
+	if (o.mode == MODE_REAL)
+		return real_merge(&o, argv[0], argv[1]);
+	else
+		return trivial_merge(argv[0], argv[1], argv[2]);
 }
git.c +1 -1
index 5ff21be21f3..6090a1289db 100644
@@ -558,7 +558,7 @@ static struct cmd_struct commands[] = {
 	{ "merge-recursive-ours", cmd_merge_recursive, RUN_SETUP | NEED_WORK_TREE | NO_PARSEOPT },
 	{ "merge-recursive-theirs", cmd_merge_recursive, RUN_SETUP | NEED_WORK_TREE | NO_PARSEOPT },
 	{ "merge-subtree", cmd_merge_recursive, RUN_SETUP | NEED_WORK_TREE | NO_PARSEOPT },
-	{ "merge-tree", cmd_merge_tree, RUN_SETUP | NO_PARSEOPT },
+	{ "merge-tree", cmd_merge_tree, RUN_SETUP },
 	{ "mktag", cmd_mktag, RUN_SETUP | NO_PARSEOPT },
 	{ "mktree", cmd_mktree, RUN_SETUP },
 	{ "multi-pack-index", cmd_multi_pack_index, RUN_SETUP },
Signature · 1 line
gitgitgadget
#213 lore raw

PATCH v6 4/12 merge-tree: implement real merges

In reply to Elijah Newren via GitGitGadget

From: Elijah Newren <[email protected]>

This adds the ability to perform real merges rather than just trivial merges (meaning handling three way content merges, recursive ancestor consolidation, renames, proper directory/file conflict handling, and so forth). However, unlike git merge, the working tree and index are left alone and no branch is updated.

The only output is:
- the toplevel resulting tree printed on stdout
- exit status of 0 (clean), 1 (conflicts present), anything else (merge could not be performed; unknown if clean or conflicted)

This output is meant to be used by some higher level script, perhaps in a sequence of steps like this:

   NEWTREE=$(git merge-tree --write-tree $BRANCH1 $BRANCH2)
   test $? -eq 0 || die "There were conflicts..."
   NEWCOMMIT=$(git commit-tree $NEWTREE -p $BRANCH1 -p $BRANCH2)
   git update-ref $BRANCH1 $NEWCOMMIT

Note that higher level scripts may also want to access the conflict/warning messages normally output during a merge, or have quick access to a list of files with conflicts. That is not available in this preliminary implementation, but subsequent commits will add that ability (meaning that NEWTREE would be a lot more than a tree in the case of conflicts).

This also marks the traditional trivial merge of merge-tree as deprecated. The trivial merge not only had limited applicability, the output format was also difficult to work with (and its format undocumented), and will generally be less performant than real merges.

---
 Documentation/git-merge-tree.txt |  98 ++++++++++++++++++++++++----
 builtin/merge-tree.c             |  41 +++++++++++-
 t/t4301-merge-tree-write-tree.sh | 106 +++++++++++++++++++++++++++++++
 3 files changed, 232 insertions(+), 13 deletions(-)
 create mode 100755 t/t4301-merge-tree-write-tree.sh
Documentation/git-merge-tree.txt +86 -12
index 58731c19422..2a9c91328de 100644
@@ -3,26 +3,100 @@ git-merge-tree(1)
 
 NAME
 ----
-git-merge-tree - Show three-way merge without touching index
+git-merge-tree - Perform merge without touching index or working tree
 
 
 SYNOPSIS
 --------
 [verse]
-'git merge-tree' <base-tree> <branch1> <branch2>
+'git merge-tree' [--write-tree] <branch1> <branch2>
+'git merge-tree' [--trivial-merge] <base-tree> <branch1> <branch2> (deprecated)
 
+[[NEWMERGE]]
 DESCRIPTION
 -----------
-Reads three tree-ish, and output trivial merge results and
-conflicting stages to the standard output.  This is similar to
-what three-way 'git read-tree -m' does, but instead of storing the
-results in the index, the command outputs the entries to the
-standard output.
-
-This is meant to be used by higher level scripts to compute
-merge results outside of the index, and stuff the results back into the
-index.  For this reason, the output from the command omits
-entries that match the <branch1> tree.
+
+This command has a modern `--write-tree` mode and a deprecated
+`--trivial-merge` mode.  With the exception of the
+<<DEPMERGE,DEPRECATED DESCRIPTION>> section at the end, the rest of
+this documentation describes modern `--write-tree` mode.
+
+Performs a merge, but does not make any new commits and does not read
+from or write to either the working tree or index.
+
+The performed merge will use the same feature as the "real"
+linkgit:git-merge[1], including:
+
+  * three way content merges of individual files
+  * rename detection
+  * proper directory/file conflict handling
+  * recursive ancestor consolidation (i.e. when there is more than one
+    merge base, creating a virtual merge base by merging the merge bases)
+  * etc.
+
+After the merge completes, a new toplevel tree object is created.  See
+`OUTPUT` below for details.
+
+[[OUTPUT]]
+OUTPUT
+------
+
+For either a successful or conflicted merge, the output from
+git-merge-tree is simply one line:
+
+	<OID of toplevel tree>
+
+The printed tree object corresponds to what would be checked out in
+the working tree at the end of `git merge`, and thus may have files
+with conflict markers in them.
+
+EXIT STATUS
+-----------
+
+For a successful, non-conflicted merge, the exit status is 0.  When the
+merge has conflicts, the exit status is 1.  If the merge is not able to
+complete (or start) due to some kind of error, the exit status is
+something other than 0 or 1 (and the output is unspecified).
+
+USAGE NOTES
+-----------
+
+This command is intended as low-level plumbing, similar to
+linkgit:git-hash-object[1], linkgit:git-mktree[1],
+linkgit:git-commit-tree[1], linkgit:git-write-tree[1],
+linkgit:git-update-ref[1], and linkgit:git-mktag[1].  Thus, it can be
+used as a part of a series of steps such as:
+
+       NEWTREE=$(git merge-tree --write-tree $BRANCH1 $BRANCH2)
+       test $? -eq 0 || die "There were conflicts..."
+       NEWCOMMIT=$(git commit-tree $NEWTREE -p $BRANCH1 -p $BRANCH2)
+       git update-ref $BRANCH1 $NEWCOMMIT
+
+[[DEPMERGE]]
+DEPRECATED DESCRIPTION
+----------------------
+
+Per the <<NEWMERGE,DESCRIPTION>> and unlike the rest of this
+documentation, this section describes the deprecated `--trivial-merge`
+mode.
+
+Other than the optional `--trivial-merge`, this mode accepts no
+options.
+
+This mode reads three tree-ish, and outputs trivial merge results and
+conflicting stages to the standard output in a semi-diff format.
+Since this was designed for higher level scripts to consume and merge
+the results back into the index, it omits entries that match
+<branch1>.  The result of this second form is similar to what
+three-way 'git read-tree -m' does, but instead of storing the results
+in the index, the command outputs the entries to the standard output.
+
+This form not only has limited applicability (a trivial merge cannot
+handle content merges of individual files, rename detection, proper
+directory/file conflict handling, etc.), the output format is also
+difficult to work with, and it will generally be less performant than
+the first form even on successful merges (especially if working in
+large repositories).
 
 GIT
 ---
builtin/merge-tree.c +40 -1

53 lines not on this page · raw message

t/t4301-merge-tree-write-tree.sh new file +106 -0

106 lines not on this page · raw message

Signature · 1 line
gitgitgadget
#214 lore raw

PATCH v6 5/12 merge-ort: split out a separate display_update_messages() function

In reply to Elijah Newren via GitGitGadget

From: Elijah Newren <[email protected]>

This patch includes no new code; it simply moves a bunch of lines into a new function. As such, there are no functional changes. This is just a preparatory step to allow the printed messages to be handled differently by other callers, such as in git merge-tree --write-tree.

(Patch best viewed with

     --color-moved --color-moved-ws=allow-indentation-change

to see that it is a simple code movement.)

---
 merge-ort.c | 78 ++++++++++++++++++++++++++++-------------------------
 merge-ort.h |  8 ++++++
 2 files changed, 49 insertions(+), 37 deletions(-)
merge-ort.c +41 -37
index 9bf15a01db8..ebaed98d53a 100644
@@ -4235,6 +4235,45 @@ static int record_conflicted_index_entries(struct merge_options *opt)
 	return errs;
 }
 
+void merge_display_update_messages(struct merge_options *opt,
+				   struct merge_result *result)
+{
+	struct merge_options_internal *opti = result->priv;
+	struct hashmap_iter iter;
+	struct strmap_entry *e;
+	struct string_list olist = STRING_LIST_INIT_NODUP;
+	int i;
+
+	if (opt->record_conflict_msgs_as_headers)
+		BUG("Either display conflict messages or record them as headers, not both");
+
+	trace2_region_enter("merge", "display messages", opt->repo);
+
+	/* Hack to pre-allocate olist to the desired size */
+	ALLOC_GROW(olist.items, strmap_get_size(&opti->output),
+		   olist.alloc);
+
+	/* Put every entry from output into olist, then sort */
+	strmap_for_each_entry(&opti->output, &iter, e) {
+		string_list_append(&olist, e->key)->util = e->value;
+	}
+	string_list_sort(&olist);
+
+	/* Iterate over the items, printing them */
+	for (i = 0; i < olist.nr; ++i) {
+		struct strbuf *sb = olist.items[i].util;
+
+		printf("%s", sb->buf);
+	}
+	string_list_clear(&olist, 0);
+
+	/* Also include needed rename limit adjustment now */
+	diff_warn_rename_limit("merge.renamelimit",
+			       opti->renames.needed_limit, 0);
+
+	trace2_region_leave("merge", "display messages", opt->repo);
+}
+
 void merge_switch_to_result(struct merge_options *opt,
 			    struct tree *head,
 			    struct merge_result *result,
@@ -4272,43 +4311,8 @@ void merge_switch_to_result(struct merge_options *opt,
 		fclose(fp);
 		trace2_region_leave("merge", "write_auto_merge", opt->repo);
 	}
-
-	if (display_update_msgs) {
-		struct merge_options_internal *opti = result->priv;
-		struct hashmap_iter iter;
-		struct strmap_entry *e;
-		struct string_list olist = STRING_LIST_INIT_NODUP;
-		int i;
-
-		if (opt->record_conflict_msgs_as_headers)
-			BUG("Either display conflict messages or record them as headers, not both");
-
-		trace2_region_enter("merge", "display messages", opt->repo);
-
-		/* Hack to pre-allocate olist to the desired size */
-		ALLOC_GROW(olist.items, strmap_get_size(&opti->output),
-			   olist.alloc);
-
-		/* Put every entry from output into olist, then sort */
-		strmap_for_each_entry(&opti->output, &iter, e) {
-			string_list_append(&olist, e->key)->util = e->value;
-		}
-		string_list_sort(&olist);
-
-		/* Iterate over the items, printing them */
-		for (i = 0; i < olist.nr; ++i) {
-			struct strbuf *sb = olist.items[i].util;
-
-			printf("%s", sb->buf);
-		}
-		string_list_clear(&olist, 0);
-
-		/* Also include needed rename limit adjustment now */
-		diff_warn_rename_limit("merge.renamelimit",
-				       opti->renames.needed_limit, 0);
-
-		trace2_region_leave("merge", "display messages", opt->repo);
-	}
+	if (display_update_msgs)
+		merge_display_update_messages(opt, result);
 
 	merge_finalize(opt, result);
 }
merge-ort.h +8 -0
index fe599b87868..e5aec45b18f 100644
@@ -80,6 +80,14 @@ void merge_switch_to_result(struct merge_options *opt,
 			    int update_worktree_and_index,
 			    int display_update_msgs);
 
+/*
+ * Display messages about conflicts and which files were 3-way merged.
+ * Automatically called by merge_switch_to_result() with stream == stdout,
+ * so only call this when bypassing merge_switch_to_result().
+ */
+void merge_display_update_messages(struct merge_options *opt,
+				   struct merge_result *result);
+
 /* Do needed cleanup when not calling merge_switch_to_result() */
 void merge_finalize(struct merge_options *opt,
 		    struct merge_result *result);
Signature · 1 line
gitgitgadget
#215 lore raw

PATCH v6 6/12 merge-tree: support including merge messages in output

In reply to Elijah Newren via GitGitGadget

From: Elijah Newren <[email protected]>

When running git merge-tree --write-tree, we previously would only return an exit status reflecting the cleanness of a merge, and print out the toplevel tree of the resulting merge. Merges also have informational messages, such as:
* "Auto-merging <PATH>"
* "CONFLICT (content): ..."
* "CONFLICT (file/directory)"
* etc.
In fact, when non-content conflicts occur (such as file/directory, modify/delete, add/add with differing modes, rename/rename (1to2), etc.), these informational messages may be the only notification the user gets since these conflicts are not representable in the contents of the file.

Add a --[no-]messages option so that callers can request these messages be included at the end of the output. Include such messages by default when there are conflicts, and omit them by default when the merge is clean.

---
 Documentation/git-merge-tree.txt | 47 ++++++++++++++++++++++++++++----
 builtin/merge-tree.c             | 21 ++++++++++++--
 t/t4301-merge-tree-write-tree.sh | 37 +++++++++++++++++++++++++
 3 files changed, 97 insertions(+), 8 deletions(-)
Documentation/git-merge-tree.txt +41 -6
index 2a9c91328de..25b462be14e 100644
@@ -9,7 +9,7 @@ git-merge-tree - Perform merge without touching index or working tree
 SYNOPSIS
 --------
 [verse]
-'git merge-tree' [--write-tree] <branch1> <branch2>
+'git merge-tree' [--write-tree] [<options>] <branch1> <branch2>
 'git merge-tree' [--trivial-merge] <base-tree> <branch1> <branch2> (deprecated)
 
 [[NEWMERGE]]
@@ -37,18 +37,50 @@ linkgit:git-merge[1], including:
 After the merge completes, a new toplevel tree object is created.  See
 `OUTPUT` below for details.
 
+OPTIONS
+-------
+
+--[no-]messages::
+	Write any informational messages such as "Auto-merging <path>"
+	or CONFLICT notices to the end of stdout.  If unspecified, the
+	default is to include these messages if there are merge
+	conflicts, and to omit them otherwise.
+
 [[OUTPUT]]
 OUTPUT
 ------
 
-For either a successful or conflicted merge, the output from
-git-merge-tree is simply one line:
+For a successful merge, the output from git-merge-tree is simply one
+line:
+
+	<OID of toplevel tree>
+
+Whereas for a conflicted merge, the output is by default of the form:
 
 	<OID of toplevel tree>
+	<Informational messages>
+
+These are discussed individually below.
 
-The printed tree object corresponds to what would be checked out in
-the working tree at the end of `git merge`, and thus may have files
-with conflict markers in them.
+[[OIDTLT]]
+OID of toplevel tree
+~~~~~~~~~~~~~~~~~~~~
+
+This is a tree object that represents what would be checked out in the
+working tree at the end of `git merge`.  If there were conflicts, then
+files within this tree may have embedded conflict markers.
+
+[[IM]]
+Informational messages
+~~~~~~~~~~~~~~~~~~~~~~
+
+This always starts with a blank line to separate it from the previous
+section, and then has free-form messages about the merge, such as:
+
+  * "Auto-merging <file>"
+  * "CONFLICT (rename/delete): <oldfile> renamed...but deleted in..."
+  * "Failed to merge submodule <submodule> (<reason>)"
+  * "Warning: cannot merge binary files: <filename>"
 
 EXIT STATUS
 -----------
@@ -72,6 +104,9 @@ used as a part of a series of steps such as:
        NEWCOMMIT=$(git commit-tree $NEWTREE -p $BRANCH1 -p $BRANCH2)
        git update-ref $BRANCH1 $NEWCOMMIT
 
+Note that when the exit status is non-zero, `NEWTREE` in this sequence
+will contain a lot more output than just a tree.
+
 [[DEPMERGE]]
 DEPRECATED DESCRIPTION
 ----------------------
builtin/merge-tree.c +19 -2
index 2332525d8bd..831d9c77583 100644
@@ -396,6 +396,7 @@ enum mode {
 
 struct merge_tree_options {
 	int mode;
+	int show_messages;
 };
 
 static int real_merge(struct merge_tree_options *o,
@@ -435,18 +436,27 @@ static int real_merge(struct merge_tree_options *o,
 	merge_incore_recursive(&opt, merge_bases, parent1, parent2, &result);
 	if (result.clean < 0)
 		die(_("failure to merge"));
+
+	if (o->show_messages == -1)
+		o->show_messages = !result.clean;
+
 	puts(oid_to_hex(&result.tree->object.oid));
+	if (o->show_messages) {
+		printf("\n");
+		merge_display_update_messages(&opt, &result);
+	}
 	merge_finalize(&opt, &result);
 	return !result.clean; /* result.clean < 0 handled above */
 }
 
 int cmd_merge_tree(int argc, const char **argv, const char *prefix)
 {
-	struct merge_tree_options o = { 0 };
+	struct merge_tree_options o = { .show_messages = -1 };
 	int expected_remaining_argc;
+	int original_argc;
 
 	const char * const merge_tree_usage[] = {
-		N_("git merge-tree [--write-tree] <branch1> <branch2>"),
+		N_("git merge-tree [--write-tree] [<options>] <branch1> <branch2>"),
 		N_("git merge-tree [--trivial-merge] <base-tree> <branch1> <branch2>"),
 		NULL
 	};
@@ -456,10 +466,13 @@ int cmd_merge_tree(int argc, const char **argv, const char *prefix)
 			    MODE_REAL),
 		OPT_CMDMODE(0, "trivial-merge", &o.mode,
 			    N_("do a trivial merge only"), MODE_TRIVIAL),
+		OPT_BOOL(0, "messages", &o.show_messages,
+			 N_("also show informational/conflict messages")),
 		OPT_END()
 	};
 
 	/* Parse arguments */
+	original_argc = argc - 1; /* ignoring argv[0] */
 	argc = parse_options(argc, argv, prefix, mt_options,
 			     merge_tree_usage, PARSE_OPT_STOP_AT_NON_OPTION);
 	switch (o.mode) {
@@ -483,8 +496,12 @@ int cmd_merge_tree(int argc, const char **argv, const char *prefix)
 		break;
 	case MODE_TRIVIAL:
 		expected_remaining_argc = 3;
+		/* Removal of `--trivial-merge` is expected */
+		original_argc--;
 		break;
 	}
+	if (o.mode == MODE_TRIVIAL && argc < original_argc)
+		die(_("--trivial-merge is incompatible with all other options"));
 
 	if (argc != expected_remaining_argc)
 		usage_with_options(merge_tree_usage, mt_options);
t/t4301-merge-tree-write-tree.sh +37 -0
index 6d321652e21..719d81e7173 100755
@@ -103,4 +103,41 @@ test_expect_success 'Barf on too many arguments' '
 	grep "^usage: git merge-tree" expect
 '
 
+anonymize_hash() {
+	sed -e "s/[0-9a-f]\{40,\}/HASH/g" "$@"
+}
+
+test_expect_success 'test conflict notices and such' '
+	test_expect_code 1 git merge-tree --write-tree side1 side2 >out &&
+	anonymize_hash out >actual &&
+
+	# Expected results:
+	#   "greeting" should merge with conflicts
+	#   "numbers" should merge cleanly
+	#   "whatever" has *both* a modify/delete and a file/directory conflict
+	cat <<-EOF >expect &&
+	HASH
+
+	Auto-merging greeting
+	CONFLICT (content): Merge conflict in greeting
+	Auto-merging numbers
+	CONFLICT (file/directory): directory in the way of whatever from side1; moving it to whatever~side1 instead.
+	CONFLICT (modify/delete): whatever~side1 deleted in side2 and modified in side1.  Version side1 of whatever~side1 left in tree.
+	EOF
+
+	test_cmp expect actual
+'
+
+for opt in $(git merge-tree --git-completion-helper-all)
+do
+	if test $opt = "--trivial-merge" || test $opt = "--write-tree"
+	then
+		continue
+	fi
+
+	test_expect_success "usage: --trivial-merge is incompatible with $opt" '
+		test_expect_code 128 git merge-tree --trivial-merge $opt side1 side2 side3
+	'
+done
+
 test_done
Signature · 1 line
gitgitgadget
#216 lore raw

PATCH v6 7/12 merge-ort: provide a merge_get_conflicted_files() helper function

In reply to Elijah Newren via GitGitGadget

From: Elijah Newren <[email protected]>

After a merge, this function allows the user to extract the same information that would be printed by ls-files -u, which means files with their mode, oid, and stage.

---
 merge-ort.c | 31 +++++++++++++++++++++++++++++++
 merge-ort.h | 21 +++++++++++++++++++++
 2 files changed, 52 insertions(+)
merge-ort.c +31 -0
index ebaed98d53a..e1b647b0a40 100644
@@ -4274,6 +4274,37 @@ void merge_display_update_messages(struct merge_options *opt,
 	trace2_region_leave("merge", "display messages", opt->repo);
 }
 
+void merge_get_conflicted_files(struct merge_result *result,
+				struct string_list *conflicted_files)
+{
+	struct hashmap_iter iter;
+	struct strmap_entry *e;
+	struct merge_options_internal *opti = result->priv;
+
+	strmap_for_each_entry(&opti->conflicted, &iter, e) {
+		const char *path = e->key;
+		struct conflict_info *ci = e->value;
+		int i;
+
+		VERIFY_CI(ci);
+
+		for (i = MERGE_BASE; i <= MERGE_SIDE2; i++) {
+			struct stage_info *si;
+
+			if (!(ci->filemask & (1ul << i)))
+				continue;
+
+			si = xmalloc(sizeof(*si));
+			si->stage = i+1;
+			si->mode = ci->stages[i].mode;
+			oidcpy(&si->oid, &ci->stages[i].oid);
+			string_list_append(conflicted_files, path)->util = si;
+		}
+	}
+	/* string_list_sort() uses a stable sort, so we're good */
+	string_list_sort(conflicted_files);
+}
+
 void merge_switch_to_result(struct merge_options *opt,
 			    struct tree *head,
 			    struct merge_result *result,
merge-ort.h +21 -0
index e5aec45b18f..ddcc39d7270 100644
@@ -2,6 +2,7 @@
 #define MERGE_ORT_H
 
 #include "merge-recursive.h"
+#include "hash.h"
 
 struct commit;
 struct tree;
@@ -88,6 +89,26 @@ void merge_switch_to_result(struct merge_options *opt,
 void merge_display_update_messages(struct merge_options *opt,
 				   struct merge_result *result);
 
+struct stage_info {
+	struct object_id oid;
+	int mode;
+	int stage;
+};
+
+/*
+ * Provide a list of path -> {struct stage_info*} mappings for
+ * all conflicted files.  Note that each path could appear up to three
+ * times in the list, corresponding to 3 different stage entries.  In short,
+ * this basically provides the info that would be printed by `ls-files -u`.
+ *
+ * result should have been populated by a call to
+ * one of the merge_incore_[non]recursive() functions.
+ *
+ * conflicted_files should be empty before calling this function.
+ */
+void merge_get_conflicted_files(struct merge_result *result,
+				struct string_list *conflicted_files);
+
 /* Do needed cleanup when not calling merge_switch_to_result() */
 void merge_finalize(struct merge_options *opt,
 		    struct merge_result *result);
Signature · 1 line
gitgitgadget
#217 lore raw

PATCH v6 8/12 merge-tree: provide a list of which files have conflicts

In reply to Elijah Newren via GitGitGadget

From: Elijah Newren <[email protected]>

Callers of git merge-tree --write-tree will often want to know which files had conflicts. While they could potentially attempt to parse the CONFLICT notices printed, those messages are not meant to be machine readable. Provide a simpler mechanism of just printing the files (in the same format as git ls-files with quoting, but restricted to unmerged files) in the output before the free-form messages.

---
 Documentation/git-merge-tree.txt |  9 +++++++++
 builtin/merge-tree.c             | 24 ++++++++++++++++++++++--
 t/t4301-merge-tree-write-tree.sh | 11 +++++++++++
 3 files changed, 42 insertions(+), 2 deletions(-)
Documentation/git-merge-tree.txt +9 -0
index 25b462be14e..68a51c82618 100644
@@ -58,6 +58,7 @@ line:
 Whereas for a conflicted merge, the output is by default of the form:
 
 	<OID of toplevel tree>
+	<Conflicted file list>
 	<Informational messages>
 
 These are discussed individually below.
@@ -70,6 +71,14 @@ This is a tree object that represents what would be checked out in the
 working tree at the end of `git merge`.  If there were conflicts, then
 files within this tree may have embedded conflict markers.
 
+[[CFI]]
+Conflicted file list
+~~~~~~~~~~~~~~~~~~~~
+
+This is a sequence of lines containing a filename on each line, quoted
+as explained for the configuration variable `core.quotePath` (see
+linkgit:git-config[1]).
+
 [[IM]]
 Informational messages
 ~~~~~~~~~~~~~~~~~~~~~~
builtin/merge-tree.c +22 -2
index 831d9c77583..3653526cc0a 100644
@@ -11,6 +11,9 @@
 #include "blob.h"
 #include "exec-cmd.h"
 #include "merge-blobs.h"
+#include "quote.h"
+
+static int line_termination = '\n';
 
 struct merge_list {
 	struct merge_list *next;
@@ -400,7 +403,8 @@ struct merge_tree_options {
 };
 
 static int real_merge(struct merge_tree_options *o,
-		      const char *branch1, const char *branch2)
+		      const char *branch1, const char *branch2,
+		      const char *prefix)
 {
 	struct commit *parent1, *parent2;
 	struct commit_list *merge_bases = NULL;
@@ -441,6 +445,22 @@ static int real_merge(struct merge_tree_options *o,
 		o->show_messages = !result.clean;
 
 	puts(oid_to_hex(&result.tree->object.oid));
+	if (!result.clean) {
+		struct string_list conflicted_files = STRING_LIST_INIT_NODUP;
+		const char *last = NULL;
+		int i;
+
+		merge_get_conflicted_files(&result, &conflicted_files);
+		for (i = 0; i < conflicted_files.nr; i++) {
+			const char *name = conflicted_files.items[i].string;
+			if (last && !strcmp(last, name))
+				continue;
+			write_name_quoted_relative(
+				name, prefix, stdout, line_termination);
+			last = name;
+		}
+		string_list_clear(&conflicted_files, 1);
+	}
 	if (o->show_messages) {
 		printf("\n");
 		merge_display_update_messages(&opt, &result);
@@ -508,7 +528,7 @@ int cmd_merge_tree(int argc, const char **argv, const char *prefix)
 
 	/* Do the relevant type of merge */
 	if (o.mode == MODE_REAL)
-		return real_merge(&o, argv[0], argv[1]);
+		return real_merge(&o, argv[0], argv[1], prefix);
 	else
 		return trivial_merge(argv[0], argv[1], argv[2]);
 }
t/t4301-merge-tree-write-tree.sh +11 -0
index 719d81e7173..8e6dba44288 100755
@@ -117,6 +117,8 @@ test_expect_success 'test conflict notices and such' '
 	#   "whatever" has *both* a modify/delete and a file/directory conflict
 	cat <<-EOF >expect &&
 	HASH
+	greeting
+	whatever~side1
 
 	Auto-merging greeting
 	CONFLICT (content): Merge conflict in greeting
@@ -140,4 +142,13 @@ do
 	'
 done
 
+test_expect_success 'Just the conflicted files without the messages' '
+	test_expect_code 1 git merge-tree --write-tree --no-messages side1 side2 >out &&
+	anonymize_hash out >actual &&
+
+	test_write_lines HASH greeting whatever~side1 >expect &&
+
+	test_cmp expect actual
+'
+
 test_done
Signature · 1 line
gitgitgadget
#218 lore raw

PATCH v6 9/12 merge-tree: provide easy access to `ls-files -u` style info

In reply to Elijah Newren via GitGitGadget

From: Elijah Newren <[email protected]>

Much like git merge updates the index with information of the form
(mode, oid, stage, name)
provide this output for conflicted files for merge-tree as well. Provide a --name-only option for users to exclude the mode, oid, and stage and only get the list of conflicted filenames.

---
 Documentation/git-merge-tree.txt | 34 ++++++++++++++++++++++++++------
 builtin/merge-tree.c             | 11 ++++++++++-
 t/t4301-merge-tree-write-tree.sh | 26 ++++++++++++++++++++++--
 3 files changed, 62 insertions(+), 9 deletions(-)
Documentation/git-merge-tree.txt +28 -6
index 68a51c82618..b89aabdb98e 100644
@@ -40,6 +40,13 @@ After the merge completes, a new toplevel tree object is created.  See
 OPTIONS
 -------
 
+--name-only::
+	In the Conflicted file info section, instead of writing a list
+	of (mode, oid, stage, path) tuples to output for conflicted
+	files, just provide a list of filenames with conflicts (and
+	do not list filenames multiple times if they have multiple
+	conflicting stages).
+
 --[no-]messages::
 	Write any informational messages such as "Auto-merging <path>"
 	or CONFLICT notices to the end of stdout.  If unspecified, the
@@ -58,7 +65,7 @@ line:
 Whereas for a conflicted merge, the output is by default of the form:
 
 	<OID of toplevel tree>
-	<Conflicted file list>
+	<Conflicted file info>
 	<Informational messages>
 
 These are discussed individually below.
@@ -72,19 +79,24 @@ working tree at the end of `git merge`.  If there were conflicts, then
 files within this tree may have embedded conflict markers.
 
 [[CFI]]
-Conflicted file list
+Conflicted file info
 ~~~~~~~~~~~~~~~~~~~~
 
-This is a sequence of lines containing a filename on each line, quoted
-as explained for the configuration variable `core.quotePath` (see
-linkgit:git-config[1]).
+This is a sequence of lines with the format
+
+	<mode> <object> <stage> <filename>
+
+The filename will be quoted as explained for the configuration
+variable `core.quotePath` (see linkgit:git-config[1]).  However, if
+the `--name-only` option is passed, the mode, object, and stage will
+be omitted.
 
 [[IM]]
 Informational messages
 ~~~~~~~~~~~~~~~~~~~~~~
 
 This always starts with a blank line to separate it from the previous
-section, and then has free-form messages about the merge, such as:
+sections, and then has free-form messages about the merge, such as:
 
   * "Auto-merging <file>"
   * "CONFLICT (rename/delete): <oldfile> renamed...but deleted in..."
@@ -116,6 +128,16 @@ used as a part of a series of steps such as:
 Note that when the exit status is non-zero, `NEWTREE` in this sequence
 will contain a lot more output than just a tree.
 
+For conflicts, the output includes the same information that you'd get
+with linkgit:git-merge[1]:
+
+  * what would be written to the working tree (the
+    <<OIDTLT,OID of toplevel tree>>)
+  * the higher order stages that would be written to the index (the
+    <<CFI,Conflicted file info>>)
+  * any messages that would have been printed to stdout (the
+    <<IM,Informational messages>>)
+
 [[DEPMERGE]]
 DEPRECATED DESCRIPTION
 ----------------------
builtin/merge-tree.c +10 -1
index 3653526cc0a..4a79caf081a 100644
@@ -400,6 +400,7 @@ enum mode {
 struct merge_tree_options {
 	int mode;
 	int show_messages;
+	int name_only;
 };
 
 static int real_merge(struct merge_tree_options *o,
@@ -453,7 +454,11 @@ static int real_merge(struct merge_tree_options *o,
 		merge_get_conflicted_files(&result, &conflicted_files);
 		for (i = 0; i < conflicted_files.nr; i++) {
 			const char *name = conflicted_files.items[i].string;
-			if (last && !strcmp(last, name))
+			struct stage_info *c = conflicted_files.items[i].util;
+			if (!o->name_only)
+				printf("%06o %s %d\t",
+				       c->mode, oid_to_hex(&c->oid), c->stage);
+			else if (last && !strcmp(last, name))
 				continue;
 			write_name_quoted_relative(
 				name, prefix, stdout, line_termination);
@@ -488,6 +493,10 @@ int cmd_merge_tree(int argc, const char **argv, const char *prefix)
 			    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, "name-only",
+			   &o.name_only,
+			   N_("list filenames without modes/oids/stages"),
+			   PARSE_OPT_NONEG),
 		OPT_END()
 	};
 
t/t4301-merge-tree-write-tree.sh +24 -2
index 8e6dba44288..0ec5f0d3f7e 100755
@@ -65,6 +65,7 @@ test_expect_success 'Content merge and a few conflicts' '
 	expected_tree=$(git rev-parse AUTO_MERGE) &&
 
 	# We will redo the merge, while we are still in a conflicted state!
+	git ls-files -u >conflicted-file-info &&
 	test_when_finished "git reset --hard" &&
 
 	test_expect_code 1 git merge-tree --write-tree side1 side2 >RESULT &&
@@ -108,7 +109,7 @@ anonymize_hash() {
 }
 
 test_expect_success 'test conflict notices and such' '
-	test_expect_code 1 git merge-tree --write-tree side1 side2 >out &&
+	test_expect_code 1 git merge-tree --write-tree --name-only side1 side2 >out &&
 	anonymize_hash out >actual &&
 
 	# Expected results:
@@ -143,7 +144,7 @@ do
 done
 
 test_expect_success 'Just the conflicted files without the messages' '
-	test_expect_code 1 git merge-tree --write-tree --no-messages side1 side2 >out &&
+	test_expect_code 1 git merge-tree --write-tree --no-messages --name-only side1 side2 >out &&
 	anonymize_hash out >actual &&
 
 	test_write_lines HASH greeting whatever~side1 >expect &&
@@ -151,4 +152,25 @@ test_expect_success 'Just the conflicted files without the messages' '
 	test_cmp expect actual
 '
 
+test_expect_success 'Check conflicted oids and modes without messages' '
+	test_expect_code 1 git merge-tree --write-tree --no-messages side1 side2 >out &&
+	anonymize_hash out >actual &&
+
+	# Compare the basic output format
+	q_to_tab >expect <<-\EOF &&
+	HASH
+	100644 HASH 1Qgreeting
+	100644 HASH 2Qgreeting
+	100644 HASH 3Qgreeting
+	100644 HASH 1Qwhatever~side1
+	100644 HASH 2Qwhatever~side1
+	EOF
+
+	test_cmp expect actual &&
+
+	# Check the actual hashes against the `ls-files -u` output too
+	tail -n +2 out | sed -e s/side1/HEAD/ >actual &&
+	test_cmp conflicted-file-info actual
+'
+
 test_done
Signature · 1 line
gitgitgadget
#219 lore raw

PATCH v6 10/12 merge-tree: allow `ls-files -u` style info to be NUL terminated

In reply to Elijah Newren via GitGitGadget

From: Elijah Newren <[email protected]>

Much as git ls-files has a -z option, let's add one to merge-tree so that the conflict-info section can be NUL terminated (and avoid quoting of unusual filenames).

---
 Documentation/git-merge-tree.txt | 21 +++++++++++++----
 builtin/merge-tree.c             |  6 +++--
 t/t4301-merge-tree-write-tree.sh | 40 ++++++++++++++++++++++++++++++++
 3 files changed, 61 insertions(+), 6 deletions(-)
Documentation/git-merge-tree.txt +17 -4
index b89aabdb98e..75b57f8abab 100644
@@ -40,6 +40,12 @@ After the merge completes, a new toplevel tree object is created.  See
 OPTIONS
 -------
 
+-z::
+	Do not quote filenames in the <Conflicted file info> section,
+	and end each filename with a NUL character rather than
+	newline.  Also begin the messages section with a NUL character
+	instead of a newline.  See <<OUTPUT>> below for more information.
+
 --name-only::
 	In the Conflicted file info section, instead of writing a list
 	of (mode, oid, stage, path) tuples to output for conflicted
@@ -76,7 +82,8 @@ OID of toplevel tree
 
 This is a tree object that represents what would be checked out in the
 working tree at the end of `git merge`.  If there were conflicts, then
-files within this tree may have embedded conflict markers.
+files within this tree may have embedded conflict markers.  This section
+is always followed by a newline (or NUL if `-z` is passed).
 
 [[CFI]]
 Conflicted file info
@@ -89,20 +96,26 @@ This is a sequence of lines with the format
 The filename will be quoted as explained for the configuration
 variable `core.quotePath` (see linkgit:git-config[1]).  However, if
 the `--name-only` option is passed, the mode, object, and stage will
-be omitted.
+be omitted.  If `-z` is passed, the "lines" are terminated by a NUL
+character instead of a newline character.
 
 [[IM]]
 Informational messages
 ~~~~~~~~~~~~~~~~~~~~~~
 
-This always starts with a blank line to separate it from the previous
-sections, and then has free-form messages about the merge, such as:
+This always starts with a blank line (or NUL if `-z` is passed) to
+separate it from the previous sections, and then has free-form
+messages about the merge, such as:
 
   * "Auto-merging <file>"
   * "CONFLICT (rename/delete): <oldfile> renamed...but deleted in..."
   * "Failed to merge submodule <submodule> (<reason>)"
   * "Warning: cannot merge binary files: <filename>"
 
+Note that these free-form messages will never have a NUL character
+in or between them, even if -z is passed.  It is simply a large block
+of text taking up the remainder of the output.
+
 EXIT STATUS
 -----------
 
builtin/merge-tree.c +4 -2
index 4a79caf081a..767892006e3 100644
@@ -445,7 +445,7 @@ static int real_merge(struct merge_tree_options *o,
 	if (o->show_messages == -1)
 		o->show_messages = !result.clean;
 
-	puts(oid_to_hex(&result.tree->object.oid));
+	printf("%s%c", oid_to_hex(&result.tree->object.oid), line_termination);
 	if (!result.clean) {
 		struct string_list conflicted_files = STRING_LIST_INIT_NODUP;
 		const char *last = NULL;
@@ -467,7 +467,7 @@ static int real_merge(struct merge_tree_options *o,
 		string_list_clear(&conflicted_files, 1);
 	}
 	if (o->show_messages) {
-		printf("\n");
+		putchar(line_termination);
 		merge_display_update_messages(&opt, &result);
 	}
 	merge_finalize(&opt, &result);
@@ -493,6 +493,8 @@ int cmd_merge_tree(int argc, const char **argv, const char *prefix)
 			    N_("do a trivial merge only"), MODE_TRIVIAL),
 		OPT_BOOL(0, "messages", &o.show_messages,
 			 N_("also show informational/conflict messages")),
+		OPT_SET_INT('z', NULL, &line_termination,
+			    N_("separate paths with the NUL character"), '\0'),
 		OPT_BOOL_F(0, "name-only",
 			   &o.name_only,
 			   N_("list filenames without modes/oids/stages"),
t/t4301-merge-tree-write-tree.sh +40 -0
index 0ec5f0d3f7e..22e03f0939c 100755
@@ -173,4 +173,44 @@ test_expect_success 'Check conflicted oids and modes without messages' '
 	test_cmp conflicted-file-info actual
 '
 
+test_expect_success 'NUL terminated conflicted file "lines"' '
+	git checkout -b tweak1 side1 &&
+	test_write_lines zero 1 2 3 4 5 6 >numbers &&
+	git add numbers &&
+	git mv numbers "Αυτά μου φαίνονται κινέζικα" &&
+	git commit -m "Renamed numbers" &&
+
+	test_expect_code 1 git merge-tree --write-tree -z tweak1 side2 >out &&
+	anonymize_hash out >actual &&
+
+	# Expected results:
+	#   "greeting" should merge with conflicts
+	#   "whatever" has *both* a modify/delete and a file/directory conflict
+	#   "Αυτά μου φαίνονται κινέζικα" should have a conflict
+	echo HASH | lf_to_nul >expect &&
+
+	q_to_tab <<-EOF | lf_to_nul >>expect &&
+	100644 HASH 1Qgreeting
+	100644 HASH 2Qgreeting
+	100644 HASH 3Qgreeting
+	100644 HASH 1Qwhatever~tweak1
+	100644 HASH 2Qwhatever~tweak1
+	100644 HASH 1QΑυτά μου φαίνονται κινέζικα
+	100644 HASH 2QΑυτά μου φαίνονται κινέζικα
+	100644 HASH 3QΑυτά μου φαίνονται κινέζικα
+
+	EOF
+
+	cat <<-EOF >>expect &&
+	Auto-merging greeting
+	CONFLICT (content): Merge conflict in greeting
+	CONFLICT (file/directory): directory in the way of whatever from tweak1; moving it to whatever~tweak1 instead.
+	CONFLICT (modify/delete): whatever~tweak1 deleted in side2 and modified in tweak1.  Version tweak1 of whatever~tweak1 left in tree.
+	Auto-merging Αυτά μου φαίνονται κινέζικα
+	CONFLICT (content): Merge conflict in Αυτά μου φαίνονται κινέζικα
+	EOF
+
+	test_cmp expect actual
+'
+
 test_done
Signature · 1 line
gitgitgadget
#220 lore raw

PATCH v6 11/12 merge-tree: add a --allow-unrelated-histories flag

In reply to Elijah Newren via GitGitGadget

From: Elijah Newren <[email protected]>

Folks may want to merge histories that have no common ancestry; provide a flag with the same name as used by git merge to allow this.

---
 Documentation/git-merge-tree.txt |  5 +++++
 builtin/merge-tree.c             |  7 ++++++-
 t/t4301-merge-tree-write-tree.sh | 24 +++++++++++++++++++++++-
 3 files changed, 34 insertions(+), 2 deletions(-)
Documentation/git-merge-tree.txt +5 -0
index 75b57f8abab..628324646d3 100644
@@ -59,6 +59,11 @@ OPTIONS
 	default is to include these messages if there are merge
 	conflicts, and to omit them otherwise.
 
+--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
+	check and make the merge proceed anyway.
+
 [[OUTPUT]]
 OUTPUT
 ------
builtin/merge-tree.c +6 -1
index 767892006e3..b4f5c7e0aab 100644
@@ -399,6 +399,7 @@ enum mode {
 
 struct merge_tree_options {
 	int mode;
+	int allow_unrelated_histories;
 	int show_messages;
 	int name_only;
 };
@@ -434,7 +435,7 @@ static int real_merge(struct merge_tree_options *o,
 	 * merge_incore_recursive in merge-ort.h
 	 */
 	merge_bases = get_merge_bases(parent1, parent2);
-	if (!merge_bases)
+	if (!merge_bases && !o->allow_unrelated_histories)
 		die(_("refusing to merge unrelated histories"));
 	merge_bases = reverse_commit_list(merge_bases);
 
@@ -499,6 +500,10 @@ int cmd_merge_tree(int argc, const char **argv, const char *prefix)
 			   &o.name_only,
 			   N_("list filenames without modes/oids/stages"),
 			   PARSE_OPT_NONEG),
+		OPT_BOOL_F(0, "allow-unrelated-histories",
+			   &o.allow_unrelated_histories,
+			   N_("allow merging unrelated histories"),
+			   PARSE_OPT_NONEG),
 		OPT_END()
 	};
 
t/t4301-merge-tree-write-tree.sh +23 -1
index 22e03f0939c..bd1769c624b 100755
@@ -44,7 +44,13 @@ test_expect_success setup '
 	git checkout side3 &&
 	git mv numbers sequence &&
 	test_tick &&
-	git commit -m rename-numbers
+	git commit -m rename-numbers &&
+
+	git switch --orphan unrelated &&
+	>something-else &&
+	git add something-else &&
+	test_tick &&
+	git commit -m first-commit
 '
 
 test_expect_success 'Clean merge' '
@@ -213,4 +219,20 @@ test_expect_success 'NUL terminated conflicted file "lines"' '
 	test_cmp expect actual
 '
 
+test_expect_success 'error out by default for unrelated histories' '
+	test_expect_code 128 git merge-tree --write-tree side1 unrelated 2>error &&
+
+	grep "refusing to merge unrelated histories" error
+'
+
+test_expect_success 'can override merge of unrelated histories' '
+	git merge-tree --write-tree --allow-unrelated-histories side1 unrelated >tree &&
+	TREE=$(cat tree) &&
+
+	git rev-parse side1:numbers side1:greeting side1:whatever unrelated:something-else >expect &&
+	git rev-parse $TREE:numbers $TREE:greeting $TREE:whatever $TREE:something-else >actual &&
+
+	test_cmp expect actual
+'
+
 test_done
Signature · 1 line
gitgitgadget
#221 lore raw

PATCH v6 12/12 git-merge-tree.txt: add a section on potentional usage mistakes

In reply to Elijah Newren via GitGitGadget

From: Elijah Newren <[email protected]>

---
 Documentation/git-merge-tree.txt | 46 ++++++++++++++++++++++++++++++++
 1 file changed, 46 insertions(+)
Documentation/git-merge-tree.txt +46 -0
index 628324646d3..ee8125810e6 100644
@@ -156,6 +156,52 @@ with linkgit:git-merge[1]:
   * any messages that would have been printed to stdout (the
     <<IM,Informational messages>>)
 
+MISTAKES TO AVOID
+-----------------
+
+Do NOT look through the resulting toplevel tree to try to find which
+files conflict; parse the <<CFI,Conflicted file info>> section instead.
+Not only would parsing an entire tree be horrendously slow in large
+repositories, there are numerous types of conflicts not representable by
+conflict markers (modify/delete, mode conflict, binary file changed on
+both sides, file/directory conflicts, various rename conflict
+permutations, etc.)
+
+Do NOT interpret an empty <<CFI,Conflicted file info>> list as a clean
+merge; check the exit status.  A merge can have conflicts without having
+individual files conflict (there are a few types of directory rename
+conflicts that fall into this category, and others might also be added
+in the future).
+
+Do NOT attempt to guess or make the user guess the conflict types from
+the <<CFI,Conflicted file info>> list.  The information there is
+insufficient to do so.  For example: Rename/rename(1to2) conflicts (both
+sides renamed the same file differently) will result in three different
+file having higher order stages (but each only has one higher order
+stage), with no way (short of the <<IM,Informational messages>> section)
+to determine which three files are related.  File/directory conflicts
+also result in a file with exactly one higher order stage.
+Possibly-involved-in-directory-rename conflicts (when
+"merge.directoryRenames" is unset or set to "conflicts") also result in
+a file with exactly one higher order stage.  In all cases, the
+<<IM,Informational messages>> section has the necessary info, though it
+is not designed to be machine parseable.
+
+Do NOT assume all filenames listed in the <<IM,Informational messages>>
+section had conflicts.  Messages can be included for files that have no
+conflicts, such as "Auto-merging <file>".
+
+AVOID taking the OIDS from the <<CFI,Conflicted file info>> and
+re-merging them to present the conflicts to the user.  This will lose
+information.  Instead, look up the version of the file found within the
+<<OIDTLT,OID of toplevel tree>> and show that instead.  In particular,
+the latter will have conflict markers annotated with the original
+branch/commit being merged and, if renames were involved, the original
+filename.  While you could include the original branch/commit in the
+conflict marker annotations when re-merging, the original filename is
+not available from the <<CFI,Conflicted file info>> and thus you would
+be losing information that might help the user resolve the conflict.
+
 [[DEPMERGE]]
 DEPRECATED DESCRIPTION
 ----------------------
Signature · 1 line
gitgitgadget
#222 lore raw

Re: [PATCH v6 00/12] In-core git merge-tree ("Server side merges")

In reply to Elijah Newren via GitGitGadget
Quoted 4 lines · Elijah Newren via GitGitGadget

== Updates Log ==

Stuff NOT included that reviewers brought up in various rounds (and which might still be an open question):

Except for them, the differences since the last round all looked sensible to me.

Will queue.

#223 lore raw

v7 cover In-core git merge-tree ("Server side merges")

In reply to Elijah Newren via GitGitGadget

Sorry for the long delay. Much thanks to Johannes for tying up the loose ends and making several improvements seen in this round. Note: rebased on main (it's been 4 months since v6, and there were a few conflicts...).

== Basic Summary ==

This series introduces a new mode to git merge-tree allowing it to perform real merges (three-way text content merges, recursive ancestor consolidation, rename detection, proper directory/file conflict handling, etc.) and write the result as a toplevel tree. It doesn't touch the working tree or index, and doesn't create any commits or update any refs. It could be used to do merges when in a bare repository (thus potentially making it of interest to Git hosting sites, i.e. "Server side merges"), or for doing a merge of branches that aren't checked out.

It does not handle similar functionality for cherry-picks, rebases, or reverts; that is also of interest, but is being deferred for a future series.

== Quick Overview ==

* Patches 1-2: preparatory cleanups
* Patches 3-4: implement basic real merges
* Patches 5-6: include informational messages ("CONFLICT" messages and such) in output
* Patches 7-15: add ability to include ls-files -u style of info in the output
* Patch 16: support --allow-unrelated-histories
* Patch 17: augment the manual with potential usage mistakes

== Updates Log ==

Stuff NOT included that reviewers brought up in various rounds (and which might still be an open question):

* Very generic (mode, oid, stage, filename) printing formatting[3]
* Providing similar functionality for doing cherry-picks/rebases/reverts, i.e. a scheme for three-way merges with a specified merge-base[4]. That's being deferred to a future series. [3]
https://lore.kernel.org/git/CABPp-BGnOes7J_piDyBUeuLVm274w4-9G3k0vR-0it3z7TPn_w@mail.gmail.com/
[4]
https://lore.kernel.org/git/CABPp-BEaemkGGm0cSofP0gau7YN-y6HFoi0yJbHA8+iGjxsYSA@mail.gmail.com/

Updates since v6:

* Significant merge-ort changes to support machine-parseable information about individual conflicts and the paths involved in each, rather than just a group of free-form output messages that may change over time. This includes 5 new patches, 2 written by Johannes (and with breaking out the other 3 from some work-in-progress changes I had including some fixes and improvements he added).

Updates since v5:

* Used reverse_commit_list() to reverse a commit_list without extra allocations
* Several documentation updates

Updates since v4:

* Fixed double "is" in documentation.
* Fixed a few small items with testcases

Updates since v3:

* Dropped previous patches 5, 6, and 8 of the old series; they weren't being used and opened a can of worms[1]
* [Patch 3] Restructured argument checking, including using an enum
* [Patch 4] Restored the extended paragraph about the deprecated form of git-merge-tree, mentioned write-tree in plumbing commands, and a few other small fixups to the documentation
* [Patch 4] Also provide an example of a clean merge rather than just a conflicted one
* [Patch 6] Fix the incompatible arguments check and add some tests for it
* [Patch 6] Introduce an anonymize_hash() shell function to make tests easier to read (less repeated sed)
* [Patch 9] Rename --exclude-modes-oids-stages to --name-only; no short option for now
* [Patch 10] When -z passed, the tree in the first section should have a trailing NUL rather than trailing newline [1]
https://lore.kernel.org/git/CABPp-BEKuXHELVx4=5JJTj5HVOKZ=Y-4G4BK47BCZYYRSrkFsQ@mail.gmail.com/

Updates since v2:

* Improved patches from Dscho for the diff_warn_rename_limit() handling
* Add a -z option for NUL-terminated conflict info lines (so that filenames do not have to be quoted)

Updates since v1 (or v3 depending on how you count; thanks to René, Ævar, Christian, Dscho for very helpful feedback):

* New patch from Dscho allowing diff_warn_rename_limit() to print somewhere other than stdout (I hope he's okay with me including his Signed-off-by)
* Now prints filenames relative to prefix, much like ls-files
* Renamed --exclude-oids-and-modes to --exclude-modes-oids-stages and gave it a -l shorthand; I'm wondering if I should just drop this option, though.
* And numerous cleanups, in lots of areas:
* Multiple parse-options cleanups
* Lots of commit message cleanups
* Wording tweaks to the "Description" section of the manual
* Several small code cleanups
* I dropped the RFC label

[There were also two submissions of a previous series; see
https://lore.kernel.org/git/[email protected]/]
[https://lore.kernel.org/git/[email protected]/%5D]

Updates since original submission v2 (thanks to Christian, Dscho, Ramsay, and René for suggestions and comments):

* Significant changes to output format:
* Flags no longer take a filename for additional output; they write to stdout instead.
* More information included by default when there are conflicts (no need to request it with additional flags, instead flags can be used to suppress it).
* Provide (mode, oid, stage, file) tuples -- i.e. ls-files -u style of information -- when there are conflicts. Add a flag to only list conflicted files if that's preferred.
* Much more thorough manual for git-merge-tree.txt
* Renamed option from --real to --write-tree
* Accept an optional --trivial-merge option to get old style merge-tree behavior
* Allow both --write-tree and --trivial-merge to be omitted since we can deduce which from number of arguments
* Document exit code when the merge cannot be run (so we can distinguish other error cases from conflicts)
* testcase cleanups: test_tick, early skip of test when using recursive backend, variable renames, etc.
* various minor code cleanups
* Add a new --allow-unrelated-histories option (with same meaning as the one used in git merge)
* Rebased on top of en/remerge-diff to avoid a small conflict

Updates since original submission v1 (thanks to Johannes Altmanninger and Fabian for suggestions):

* Fixed a bad patch splitting, and a style issue pointed out by Johannes Altimanninger
* Fixed misleading commit messages in new test cases
* Fixed my comments about how commit-tree could be used to correctly use two -p flags

Elijah Newren (15):
merge-tree: rename merge_trees() to trivial_merge_trees()
merge-tree: move logic for existing merge into new function
merge-tree: add option parsing and initial shell for real merge
function
merge-tree: implement real merges
merge-ort: split out a separate display_update_messages() function
merge-tree: support including merge messages in output
merge-ort: provide a merge_get_conflicted_files() helper function
merge-ort: remove command-line-centric submodule message from
merge-ort
merge-tree: provide a list of which files have conflicts
merge-tree: provide easy access to ls-files -u style info
merge-ort: store more specific conflict information
merge-ort: optionally produce machine-readable output
merge-tree: allow ls-files -u style info to be NUL terminated
merge-tree: add a --allow-unrelated-histories flag git-merge-tree.txt: add a section on potentional usage mistakes

Johannes Schindelin (2):
merge-ort: store messages in a list, not in a single strbuf
merge-ort: make path_messages a strmap to a string_list

 Documentation/git-merge-tree.txt | 235 ++++++++++++++-
 builtin/merge-tree.c             | 187 +++++++++++-
 diff.c                           |  27 +-
 git.c                            |   2 +-
 merge-ort.c                      | 474 ++++++++++++++++++++++---------
 merge-ort.h                      |  32 ++-
 t/t4301-merge-tree-write-tree.sh | 240 ++++++++++++++++
 t/t6437-submodule-merge.sh       |   2 +-
 8 files changed, 1032 insertions(+), 167 deletions(-)
 create mode 100755 t/t4301-merge-tree-write-tree.sh

base-commit: ab336e8f1c8009c8b1aab8deb592148e69217085
Published-As: https://github.com/gitgitgadget/git/releases/tag/pr-1122%2Fnewren%2Fin-core-merge-tree-v7
Fetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-1122/newren/in-core-merge-tree-v7
Pull-Request: https://github.com/gitgitgadget/git/pull/1122

Range-diff vs v6:

  1:  4a7cd5542bb =  1:  8fb51817ed4 merge-tree: rename merge_trees() to trivial_merge_trees()
  2:  4780ff6784d =  2:  8e0a79fa1ad merge-tree: move logic for existing merge into new function
  3:  60253745f5c =  3:  baf0950bcb6 merge-tree: add option parsing and initial shell for real merge function
  4:  f8266d39c1b =  4:  697470e50ae merge-tree: implement real merges
  5:  6629af14919 =  5:  069af1ecc30 merge-ort: split out a separate display_update_messages() function
  6:  17b57efb714 =  6:  53c92a5d8d9 merge-tree: support including merge messages in output
  7:  4c8f42372dd =  7:  67a728d35f0 merge-ort: provide a merge_get_conflicted_files() helper function
  -:  ----------- >  8:  6419487e26b merge-ort: remove command-line-centric submodule message from merge-ort
  8:  7b1ee417f3d !  9:  c92b81e7366 merge-tree: provide a list of which files have conflicts
     @@ builtin/merge-tree.c: static int real_merge(struct merge_tree_options *o,
      +		string_list_clear(&conflicted_files, 1);
      +	}
       	if (o->show_messages) {
     - 		printf("\n");
     +-		printf("\n");
     ++		putchar(line_termination);
       		merge_display_update_messages(&opt, &result);
     + 	}
     + 	merge_finalize(&opt, &result);
      @@ builtin/merge-tree.c: int cmd_merge_tree(int argc, const char **argv, const char *prefix)
       
       	/* Do the relevant type of merge */
  9:  f1231a8fbc8 = 10:  d7360f58f16 merge-tree: provide easy access to `ls-files -u` style info
  -:  ----------- > 11:  f523b08ab5a merge-ort: store messages in a list, not in a single strbuf
  -:  ----------- > 12:  6b47c0fdbd7 merge-ort: make `path_messages` a strmap to a string_list
  -:  ----------- > 13:  7eb70f77c81 merge-ort: store more specific conflict information
  -:  ----------- > 14:  662e97f2ed4 merge-ort: optionally produce machine-readable output
 10:  22297e6ce75 ! 15:  b314aa9c436 merge-tree: allow `ls-files -u` style info to be NUL terminated
     @@ builtin/merge-tree.c: static int real_merge(struct merge_tree_options *o,
       	if (!result.clean) {
       		struct string_list conflicted_files = STRING_LIST_INIT_NODUP;
       		const char *last = NULL;
     -@@ builtin/merge-tree.c: static int real_merge(struct merge_tree_options *o,
     - 		string_list_clear(&conflicted_files, 1);
     - 	}
     - 	if (o->show_messages) {
     --		printf("\n");
     -+		putchar(line_termination);
     - 		merge_display_update_messages(&opt, &result);
     - 	}
     - 	merge_finalize(&opt, &result);
      @@ builtin/merge-tree.c: int cmd_merge_tree(int argc, const char **argv, const char *prefix)
       			    N_("do a trivial merge only"), MODE_TRIVIAL),
       		OPT_BOOL(0, "messages", &o.show_messages,
     @@ t/t4301-merge-tree-write-tree.sh: test_expect_success 'Check conflicted oids and
      +
      +	test_expect_code 1 git merge-tree --write-tree -z tweak1 side2 >out &&
      +	anonymize_hash out >actual &&
     ++	printf "\\n" >>actual &&
      +
      +	# Expected results:
      +	#   "greeting" should merge with conflicts
     @@ t/t4301-merge-tree-write-tree.sh: test_expect_success 'Check conflicted oids and
      +
      +	EOF
      +
     -+	cat <<-EOF >>expect &&
     -+	Auto-merging greeting
     -+	CONFLICT (content): Merge conflict in greeting
     -+	CONFLICT (file/directory): directory in the way of whatever from tweak1; moving it to whatever~tweak1 instead.
     -+	CONFLICT (modify/delete): whatever~tweak1 deleted in side2 and modified in tweak1.  Version tweak1 of whatever~tweak1 left in tree.
     -+	Auto-merging Αυτά μου φαίνονται κινέζικα
     -+	CONFLICT (content): Merge conflict in Αυτά μου φαίνονται κινέζικα
     ++	q_to_nul <<-EOF >>expect &&
     ++	1QgreetingQAuto-mergingQAuto-merging greeting
     ++	Q1QgreetingQCONFLICT (contents)QCONFLICT (content): Merge conflict in greeting
     ++	Q2Qwhatever~tweak1QwhateverQCONFLICT (file/directory)QCONFLICT (file/directory): directory in the way of whatever from tweak1; moving it to whatever~tweak1 instead.
     ++	Q1Qwhatever~tweak1QCONFLICT (modify/delete)QCONFLICT (modify/delete): whatever~tweak1 deleted in side2 and modified in tweak1.  Version tweak1 of whatever~tweak1 left in tree.
     ++	Q1QΑυτά μου φαίνονται κινέζικαQAuto-mergingQAuto-merging Αυτά μου φαίνονται κινέζικα
     ++	Q1QΑυτά μου φαίνονται κινέζικαQCONFLICT (contents)QCONFLICT (content): Merge conflict in Αυτά μου φαίνονται κινέζικα
     ++	Q
      +	EOF
      +
      +	test_cmp expect actual
 11:  db73c6dd823 = 16:  66df0c2e837 merge-tree: add a --allow-unrelated-histories flag
 12:  d58a7c7a9f6 ! 17:  70ea8281952 git-merge-tree.txt: add a section on potentional usage mistakes

@@ Documentation/git-merge-tree.txt: with linkgit:git-merge[1]:
+<<IM,Informational messages>> section has the necessary info, though it +is not designed to be machine parseable.
+
++Do NOT assume that each paths from <<CFI,Conflicted file info>>, and ++the logical conflicts in the <<IM,Informational messages>> have a ++one-to-one mapping, nor that there is a one-to-many mapping, nor a ++many-to-one mapping. Many-to-many mappings exist, meaning that each ++path can have many logical conflict types in a single merge, and each ++logical conflict type can affect many paths.
++
+Do NOT assume all filenames listed in the <<IM,Informational messages>> +section had conflicts. Messages can be included for files that have no +conflicts, such as "Auto-merging <file>".

Signature · 1 line
gitgitgadget
#224 lore raw

PATCH v7 1/17 merge-tree: rename merge_trees() to trivial_merge_trees()

From: Elijah Newren <[email protected]>

merge-recursive.h defined its own merge_trees() function, different than the one found in builtin/merge-tree.c. That was okay in the past, but we want merge-tree to be able to use the merge-ort functions, which will end up including merge-recursive.h. Rename the function found in builtin/merge-tree.c to avoid the conflict.

---
 builtin/merge-tree.c | 8 ++++----
 1 file changed, 4 insertions(+), 4 deletions(-)
builtin/merge-tree.c +4 -4
index 5dc94d6f880..06f9eee9f78 100644
@@ -28,7 +28,7 @@ static void add_merge_entry(struct merge_list *entry)
 	merge_result_end = &entry->next;
 }
 
-static void merge_trees(struct tree_desc t[3], const char *base);
+static void trivial_merge_trees(struct tree_desc t[3], const char *base);
 
 static const char *explanation(struct merge_list *entry)
 {
@@ -225,7 +225,7 @@ static void unresolved_directory(const struct traverse_info *info,
 	buf2 = fill_tree_descriptor(r, t + 2, ENTRY_OID(n + 2));
 #undef ENTRY_OID
 
-	merge_trees(t, newbase);
+	trivial_merge_trees(t, newbase);
 
 	free(buf0);
 	free(buf1);
@@ -342,7 +342,7 @@ static int threeway_callback(int n, unsigned long mask, unsigned long dirmask, s
 	return mask;
 }
 
-static void merge_trees(struct tree_desc t[3], const char *base)
+static void trivial_merge_trees(struct tree_desc t[3], const char *base)
 {
 	struct traverse_info info;
 
@@ -378,7 +378,7 @@ int cmd_merge_tree(int argc, const char **argv, const char *prefix)
 	buf1 = get_tree_descriptor(r, t+0, argv[1]);
 	buf2 = get_tree_descriptor(r, t+1, argv[2]);
 	buf3 = get_tree_descriptor(r, t+2, argv[3]);
-	merge_trees(t, "");
+	trivial_merge_trees(t, "");
 	free(buf1);
 	free(buf2);
 	free(buf3);
Signature · 1 line
gitgitgadget
#225 lore raw

PATCH v7 2/17 merge-tree: move logic for existing merge into new function

In reply to Elijah Newren via GitGitGadget

From: Elijah Newren <[email protected]>

In preparation for adding a non-trivial merge capability to merge-tree, move the existing merge logic for trivial merges into a new function.

---
 builtin/merge-tree.c | 12 ++++++++----
 1 file changed, 8 insertions(+), 4 deletions(-)
builtin/merge-tree.c +8 -4
index 06f9eee9f78..914ec960b7e 100644
@@ -366,15 +366,12 @@ static void *get_tree_descriptor(struct repository *r,
 	return buf;
 }
 
-int cmd_merge_tree(int argc, const char **argv, const char *prefix)
+static int trivial_merge(int argc, const char **argv)
 {
 	struct repository *r = the_repository;
 	struct tree_desc t[3];
 	void *buf1, *buf2, *buf3;
 
-	if (argc != 4)
-		usage(merge_tree_usage);
-
 	buf1 = get_tree_descriptor(r, t+0, argv[1]);
 	buf2 = get_tree_descriptor(r, t+1, argv[2]);
 	buf3 = get_tree_descriptor(r, t+2, argv[3]);
@@ -386,3 +383,10 @@ int cmd_merge_tree(int argc, const char **argv, const char *prefix)
 	show_result();
 	return 0;
 }
+
+int cmd_merge_tree(int argc, const char **argv, const char *prefix)
+{
+	if (argc != 4)
+		usage(merge_tree_usage);
+	return trivial_merge(argc, argv);
+}
Signature · 1 line
gitgitgadget